1. Introduction
Though cross-site scripting (XSS) is technically trivial to mitigate through proper escaping techniques, it remains a somewhat pervasive risk to web applications. Content Security Policy attempts to contain this risk by giving developers more control over the way script is executed on a given page, and developers have had moderate success at rolling it out. They’ve also expressed more than a little frustration with the complexity that’s crept into CSP over time.
Originally, CSP aimed to address script injection attacks by limiting the sources from which
scripts could be loaded. Over time, we learned that limiting scripts' sources was not a terribly
effective way of addressing the problem. [CSP-IS-DEAD] shifted experts' recommendations away from
a list of allowed sources for script, and towards more robust verification of individual script
elements via nonce
attributes on the one hand, or integrity verification on
the other (see Content Security Policy §8.4 Allowing external JavaScript via hashes).
The [ARTUR] proposal is a tounge-in-cheek proposal that’s a fairly bad idea as specified, but seems like an interesting foundation conceptually. If we step back from CSP’s current syntax, and the contortions it takes to remain backwards compatible, it seems like we could extract a concise and comprehensible mechanism that would encompass the kinds of requirements that hard experience has taught us can be effective (e.g. those expressed in [STRICT-CSP]):
-
Turn off dangerous parts of the platform that influence scripting (e.g.
base
,embed
, andobject
. -
Create an out-of-band signal that a given script is intended to execute (e.g. the aforementioned nonces and hashes).
-
Allow developers to roll the above out in a report-only manner.
This document aims to build upon that conceptual foundation, defining a Scripting Policy that governs the way script executes within a given context, focused entirely on mitigating injection attacks in a way that developers can easily deploy.
1.1. Examples
Most users would be well-served with the following policy:
Scripting-Policy: nonce=number-used-once
This would have the effect of:
-
Executing "parser-inserted" script iff it has a
nonce
attribute matching the header-specified nonce (e.g.<script nonce="number-used-once">...</script>
), and executing all non-"parser-inserted" script in a manner similar to CSP’s'strict-dynamic'
keyword (see Content Security Policy §8.2 Usage of "'strict-dynamic'"). -
Preventing
base
from changing the meaning of relative URLs. -
Blocking
eval(
, while allowingDOMString
)eval(
.TrustedScript
) -
Blocking inline event handlers, XSLT, navigation to
javascript:...
,embed
, andobject
.
This is a pretty reasonable set of hurdles to put in front of an attacker.
Users with complicated sites might need more complicated policies that take advantage of optional features, perhaps along the lines of:
Scripting-Policy: integrity=(hash1 hash2 hash3 hash4), report-to=name, trusted-types-policy=policyName Scripting-Policy-Report-Only: integrity=(hash1 hash2 ...hash18 ... hash37), eval=block, dynamic-loading=checked, report-to=name, trusted-types-policy=policyName
1.2. Threat Model
Scripting Policy aims to deal soley with injection attacks that cause script execution. It does not try to affect resource loading, mitigate data exfiltration, nor does it gate access to any feature unrelated to script execution.
This mechanism assumes an attacker can:
-
Cause a server to output unexpected content directly into the body of any given response, leading to "reflected" cross-site scripting.
-
Manipulate the inputs to client-side code, causing "stored" or "DOM-based" cross-site scripting.
It does not address other, related attacks. In particular, it assumes that attackers cannot reliably inject headers into a server’s response.
1.3. Overview
This section is non-normative (but hopefully helpful anyway).
At its core, this story this document wants to tell is the following:
-
Scripting Policy objects define the ways in which script can be executed within a given Document, Worker, etc. They have a number of properties that influence different aspects of script execution, defined in some detail in § 2 Framework.
-
These policies are delivered as HTTP response headers:
Scripting-Policy
defines a policy to be enforced in a given context, andScripting-Policy-Report-Only
a policy to be reported upon, but not enforced.The headers' syntax is defined in § 2.1 Scripting Policy HTTP Response Headers, and parsing rules are in § 2.2 Parsing Scripting Policy Headers.
-
Policies are bound to
Document
,WorkerGlobalScope
, andWorkletGlobalScope
objects when each is initialized. Each object obtain a policy from the HTTP response used when initializing the document or worker, or from the context responsible for the document’s navigation or worker’s instantiation if that response has a local scheme (e.g.data:
,about:
,blob:
, etc.). These details are spelled out in § 2.4 Initialize a Document’s Scripting Policy and § 2.5 Initialize a global object’s Scripting Policy. -
A policy’s effects are enforced in a given context through hooks in HTML:
-
Nonces and hashes are enforced by policy in HTML’s prepare a script algorithm, and rely upon the existing
nonce
andintegrity
attributes, respectively, for enforcement. Hashes further rely upon Subresource Integrity [SRI] enforcement in Fetch’s main fetch algorithm. -
Inline event handlers rely upon integrity assertions for enforcement. Details in § 2.6.2 Checking Inline Event Handlers.
-
base
's effects are gated via hooks in the set the frozen base URL algorithm, detailed in § 2.6.4 Checking <base>. -
embed
andobject
are gated via hooks in their respective intialization mechanisms, detailed in § 2.6.5 Checking <embed> and <object>.. -
eval()
is gated via HTML’s implementation of JavaScript’s HTML’s implementation of JavaScript’sHostEnsureCanCompileStrings()
abstract operation, detailed in § 2.6.3 Checking String Compilation (eval() and friends). -
Reporting relies upon the Reporting API [REPORTING], defining a "
scripting-policy
" report type via theScriptingPolicyReportBody
interface. Details in § 2.7 Reporting Violations.
-
The rest of the document’s structure follows this general outline: § 2 Framework defines the concepts we’ll need, and the header delivery mechanism. § 2.3 Policy Application shows how policies are associated with the contexts that love them. § 2.6 Policy Enforcement explains how HTML and Fetch make decisions about how code is executed.
Enjoy!
1.4. Dependencies
The rest of the document relies upon Structured Headers to define policies' header syntax [I-D.ietf-httpbis-header-structure]. It also depends upon the Infra Standard for a number of foundational concepts used in its algorithms and prose [INFRA].
2. Framework
A Scripting Policy defines the characteristics of
script execution in a given context, and may have effect on a Document
, WorkerGlobalScope
,
or WorkletGlobalScope
, as described in § 2.4 Initialize a Document’s Scripting Policy and § 2.5 Initialize a global object’s Scripting Policy.
Each policy has a nonce member, which is either null
or a string representing a cryptographic "number-used-once". Unless otherwise specified, its value
is null
.
Note: The nonce
member defines a "number-used-once" which must be reflected in script
elements' nonce
attribute in order to enable script execution. This check is
performed during HTML’s prepare a script algorithm, as described in § 2.6 Policy Enforcement.
Each policy has an integrity member, which is either null
, or a list either null
or a list of integrity metadata objects. Unless otherwise specified, its
value is null
.
Note: The integrity
member is a list of integrity metadata that define the set of scripts
which can be executed on a given page, layering on top of [SRI] for enforcement. This check is
performed during Fetch’s main fetch algorithm, as described in § 2.6 Policy Enforcement.
Each policy has an eval member, which is either "allow
",
"blocked
", or "allow-trustedscript
". Unless otherwise specified, its value is
"allow-trustedscript
".
Note: The eval
member influences the way eval()
is processed (shocking, right?). "allow
"
places no restriction upon eval()
. "blocked
" causes both eval(TrustedScript)
and eval(DOMString)
to throw. "allow-trustedscript
" causes eval(DOMString)
to throw, but allows eval(TrustedScript)
to execute. This is detailed in § 2.6.3 Checking String Compilation (eval() and friends).
Each policy has a report-to member, which is either null
,
or a string representing a reporting group, as defined in [REPORTING].
Unless otherwise specified, its value is null
.
Note: The report-to
member wires Scripting Policy enforcement up to the Reporting API in order to
inform developers about violations on their pages. This is detailed in § 2.7 Reporting Violations.
Each policy has a trusted-types-required-for member, which
is either null
, or a list of strings representing categories for which Trusted Types are
enforced. Unless otherwise specified, its value is null
.
Note: The trusted-types-require-for
member configures Trusted Type enforcement. The only currently
valid category is "script
".
Each policy has a dynamic-loading member, which is either
"allow-non-parser-inserted
", or "check-non-parser-inserted
". Unless otherwise specified, its
value is "allow-non-parser-inserted
".
Note: The dynamic-loading
member controls how script that executes in a given context can cause
more script to execute. "allow-non-parser-inserted
" means that non-[=script/"parser-inserted"=]
scripts will execute, regardless of nonce or integrity enforcement. "check-non-parser-inserted
" will not privilege non-[script/"parser-inserted"=]
script in any way, applying all checks that were applied to the original script.
Names are hard, but {allow,check}-non-parser-inserted
are terrible.
Scripting Policies are often found in a Scripting Policy pair, combining
one enforced policy with one policy which is unenforced, and merely reported upon. This is a pair whose first item is named enforced and whose second item is named report-only.
Both items are either null
or a Scripting Policy object. Unless otherwise
specified, their values are both null
.
2.1. Scripting Policy HTTP Response Headers
Servers can serialize Scripting Policy objects, and deliver them to user agents via HTTP
response headers that declare the scripting requirements for a given response. The Scripting-Policy
HTTP response header informs a user agent about
the scripting requirements which are to be enforced for a given response. The Scripting-Policy-Report-Only
HTTP response header has the same
grammar and semantics as Scripting-Policy
, but declares a policy that is not
enforced in a given context, but merely reported upon.
Both are Structured Headers whose value MUST be a dictionary [I-D.ietf-httpbis-header-structure]. Their ABNF is:
Scripting-Policy = scripting-policy-dict Scripting-Policy-Report-Only = scripting-policy-dict scripting-policy-dict = sh-dictionary
Note: The sh-dictionary type is defined along with Structured Header’s dictionary concept in Section 3.2 of [I-D.ietf-httpbis-header-structure].
The scripting-policy-dict
dictionary MAY contain one or
more of the following members:
- nonce
-
The member’s value is a token, which maps to the Scripting Policy's nonce member.
- integrity
-
The member’s value is a list of token values, which are processed as integrity metadata values, and map to the Scripting Policy's integrity member.
- eval
-
The member’s value is an enum represented as one of the following tokens: "
allow
", "blocked
", and "allow-trustedscript
". It maps to the Scripting Policy's eval member. - report-to
-
The member’s value is a token, which maps to the Scripting Policy's report-to member.
- trusted-types-required-for
-
The member’s value is a list of token values, which maps to the Scripting Policy's trusted-types-required-for member.
- dynamic-loading
-
The member’s value is an enum represented as one of the following tokens: "
allow-non-parser-inserted
", and "check-non-parser-inserted
". It maps to the Scripting Policy's dynamic-loading member.
Both headers are currently meaningful only for responses that result in a navigation to a new
document, as well as for responses that are executed as Worker
, SharedWorker
, and ServiceWorker
. Detailed processing rules are found in § 2.6 Policy Enforcement.
2.2. Parsing Scripting Policy Headers
-
Let headers be r’s header list.
-
Let result be a new Scripting Policy pair.
-
Let header be the result of getting
Scripting-Policy
from headers. -
If header is not
null
, set result’ enforced to the result of obtaining a Scripting Policy from header. -
Let header be the result of getting
Scripting-Policy-Report-Only
from headers. -
If header is not
null
, set result’ report-only to the result of obtaining a Scripting Policy from header. -
Return result.
null
if no policy can be parsed successfully.
-
Let policy be a new Scripting Policy object.
-
Let struct be the result of parsing a Structured Header with an
input_bytes
of header and aheader_type
of dictionary.If parsing fails, return
null
. -
For each key → value of struct, byte-lowercase key and switch on the result:
- "
dynamic-loading
" -
-
If value is not contained in the list « "
allow-non-parser-inserted
", "check-non-parser-inserted
" », continue. -
Set policy’s dynamic-loading member to value.
- "
eval
" -
-
If value is not contained in the list « "
allow
", "allow-trustedscript
", "block
" », continue. -
Set policy’s eval member to value.
- "
integrity
" -
Parse the integrity metadata, similar to the definition SRI §3.3.3 Parse metadata.. This might be as simple as joining the list on space, and running it through exactly that algorithm.
Ensure that we normalize the values to base64 from base64url.
- "
nonce
" - "
report-to
" - "
trusted-types-required-for
" -
-
If value is not contained in the list « "
script
" », continue. -
Set policy’s trusted-types-required-for member to value.
- "
-
Return policy.
Note: Parsing errors fail open for compatibility with future changes to the dictionary names and values, as well as the underlying syntax. In particular, if parsing fails because the header cannot be interpreted as a Structured Header per the parsing rules defined in [I-D.ietf-httpbis-header-structure], no policy will be applied. This explicitly prioritizes forward compatibility with future iterations of Structured Headers, which may be surprising. User agents are encouraged to warn developers in this case.
2.3. Policy Application
Each context that can execute script has at most one Scripting Policy that governs script execution in that context.
Monkeypatching [HTML]:
Document
objects have a scripting policy, which is a Scripting Policy pair.
WorkerGlobalScope
objects have a scripting policy, which is a Scripting Policy pair.
WorkletGlobalScope
objects have a scripting policy, which is a Scripting Policy pair.Environment settings objects have an algorithm to obtain a scripting policy, defined as follows:
A
Window
objects’s environment settings object's scripting policy algorithm returns the scripting policy of theWindow
's associated Document.A
WorkerGlobalScope
objects’s environment settings object's scripting policy algorithm returns the scripting policy of theWorkerGlobalScope
.A
WorkletGlobalScope
objects’s environment settings object's scripting policy algorithm returns the scripting policy of theWorkletGlobalScope
.
2.4. Initialize a Document’s Scripting Policy
A Document
's scripting policy comes from one of two places: the response to
which the user agent navigated, or the environment settings object responsible for a navigation
to a local scheme (e.g. data:
, blob:
, about:
). In particular, note that this latter case
covers <iframe srcdoc>
.
Document
(document), a response (response), and a request or null
(request), the user agent can initialize a Document
's Scripting Policy by executing the following steps:
-
If request is not
null
, and response’s url's scheme is a local scheme:-
Set document’s scripting policy to a copy of request’s client's scripting policy.
-
Return.
Does this cover
about:srcdoc
? Presumably this will be simplified in the future. <https://github.com/whatwg/html/issues/4926> -
-
Set document’s scripting policy to the result of obtaining a Scripting Policy pair from response.
This algorithm should be called from HTML’s create and initialize a Document object algorithm, directly after the existing hook for CSP:
Monkeypatching [HTML]'s create and initialize a Document object algorithm:
Initialize a
Document
's Scripting Policy given document, response, and request.
2.5. Initialize a global object’s Scripting Policy
A global object's scripting policy comes from one of two places: the response used to initialize a given worker, or the environment settings object responsible
for intializing a worker from a local scheme (e.g. new Worker('data:...');
)
-
If response’s url's scheme is a local scheme:
-
Assert: global is a
DedicatedWorkerGlobalScope
, aWorkletGlobalScope
, or aSharedWorkerGlobalScope
with one item in its owner set. -
With the single owner in global’s owner set, set global’s scripting policy to a copy of owner’s relevant settings object's scripting policy.
-
Return.
Is the assertion above correct? Presumably this will be simplified in the future. <https://github.com/whatwg/html/issues/4926>
-
-
Set global’s scripting policy to the result of obtaining a Scripting Policy pair from response.
This algorithm should be called from HTML’s run a worker algorithm, directly after the existing hook for CSP:
Monkeypatching [HTML]'s run a worker algorithm:
Obtain script ...
In both cases, ...
...
Initialize a global object’s Scripting Policy given worker global scope and response.
2.6. Policy Enforcement
2.6.1. Checking Nonces and Hashes
Scripting Policy’s restrictions on script
execution are generally performed during HTML’s prepare a script algorithm, before requests are fired for externalized scripts, and prior to
evaluating inline scripts.
Note: If a policy sets requirements for both a nonce and some set of integrity, either will be sufficient to allow script execution. That is,
the policy nonce=abcdefg, integrity=(sha256-123456)
would allow execution for each of <script nonce="abcdefg"></script>
, <script></script>
, and <script nonce="abcdefg"></script>
.
Element
(el), execute the following
algorithm. It will return "Blocked
" or "Allowed
".
-
Assert: el is an
HTMLScriptElement
or anSVGScriptElement
. -
Let enforced be el’s node document's scripting policy's enforced.
-
Let report-only be el’s node document's scripting policy's report-only.
-
If report-only is not
null
: -
If enforced is not
null
: -
Return "
Allowed
".
This relies on SRI doing something useful for inline script blocks. We should probably make that happen. <https://github.com/w3c/webappsec-subresource-integrity/issues/44>
2.6.2. Checking Inline Event Handlers
Inline event handlers (e.g. <a onclick="amazing_javascript();">...</a>
) are allowed iff they
match integrity metadata explicitly allowed via a policy’s integrity list.
Match
":
-
Set source to the result of executing UTF-8 encode on the result of executing JavaScript string converstion on value.
-
For each integrity metadata in list:
-
Let expected be integrity metadata’s digest.
-
Switch on integrity metadata’s algorithm:
- "
sha256
" -
-
If expected is a case-sensitive match with the result of base64 encoding the result of applying SHA-256 to source, return "
Matches
".
-
- "
sha384
" -
-
If expected is a case-sensitive match with the result of base64 encoding the result of applying SHA-384 to source, return "
Matches
".
-
- "
sha512
" -
-
If expected is a case-sensitive match with the result of base64 encoding the result of applying SHA-512 to source, return "
Matches
".
-
- "
-
-
Return "
Does Not Match
".
-
Let enforced be el’s node document's scripting policy's enforced.
-
Let report-only be el’s node document's scripting policy's report-only.
-
If report-only is not
null
: -
If enforced is not
null
: -
Return "
Allowed
".
Monkeypatching [HTML]'s event handler content attribute's attribute change steps:
Otherwise:
...
If Should an event handler be blocked by Scripting Policy? returns "
Blocked
" when executed upon element and value, then return.
2.6.3. Checking String Compilation (eval()
and friends)
To address eval()
, Scripting Policy modifies HTML’s implementation of
JavaScript’s HostEnsureCanCompileStrings
abstract operation to include verification against
the context’s policy.
EvalError
if not:
-
Let globals be a list containing callerRealm’s global object and calleeRealm’s global object.
-
For each global in globals:
-
Let enforced be global’s relevant settings object's scripting policy's enforced.
-
Let report-only be global’s relevant settings object's scripting policy's report-only.
-
If report-only is not
null
, switch on its eval value: -
If enforced is not
null
, switch on its eval value:
-
-
Return.
HostEnsureCanCompileStrings()
does not include the string which is
going to be compiled as a parameter. We’ll also need to update HTML to pipe that value through. <https://github.com/tc39/ecma262/issues/938>
Monkeypatching [HTML]'s HostEnsureCanCompileStrings():
Perform ? EnsureCSPDoesNotBlockStringCompilation(callerRealm, calleeRealm). [CSP]
Perform ? EnsureScriptingPolicyDoesNotBlockStringCompilation(callerRealm, calleeRealm).
2.6.4. Checking <base>
Scripting Policy blocks <base>
from pointing to any cross-origin URL, as this can change the
meaning of a script tag in unexpected and dangerous ways.
Element
(el) and a URL (url), execute
the following algorithm. It will return "Blocked
" or "Allowed
".
-
If url’s origin is same origin with el’s node document's relevant settings object's origin, return "
Allowed
". -
Let enforced be el’s node document's scripting policy's enforced.
-
Let report-only be el’s node document's scripting policy's report-only.
-
If report-only is not
null
:-
Report a Scripting Policy violation, using report-only, handler, and el.
-
-
If enforced is not
null
:-
Report a Scripting Policy violation, using enforced, handler, and el.
-
Return "
Blocked
".
-
-
Return "
Allowed
".
Monkeypatching [HTML]'s set the frozen base URL algorithm:
Insert the algorithm above into the current step 3, as follows:
Set element’s frozen base URL to document’s
fallback base URL
, if urlRecord is failure orrunning Is base allowed for Document? on the resulting URL record and document returns "Blocked"eitherIs base allowed by Content Security Policy?
or Is base allowed by Scripting Policy? return "Blocked
" when executed upon theresulting URL record
and document, and to urlRecord otherwise.
2.6.5. Checking <embed>
and <object>
.
Scripting Policy blocks <embed>
and <object>
, as they have capabilities in line with (and
beyond the capabilities of) script execution.
Element
(el), execute the following algorithm.
It will return "Blocked
" or "Allowed
".
-
Let enforced be el’s node document's scripting policy's enforced.
-
Let report-only be el’s node document's scripting policy's report-only.
-
If report-only is not
null
:-
Report a Scripting Policy violation, using report-only, handler, and el.
-
-
If enforced is not
null
:-
Report a Scripting Policy violation, using enforced, handler, and el.
-
Return "
Blocked
".
-
-
Return "
Allowed
".
Monkeypatching [HTML]'s
object
type determination stepsInsert the algorithm above into the current step 2, as follows:
If the element has an ancestor media element, or has an ancestor object element that is not showing its fallback content, or if the element is not in a document whose browsing context is non-null, or if the element’s node document is not fully active, or if the element is still in the stack of open elements of an HTML parser or XML parser, or if the element is not being rendered, or if the Should element be blocked a priori by Content Security Policy? algorithm returns "Blocked" when executed on the element, or if the Should a plugin element be blocked by Scripting Policy? algorithm returns "
Blocked
" when executed on the element, then jump to the step below labeled fallback.
This might be too strict, and we might want to have something akin to CSP’s plugin-types
so folks can allow PDF in user agents that use PDFium? Or have a carveout similar to the sandboxed plugins browsing context flag concept of a "secured plugin"?
2.7. Reporting Violations
Scripting Policy reports provide developers with context when a given page or
worker violates its policy. These violations are reported to developers via the Reporting API,
which exposes violations via the ReportingObserver
interface, as well as via reports delivered to endpoints configured via the Report-To
header as defined in [REPORTING].
Scripting Policy reports have a report type of "scripting-policy
", and are visible to ReportingObservers.
A Scripting Policy report's body is represented in JavaScript by the ScriptingPolicyReportBody
interface:
enum {
ScriptingPolicyViolationType ,
"externalScript" ,
"inlineScript" ,
"inlineEventHandler" }; [
"eval" Exposed =(Window ,Worker ),SecureContext ]interface :
ScriptingPolicyReportBody ReportBody { [Default ]object ();
toJSON readonly attribute DOMString violationType ;readonly attribute USVString ?violationURL ;readonly attribute USVString ?violationSample ;readonly attribute unsigned long lineno ;readonly attribute unsigned long colno ; };
violationType
, of type DOMString, readonly-
This attribute, to your certain astonishment, reflects the violation’s type:
-
"
externalScript
" reflects a violation when fetching a script (e.g.<script src="...">
). -
"
inlineScript
" reflects a violation from an inline script block (e.g.<script>...</script>
). -
"
inlineEventHandler
" reflects a violation from an inline event handler (e.g.<a onclick="...">
). -
"
eval
" reflects a violation fromeval()
,new Function()
, etc.
-
violationURL
, of type USVString, readonly, nullable-
When the
violationType
is "externalScript
", this attribute will contain that script’s URL. It will benull
for any other type. violationSample
, of type USVString, readonly, nullable-
When the
violationType
is not "externalScript
", this attribute will contain additional detail about the violation’s cause. When inline event handlers cause a violation, for example, this attribute will contain the first 40 characters of the handler’s source.When the
violationType
is "externalScript
", this attribute will benull
. lineno
, of type unsigned long, readonlycolno
, of type unsigned long, readonly-
These attributes contain the line and column numbers, respectively, associated with a violation, if the user agent can obtain them. If no positional information can be obtained, these attributes will both be 0.
3. Security and Privacy Considerations
4. IANA Considerations
The permanent message header field registry should be updated with the following registrations [RFC3864]:
4.1. Scripting-Policy
Registration
- Header field name
-
Scripting-Policy
- Applicable protocol
-
http
- Status
-
standard
- Author/Change controller
-
Me
- Specification document
-
This specification (See § 2.1 Scripting Policy HTTP Response Headers)
4.2. Scripting-Policy-Report-Only
Registration
- Header field name
-
Scripting-Policy-Report-Only
- Applicable protocol
-
http
- Status
-
standard
- Author/Change controller
-
Me
- Specification document
-
This specification (See § 2.1 Scripting Policy HTTP Response Headers)
5. Acknowledgements
This document owes much to those developers who have struggled with Content Security Policy
deployments over the years. In particular, Google (