1. Introduction
Cross-site scripting is a problem for developers. Despite the existence of context-aware escaping mechanisms in templating systems, content injection continues to be one of of the most common attack vectors.
Content Security Policy [CSP] aims to reduce the risk of these attacks, and is widely supported by browsers. It has proven to be difficult to deploy, however, as the syntax is complicated, the overriding rules are confused, and the feature itself serves several different purposes. Recent research has shown flaws in real-world deployments result in policies that are ineffective at directly preventing cross-site scripting in the overwhelming majority of cases [CSP-IS-DEAD]. That research, and related discussions, has lead to new approaches within CSP (like those described in Content Security Policy Level 3 §strict-dynamic-usage) which seem promising, but the syntax ends up being somewhat verbose and the behavior confusing due to backwards compatibility concerns.
This document proposes a policy language focused strictly on XSS protection, with the goal of providing the same XSS mitigation guarantees as a verbose and complicated CSP, but in a pithy and comprehensible package.
ARTUR
header
and in the script
element’s nonce
attribute, as follows:
ARTUR: { "nonce": "abcdefg" }
<!-- This script will execute. --> <script src="script.js" nonce="abcdefg"></script> <!-- This script won’t (no nonce). --> <script src="script.js"></script>
2. Framework
This specification uses terminology from [RFC5234] and [HTTP-JFV] to define its syntax expectations, and relies heavily on [CSP] (which itself relies on [FETCH] and [HTML]) for enforcement.
2.1. Policy
An ARTUR policy defines a set of behaviors which are to be applied to
a Window
or WorkerGlobalScope
.
Each policy has a nonce which is a string. Its value is the empty string unless otherwise specified.
Each policy has a hash set which is a set of strings. Its value is the empty set unless otherwise specified.
Each policy has a reporting disposition which is either
"enforce
" or "report
". Its value is "enforce
" unless otherwise specified.
Each policy has a reporting group which is a string. Its
value is the string "default
" unless otherwise specified.
Each policy has a eval policy which is either "block
" or
"unsafe-eval
". Its value is "unsafe-eval
" unless otherwise specified.
2.2. Algorithms
2.2.1. Obtain a set of ARTUR policies for response.
Given a response (response), this algorithm returns a sequence of ARTUR policy objects. If no valid policy is found for response, the returned sequence will be empty.
-
Let policies be an empty sequence.
-
Let value be the result of parsing "
ARTUR
" in response’s header list. -
If value is
null
, return policies. -
Let list be the result of executing the algorithm defined in Section 4 of [HTTP-JFV] on value. If that algorithm results in an error, return policies.
-
For each item in list:
-
Let policy be a new ARTUR policy.
-
If item has a
hash
member whose value (value) is asequence<DOMString>
, then for each digest in value:-
If digest matches the ABNF grammar
hash-algorithm-hash-source
(as defined in [CSP]), then add digest to policy’s hash set.
-
-
If item has a
nonce
member whose value (value) is aDOMString
that matches the ABNF grammarbase64-value
(as defined in [CSP]), then set policy’s nonce to value. -
If item has a
report
member whose value (value) matches thetoken
grammar (as defined in [RFC7230]), then set policy’s reporting group to value. -
If item has a
eval
member whose value (value) is a validEvalPolicy
, then set policy’s eval policy to value. -
Append policy to policies.
-
-
Return policies.
3. The ARTUR
HTTP Response Header
The ARTUR
HTTP response header field is the delivery
mechanism for the mitigations this document offers. The header’s value is a serialized ARTUR
policy represented by the following ABNF [RFC5234]:
ARTUR = json-field-value
ARTUR: { "nonce": "abcdefg", "report": "group1" }
This header will have more or less the same effect as delivering the following header:
Content-Security-Policy: script-src 'strict-dynamic' 'nonce-abcdegf'; object-src 'none'; report-to group1;
A server MAY send different ARTUR
header field values with different representations of the same
resource.
The header’s value is interpreted as an array of JSON objects, as described in Section 4 of [HTTP-JFV]. Detailed parsing instructions are found in §2.2.1 Obtain a set of ARTUR policies for response., but the syntax boils down to a representation of the following dictionary type:
enum EvalPolicy { "block", "unsafe-allow" }; dictionary ArturPolicy { EvalPolicy eval = "unsafe-allow"; sequence<DOMString> hash; DOMString nonce; DOMString report = "default"; boolean reportOnly = false; };
The following subsections define the behavior of each of the known members the header’s value defines. Future versions of this document may define additional members; user agents MUST ignore unknown members when parsing the header.
3.1. The eval
member
The eval member defines the page’s policy towards eval()
. For increased security, developers can set the value to "block
", which causes eval()
to throw a SecurityError
rather than executing a string as code. It is set to "unsafe-allow
"
by default in order to keep policies simple, as we’ve seen widespread-enough usage of eval()
to
make it hard to deny by fiat.
eval()
for various purposes. They can
allow such execution by sending an appropriate eval
member along with
their ARTUR policy:
ARTUR: { "hash": [ "sha256-abcd...", "sha256-zyx...", "eval": "unsafe-allow" ] }
3.2. The hash
member
The hash member defines a set of digests which specify specific script content which is allowed to execute on a given page.
If present, the member’s value MUST be a sequence of DOMString
s, and each MUST adhere to the hash-source grammar specified in Content Security Policy Level 3 §framework-directive-source-list. Any
item in the sequence that does not match that grammer MUST be ignored.
script
elements do not execute. They can do so
by executing a secure hash function (like those specified in [SHA2]) on each script they wish to
execute, and including the resulting list of digests in an ARTUR
header. These digests either match the inline content of a script
element or event handler, or
match a script
element’s integrity
attribute:
ARTUR: { "hash": [ "sha256-abcd...", "sha256-zyx..." ] } ... <script src="script.js" integrity="sha256-abcd..."></script> <script> // Content which hashes toabcd...
goes here. </script> <a onclick="// Content which hashes tozyx...
goes here.">Click!</a>
Matching inline event handlers requires some changes to CSP. <https://github.com/w3c/webappsec-csp/issues/13>
Matching external scripts requires solidifying CSP’s SRI integration. <https://github.com/w3c/webappsec-csp/issues/78>
3.3. The nonce
member
The nonce
member defines a cryptographic
nonce ("number used once") which can be used to ensure that only script
elements whose nonce
attributes contain a specific value are allowed to execute script for a given page.
If present, the member’s value MUST be a DOMString
, and MUST adhere to the base64-value grammar specified in Content Security Policy Level 3 §framework-directive-source-list. If the
member’s value does not match that grammar, the value MUST be ignored.
script
elements do not execute. They can do so
by generating a unique string for each page load, and including that string both in an ARTUR
header and in the script
element’s nonce
attribute, as follows:
ARTUR: { "nonce": "abcdefg" } ... <script src="script.js" nonce="abcdefg"></script>
3.4. The report
member
The report
member defines the reporting
group to which violation reports will be sent [REPORTING].
The member’s value MUST be a DOMString
. If an empty string is provided, reporting will be
disabled.
Note: Reporting to the group named "default
" is enabled unless otherwise specified.
report
member along with the
policy, and specifying a reporting endpoint, as follows:
ARTUR: { "nonce": "abcdefg", "report": "group1" } Report-To: { "url": "https://example.com/report", "group": "group1", "max-age": 10886400 }
3.5. The reportOnly
member
The reportOnly
member defines whether the
policy specified will actually block script execution, or whether it will operate in "report-only"
mode, sending violation reports up to a server-side collector while allowing script execution.
report
and reportOnly
members along with the policy, and specifying a reporting
endpoint, as follows:
ARTUR: { "nonce": "abcdefg", "report": "group1", "reportOnly": true } Report-To: { "url": "https://example.com/report", "group": "group1", "max-age": 10886400 }
4. Integrations
The ARTUR
header is little more than syntactic sugar on top of existing
processing algorithms for [CSP]. This section explains the ways in which the ARTUR
policy is enforced for a given context:
4.1. Integration with Fetch
-
When executing Content Security Policy Level 3 §set-response-csp-list (currently as step 3.3.4 of HTTP fetch, and step 12 of HTTP-network fetch), Fetch should also execute the algorithm defined in §4.1.1 Populate response’s CSP list from its ARTUR policy..
Upstream this once we decide it’s sane.
4.1.1. Populate response’s CSP list from its ARTUR policy.
Given a response (response), this algorithm modifies its CSP list to enforce the ARTUR policy it specifies.
-
For each policy in the result of executing §2.2.1 Obtain a set of ARTUR policies for response. on response:
-
If policy’s nonce is an empty string and policy’s hash set is empty, then skip the remaining substeps and move to the next policy.
-
Assert: policy’s nonce is either an empty string or matches the base64-value grammar specified in [CSP]. Likewise, policy’s hash set is either empty or all its values match the grammar "hash-algorithm-hash-source". Further, policy’s reporting group is a string that matches the
token
grammar defined in [RFC7230]. -
Let csp be a new Content Security Policy with an empty directive set, and a disposition of "
enforce
" if policy’s reporting disposition is "enforce
", and "report
" otherwise. -
Let script-src be the string "
'strict-dynamic'
". -
If policy has a eval policy of "
unsafe-allow
", then append "'unsafe-eval'
" to script-src. -
If policy’s nonce is not an empty string, append to script-src the result of replacing "
%s
" in the string "'nonce-%s'
" with policy’s nonce. -
For each digest in policy’s hash set:
-
Append to script-src the result of replacing "
%s
" in the string "'%s'
" with digest.
-
-
Add a new directive to csp’s directive set whose name is "
script-src
", and whose value is script-src. -
Add a new directive to csp’s directive set whose name is "
object-src
", and whose value is "'none'
". -
If policy has a reporting group (group) which is not an empty string, then add a directive to csp whose name is "
report-to
", and whose value is group. -
Append policy to response’s CSP list.
-
5. Implementation Considerations
5.1. Error Messages
The ARTUR
mechanism is defined almost entirely in terms of [CSP].
This is intentional, as it means that user agents can easily reuse all the existing machinery
they’ve built up for that purpose. It might be confusing for developers, however, especially if,
in the distant future, CSP becomes an esoteric thing that no one uses on its own. Error messages,
in particular, might be confusing if they contain the synthesized CSP headers rather than the
headers the developer actually sent.
That is, violation report console messages might look something like "Refused to execute inline
event handler because it violates the following Content Security Policy directive: 'script-src
'strict-dynamic' 'nonce-abcdefg'
...". That is technically accurate (the best kind of
accurate), but not particularly helpful.
User agents SHOULD ensure that the source of the policy is clear when triggering violations.
Perhaps we should even extend the SecurityPolicyViolationEvent
and corresponding
violation report?
6. Acknowledgements
This proposal arose from a conversation in the WebAppSec meetings at TPAC 2016. Thanks to Artur Janc for being a good sport about the original name (the lovingly backronymed ""), to Richard Barnes for being appropriately skeptical at the value provided, and to Sebastian Lekies and Krzysztof Kotowicz for being CSP gadflies in the most honorably Socratic tradition.