CSP strict-dynamic bypass: XSLT loads exempt from strict-dynamic enforcement, allowing same-origin stylesheet loading when host sources should be ignored
Categories
(Core :: DOM: Security, defect)
Tracking
()
People
(Reporter: hanaputrarayhan, Assigned: tschuster)
References
(Blocks 1 open bug)
Details
(4 keywords, Whiteboard: [client-bounty-form][adv-main150+][adv-esr140.10+])
Attachments
(2 files, 4 external files)
Discovery
- Method: Manual source code audit of
dom/security/nsCSPUtils.cppCSP evaluation logic - Firefox version: mozilla-central trunk, affects all current versions
- OS: All platforms
Root Cause
At dom/security/nsCSPUtils.cpp:1342-1344, XSLT loads are explicitly exempted from strict-dynamic enforcement:
// XXX I don't think we should apply strict-dynamic to XSLT.
if (hasStrictDynamicKeyword && aLoadInfo->InternalContentPolicyType() !=
nsIContentPolicy::TYPE_XSLT) {
// Step 1.4.1 If the request's parser metadata is "parser-inserted",
// return "Blocked". Otherwise, return "Allowed".
if (aLoadInfo->GetParserCreatedScript()) {
return false; // blocked
}
return true; // allowed
}
When a request has TYPE_XSLT, this entire block is skipped. Execution falls through to lines 1360-1365:
for (uint32_t i = 0; i < mSrcs.Length(); i++) {
if (mSrcs[i]->permits(aUri, aWasRedirected, aReportOnly,
aUpgradeInsecure)) {
return true; // XSLT allowed by host-based source matching
}
}
This evaluates host-based source expressions like 'self' against the XSLT URL. Per the CSP Level 3 specification, when strict-dynamic is present, host-based sources should be ignored for script-like requests. But the XSLT exemption causes them to be evaluated.
XSLT is Treated as Script for CSP
This is not a case of XSLT being outside CSP scope. Line 321-322 explicitly maps XSLT to the script-src directive:
// BLock XSLT as script, see bug 910139
case nsIContentPolicy::TYPE_XSLT:
case nsIContentPolicy::TYPE_SCRIPT:
return nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE;
So XSLT loads are governed by script-src, and strict-dynamic applies to script-src, but the exemption at line 1342 creates an inconsistency.
The Developer Was Uncertain
The XXX comment ("I don't think we should apply strict-dynamic to XSLT") indicates this was a deliberate but uncertain decision. The CSP3 spec does not explicitly carve out an exception for XSLT.
Attack Scenario
Consider an XML API endpoint with CSP: script-src 'strict-dynamic' 'nonce-abc123' 'self'
Per the CSP3 spec, 'self' should be ignored when strict-dynamic is present. But due to the XSLT exemption:
- Attacker finds an injection point in an XML document (e.g., reflected content in an XML API response)
- Attacker injects:
<?xml-stylesheet type="text/xsl" href="/path/to/attacker-uploaded.xsl"?> - The XSLT load check:
TYPE_XSLT-> skips strict-dynamic block -> falls to source matching ->'self'matches -> ALLOWED - The XSLT stylesheet loads and transforms the entire page content
Impact
The loaded XSLT can:
- Completely restructure the page -- replace all visible content with attacker-controlled HTML
- Phishing -- display a fake login form that looks like the legitimate site
- Content injection -- insert misleading information, fake error messages, etc.
- CSS-based data exfiltration -- use CSS selectors to leak page data to an external server via background-image URLs
The XSLT cannot execute JavaScript -- scripts in XSLT output are created with FROM_PARSER_XSLT (line 483 of txMozillaXMLOutput.cpp), which is treated as parser-inserted and blocked by strict-dynamic. So this is content injection, not full XSS.
Limitations
- Requires an XML document (not HTML) -- the
<?xml-stylesheet?>processing instruction is only valid in XML - Requires the CSP to include both
strict-dynamicAND a host-based source like'self'(this is a common migration pattern) - The XSL file must be on an allowed host (e.g., same-origin for
'self') - Scripts in XSLT output are still blocked by strict-dynamic
Steps to Reproduce
- Serve an XML page with CSP:
Content-Security-Policy: script-src 'strict-dynamic' 'nonce-test' 'self' - The XML page includes:
<?xml-stylesheet type="text/xsl" href="transform.xsl"?> transform.xslis a same-origin XSLT that restructures the page- Expected (per spec): XSLT load blocked (strict-dynamic ignores 'self')
- Actual: XSLT loads and transforms the page content
Suggested Fix
Remove the XSLT exemption and let strict-dynamic apply uniformly:
// Remove the TYPE_XSLT exception:
if (hasStrictDynamicKeyword) {
if (aLoadInfo->GetParserCreatedScript()) {
return false;
}
return true;
}
If XSLT needs special handling, it should be parser-inserted (blocked) since <?xml-stylesheet?> is parser-inserted by definition. This would make the behavior consistent with the spec and with how Firefox treats scripts under strict-dynamic.
Comment 1•7 months ago
|
||
Please attach a testcase to the bug that demonstrates that this issue actually happens and isn't just AI hallucination.
Updated•7 months ago
|
Updated•7 months ago
|
| Reporter | ||
Comment 2•7 months ago
|
||
(In reply to Daniel Veditz [:dveditz] from comment #1)
Please attach a testcase to the bug that demonstrates that this issue actually happens and isn't just AI hallucination.
Attached a testcase that demonstrates the issue. The archive contains a small Python HTTP server and test files.
How to reproduce
tar xzf attachment_6_csp_xslt_testcase.tar.gz
python3 server.py
Then open http://localhost:8080/ in Firefox. The index page links to two tests:
Test 1: http://localhost:8080/test.xml (XSLT - demonstrates the bug)
- XML page with
<?xml-stylesheet type="text/xsl" href="transform.xsl"?> - Served with CSP:
script-src 'strict-dynamic' 'nonce-test123' 'self' - Expected (per spec): XSLT blocked. You should see raw XML.
- Actual (Firefox bug): XSLT loads and transforms the page. You see "XSLT LOADED" in big red text.
Test 2: http://localhost:8080/test_script.html (Script - control test)
- HTML page with
<script src="script.js">(no nonce), same CSP header - Expected: Script blocked by strict-dynamic.
- Actual: Script correctly blocked. You see "SCRIPT BLOCKED" in green.
Both pages have the identical CSP header. The comparison proves that strict-dynamic correctly blocks same-origin scripts (as the spec requires) but incorrectly allows same-origin XSLT loads. Per CSP3, 'self' should be ignored when strict-dynamic is present, which should block both.
The test_script.html page also includes a nonce-matched <script nonce="test123"> that executes successfully, confirming CSP is active and enforced on both pages.
Files in archive
server.py- Python HTTP server that adds CSP headersindex.html- Landing page with test descriptionstest.xml- XML page with XSLT reference (bug demo)transform.xsl- XSLT that transforms the pagetest_script.html- HTML page with script (control test)script.js- Script that would change status text if it loaded
Tested with Firefox Nightly (current) on Linux.
| Reporter | ||
Comment 3•7 months ago
|
||
| Assignee | ||
Comment 4•7 months ago
|
||
We can't implement 'strict-dynamic' for XSLT, because GetParserCreatedScript() doesn't apply to it. We could in theory of course block XSLT completely with strict-dynamic. I think the fallback behavior isn't totally unreasonable and would also happen in implementation that don't support strict-dynamic at all.
| Assignee | ||
Updated•7 months ago
|
| Reporter | ||
Comment 5•7 months ago
|
||
(In reply to Tom Schuster from comment #4)
We can't implement
'strict-dynamic'for XSLT, becauseGetParserCreatedScript()doesn't apply to it. We could in theory of course block XSLT completely with strict-dynamic. I think the fallback behavior isn't totally unreasonable and would also happen in implementation that don't support strict-dynamic at all.
I appreciate the technical explanation about GetParserCreatedScript() not applying to XSLT. However, I'd argue that blocking XSLT entirely under strict-dynamic is the right call, for several reasons:
1. Mozilla already treats XSLT+CSP bypasses as security vulnerabilities. CVE-2025-8032 (bug 1974407) was assigned for "XSLT document() CSP SRC bypass" where XSLT document loading didn't correctly propagate the source document's CSP. That bug was rated as a security fix in Firefox 141 / ESR 128.13. The issue I'm reporting is the same class -- XSLT bypassing CSP enforcement -- just through a different mechanism (strict-dynamic exemption rather than document() propagation).
2. Chrome is removing XSLT entirely due to security risk. Chromium has announced full XSLT deprecation and removal (Chrome 158, November 2026), citing that "the underlying libraries like libxslt are complex, aging C/C++ codebases notoriously susceptible to memory safety vulnerabilities." In the interim, Chrome blocks XSLT under strict-dynamic CSP. Firefox allowing XSLT where Chrome blocks it creates a cross-browser inconsistency that attackers can specifically target Firefox users on pages that are protected in Chrome.
3. The "fallback behavior" argument works against security. You mention this fallback "would also happen in implementations that don't support strict-dynamic at all." But that's exactly the problem -- a site deploying strict-dynamic is explicitly opting into stronger security. Falling back to pre-strict-dynamic behavior for XSLT silently downgrades that security. The developer chose strict-dynamic because they want nonce-based trust, not host-allowlist trust. The XSLT exemption breaks that contract.
4. Blocking is the safe default when the mechanism doesn't apply. I agree that strict-dynamic's parser-inserted/non-parser-inserted distinction doesn't map to XSLT. But the spec's intent is clear: when strict-dynamic is present, host-based sources are ignored. If the distinguishing mechanism can't apply to XSLT, the conservative choice is to block (like any other script-like load without a nonce), not to fall back to weaker enforcement. Developers who need XSLT could use a hash or nonce on the <?xml-stylesheet?> PI.
5. Content injection is real impact. The attached testcase demonstrates that an attacker who can inject <?xml-stylesheet?> into an XML endpoint served with strict-dynamic CSP can completely rewrite the page content. On XML API endpoints rendered in-browser, this enables phishing, fake login forms, and CSS-based data exfiltration -- exactly what the developer deployed CSP to prevent.
The testcase clearly shows: same CSP, same origin -- scripts are blocked (correct), XSLT loads (incorrect). I believe this warrants treatment consistent with CVE-2025-8032.
| Assignee | ||
Comment 6•7 months ago
|
||
Please respect my time and don't reply with AI generated walls of texts.
| Assignee | ||
Updated•7 months ago
|
| Reporter | ||
Comment 7•7 months ago
|
||
(In reply to Tom Schuster from comment #6)
Please respect my time and don't reply with AI generated walls of texts.
Sorry for the previous reply. The CVE-2025-8032 (bug 1974407) fixed an XSLT-based CSP bypass via document(), and this issue shows the same class which is XSLT bypassing strict-dynamic through a different path. Chromium browser already blocks XSLT under strict-dynamic (and is deprecating it entirely), while Firefox allows it, creating a targetable gap. The testcase demonstrates that scripts are correctly blocked under the same CSP, but XSLT still executes, suggesting Firefox should also block XSLT when strict-dynamic is active.
Reference: https://developer.chrome.com/docs/web-platform/deprecating-xslt
| Assignee | ||
Updated•7 months ago
|
| Assignee | ||
Comment 8•7 months ago
|
||
Updated•7 months ago
|
| Assignee | ||
Comment 10•7 months ago
|
||
Comment 11•7 months ago
|
||
Updated•7 months ago
|
Comment 12•7 months ago
|
||
The patch landed in nightly and beta is affected.
:tschuster, is this bug important enough to require an uplift?
- If yes, please nominate the patch for beta approval.
- See https://wiki.mozilla.org/Release_Management/Requesting_an_Uplift for documentation on how to request an uplift.
- If no, please set
status-firefox150towontfix.
For more information, please visit BugBot documentation.
| Assignee | ||
Comment 13•7 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D289236
Updated•7 months ago
|
Comment 14•7 months ago
|
||
firefox-beta Uplift Approval Request
- User impact if declined/Reason for urgency: For certain Content-Security-Policies we would incorrectly apply XSLT to XML documents. This could negatively affect the security of websites.
- Code covered by automated testing?: no
- Fix verified in Nightly?: yes
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: Simple change and XSLT is hopefully not important anymore.
- String changes made/needed?: no
- Is Android affected?: yes
| Assignee | ||
Comment 15•7 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D289236
Updated•7 months ago
|
Comment 16•7 months ago
|
||
firefox-esr140 Uplift Approval Request
- User impact if declined/Reason for urgency: For certain Content-Security-Policies we would incorrectly apply XSLT to XML documents. This could negatively affect the security of websites.
- Code covered by automated testing?: no
- Fix verified in Nightly?: yes
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: Simple change and XSLT is hopefully not important anymore.
- String changes made/needed?: no
- Is Android affected?: yes
Updated•7 months ago
|
Updated•7 months ago
|
Comment 17•7 months ago
|
||
| uplift | ||
Updated•7 months ago
|
Updated•7 months ago
|
Updated•7 months ago
|
Updated•7 months ago
|
Comment 18•7 months ago
|
||
| uplift | ||
Updated•6 months ago
|
Updated•6 months ago
|
Updated•6 months ago
|
Updated•5 months ago
|
Comment 19•4 months ago
|
||
Comment 20•4 months ago
|
||
Updated•2 months ago
|
Description
•