Content Security Policy: strict-dynamic + (cached) nonce
Introduction
Content Security Policy is a browser feature that helps prevent and mitigate certain types of threats and attacks.
There are multiple directives to restrict and control which resources can be loaded on your webpage by the browser, which other domains are allowed to iframe your content, etc.
This guide is very specific for mitigating Cross Site Scripting (XSS) attacks with a specific Content Security Policy configuration in AEM Edge Delivery Services and is not intended to be a general, exhaustive guide of all the Content Security Policy options and possibilities supported by the browser.
Important Note: Content Security Policies are meant as a last line of defense when other measures fail. It is not intended to replace the use of safe DOM APIs and proper sanitisation of user input by developers in the code. It will not cover every single attack path and it is not meant to. It is always recommended that developers code in a safe manner.
The specific content security policy configuration recommended by this guide is:
script-src 'nonce-aem' 'strict-dynamic'; base-uri 'self'; object-src 'none';
As a short summary: This content security policy will allow the execution of only those scripts which have the correct nonce value at top level (nonce
component) and any script loaded by these and their descendants client-side, creating a trust chain, as long as the additional scripts are loaded via non-”parser-inserted” elements (strict-dynamic
component). More information
We have chosen this specific content security policy after reviewing the following:
- The findings of the following Google Research paper in regards to host based CSPs
- Ease of deployment and maintenance on customer sites (through the use of
strict-dynamic
) - The number of threats it mitigates as opposed to other alternatives
- How well it fits in the AEM Edge Delivery Services architecture
- The recommendation coming from the Google Lighthouse Report
Customers can choose to add other directives to complement this one to meet their security needs.
Configuration
This feature is currently available only for sites using version 5 of the AEM Edge Delivery Services architecture.
There are two components to the configuration:
- Trusted Scripts
- Policy Enforcement.
Instructions below show how to mark trusted scripts in your repository and then how to configure the policy to be enforced.
After the enforcement is put in place, AEM will replace the aem
part of the nonce in both the policy and the script attributes with a cryptographic random string, for every request that hits the rendering engine, generating new HTML markup from content. Once the HTML markup + headers is generated, it is cached on the CDN and is considered immutable.
Requests that hit the CDN cache will see the same nonce value until cache expiration.
The result should look as follows. If you don’t see the nonce generating a random value, it means the configuration was not correctly done.
1. Trusted Scripts
First, no matter how you choose to enforce it, you must add the nonce attribute marker (nonce="aem"
) to the scripts you trust in HTML. This can be in:
1.1. head.html
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<script nonce="aem" src="/scripts/aem.js" type="module"></script>
<script nonce="aem" src="/scripts/scripts.js" type="module"></script>
<link rel="stylesheet" href="/styles/styles.css"/>
1.2. Other static HTML files from your repository (for example 404.html
)
2. Policy Enforcement
For enforcing the policy you can configure it via either
- Header (recommended)
<meta>
tag
It is recommended that the Content Security Policy is delivered to the browser using a header. That way, the browser will apply it before scripts get a chance to execute. When using the meta tag the browser will only apply the policy after it is encountered during parsing, providing no protection for anything that is above it in the HTML.
2.1 Header Configuration
You can choose to configure either the content-security-policy
or the content-security-policy-report-only
headers with the following value:
script-src 'nonce-aem' 'strict-dynamic'; base-uri 'self'; object-src 'none';
Note: The content-security-policy-report-only
header doesn’t offer any protection, it only allows you to test if there are any scripts which would be blocked when the policy is fully enforced through error messages.
This can be done using one of the following methods:
2.1.1 The configuration service
curl -X POST https://admin.hlx.page/config/acme/sites/website/headers.json \
-H 'content-type: application/json' \
-H 'x-auth-token: <your-auth-token>' \
--data '{
"/**": [
{
"key": "content-security-policy",
"value": "script-src 'nonce-aem' 'strict-dynamic'; base-uri 'self'; object-src 'none';"
}
]
}'
2.1.2 Headers sheet
2.1.3 A specific attribute for the meta tag move-as-header="true"
(only for content-security-policy
and only when using a nonce based CSP).
This method can be useful for trying out the configuration in a branch, but has the disadvantage that it needs to be added to head.html
and every other static HTML file from your repository.
<meta
http-equiv="content-security-policy"
content="script-src 'nonce-aem' 'strict-dynamic'; base-uri 'self'; object-src 'none';"
move-as-header="true"
>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<script nonce="aem" src="/scripts/aem.js" type="module"></script>
<script nonce="aem" src="/scripts/scripts.js" type="module"></script>
<link rel="stylesheet" href="/styles/styles.css"/>
2.2 Meta Tag Configuration
You can configure the content security policy also as a meta tag. This is a browser out of the box feature, but it is considered less secure than using a header.
<meta
http-equiv="content-security-policy"
content="script-src 'nonce-aem' 'strict-dynamic'; base-uri 'self'; object-src 'none';"
>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<script nonce="aem" src="/scripts/aem.js" type="module"></script>
<script nonce="aem" src="/scripts/scripts.js" type="module"></script>
<link rel="stylesheet" href="/styles/styles.css"/>
Technical Deepdive - FAQ
1. Does caching the nonce render the content security policy ineffective?
Based on our current understanding and tests, no, that is not the case, given a set of assumptions, which hold true for typical sites implemented with AEM Edge Delivery Services.
Customers with implementations which break these assumptions should evaluate the effectiveness of the content security policy in the context of the new set of conditions specific for their website.
Assumptions:
- Websites do not make modifications server-side to the HTML produced by AEM Edge Delivery Services (for example in the customer’s CDN).
unsafe-eval
,unsafe-hashes
are not added to the content security policy.
Breaking this down into classes of Cross Site Scripting Attacks:
1. Stored XSS + Reflected XSS
These types of Cross Site Scripting attacks occur through injection server side.
Every new request that hits the AEM Edge Delivery Services’ HTML rendering pipeline will always receive a new nonce value.
This effectively mitigates these types of attacks, because attackers trying to inject a <script>
tag server side into the HTML cannot guess the new value of the nonce.
2. DOM Based XSS
This type of Cross Site Scripting attack occurs client side, when the client side Javascript injects user input (from query parameters, url fragments, author supplied information in HTML) without proper context specific encoding/sanitization into a unsafe DOM APIs which interpret the input as HTML or Javascript, instead of text.
The list below shows which use of DOM APIs* are considered mitigated and which aren't.
*Tested with Chrome, Mozilla, Safari
2.1 Mitigated even if the nonce is cached
✅ HTML inlined attribute event handlers (onclick
, onblur
, onload
, onerror
etc.). e.g: <img src='x' onerror='alert("xss")'>
When strict-dynamic
is present, browsers will ignore unsafe-inline
directive and will not execute inlined even handlers.
✅ Eval based XSS - (obviously, as long as unsafe-eval
is not present in the policy)
eval
/setTimeout
/setInterval
/Function
javascript:
protocol in thehref
/src
attributes (<a href=”javascript:alert(1)” >click me!</a>
)location.href
/location.assign()
/location.replace
(e.g.location.href = 'javascript:alert("xss")'
)
✅ .innerHTML
, .outerHTML
, .insertAdjecentHTML
, .setHTMLUnsafe
Modern browsers should not execute <script>
tags injected through these DOM APIs, according to the MDN documentation.
This leaves just
- HTML inlined event handlers, which as specified above are also mitigated
- attributes where the javascript context can be forced using
javascript:
, which as specified above are also mitigated, as long asunsafe-eval
is not present in the policy
2.2 not protected, because the nonce is cached
The following DOM APIs remain vulnerable, because looking up the value of the cached nonce can lead to a successful XSS.
❌ document.write
/ document.writeln
The MDN documentation strongly discourages the use of these APIs.
2.3 not protected, regardless if the nonce is cached
❌ document.createRange().createContextualFragment
whether the nonce is cached/known is irrelevant, scripts injected using this API are always executed when strict-dynamic
.
Currently reported to:
❌ Unsanitized user input in the following scenarios is not mitigated as the use is permitted by strict-dynamic
src
attribute of a<script>
tag created bydocument.createElement('script')
- body of a
<script>
tag created bydocument.createElement('script')
- usage of
import
API
The general recommendation in these cases is simply that no user input should reach these places (either url path, query parameters, fragment or even from the HTML), as they are extremely difficult to mitigate even with the strictest CSPs.
2.4. not protected by CSPs in general
❌ Script-less attacks: HTML injection, DOM clobbering, etc.
It is important to note that CSPs don’t block every type of web attack and that’s why they are meant as an additional defense, rather than your main defense.
2. Why isn’t the cache simply disabled when the nonce is present?
Like with any engineering solution, the content security policy and protection it provides needs to be put in the context of where it is used and what are the trade-offs.
In AEM Edge Delivery Services the highly efficient use of the CDN cache is a key component of how Adobe delivers high performance, reliability and scalability.
With the current data outlined above, our understanding is that the theoretical disabling of the cache would benefit only a highly discouraged edge case (the usage of document.write
/ document.writeln
), as the other cases don’t seem to depend on whether the nonce is cached or not.
3. Why not use hash based content security policies, which work with caching?
There are a couple of reasons that make the use of hashes less efficient for this architecture:
3.1. We observed that when hashes are used, the import
browser API doesn’t work. This is a key component in the AEM Boilerplate and how the client side rendering is structured.
3.2. The second problem is that the policy (which sits in either the HTML header or body) would have a separate caching lifecycle from the scripts referenced in the HTML. This can cause the rendering of the website to break if for any reason the hash from the policy is outdated, compared to the script delivered. This felt like a too high risk for the uptime of the site, especially when the nonce alternative does not suffer from this problem. Since the nonce header and HTML body are always stored together in the cache, this problem should not appear.
4. Why use this content security policy instead of a host based one?
The following research paper from Google has found the following problems, which we could confirm in practice for AEM Edge Delivery Services customers that did have host based content security policies:
4.1. Presence of unsafe-inline
for inline scripts, this makes trivial DOM based XSS possible, since any unsanitised user input that ends up in a sink like innerHTML
can be exploited.
4.2. Presence of domains where anybody can without restrictions place their own scripts. This presents a very low barrier for an attacker to instead of serving a specific script from a denylisted domain, to just upload it to one that’s allowlisted. Making the advantages over strict-dynamic
in practice quite limited.
The host based content security policies are also quite hard to maintain, so we were looking for an alternative which works easily for a majority of customers, without the need for constant maintenance.
If you have script-src ‘self’; object-src ‘none’; base-uri ‘self’;
you are probably safer, but it means you cannot load any tag manager and any script from outside domains, which we haven’t seen this yet in practice.
5. Can’t an attacker simply add another script with nonce=”aem” in the HTML, before it is replaced with the actual nonce value?
No. The nonce="aem"
attribute is not the main way we determine which scripts to add the new unique nonce value to.
We only apply the unique nonce value to scripts coming from head.html
and static HTML files from your repository. These resources are considered trusted and controlled only by your developers. It is considered that if attackers can change these, they already can modify any code in your repository, thus trying to prevent XSS is no longer a concern. Scripts that could theoretically end up injected through any currently unknown method in the HTML will not receive the correct nonce value, even if they have the nonce="aem"
attribute.
The nonce="aem"
is rather a fallback mechanism, in case Adobe ever needs to deactivate this feature, your site continues to be served, without downtime, because the aem
nonce value is present in both script attributes and policy enforcement, even if the protection is degraded.
6. I found a bypass/vulnerability which is not covered here. Can I report it?
Absolutely! Trust, but verify!
We appreciate feedback regarding any inaccuracies of this guide or content security policy approach and thank you for the time to report it to us!
Security measures work best when they are peer-reviewed and limitations well understood!
Please disclose responsibly vulnerabilities, bypasses or findings using the following two possibilities:
- Reach out to Adobe using your dedicated Slack channel if you are already a customer
- Notify our Adobe Security team at psirt@adobe.com
It is preferred that your report is also accompanied by a proof of concept example bypass, so we understand best your concern.