Closed Bug 2025067 (CVE-2026-6771) Opened 7 months ago Closed 7 months ago

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)

defect

Tracking

()

RESOLVED FIXED
151 Branch
Tracking Status
firefox-esr115 --- unaffected
firefox-esr140 150+ fixed
firefox149 --- wontfix
firefox150 + fixed
firefox151 + fixed

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.cpp CSP 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:

  1. Attacker finds an injection point in an XML document (e.g., reflected content in an XML API response)
  2. Attacker injects: <?xml-stylesheet type="text/xsl" href="/path/to/attacker-uploaded.xsl"?>
  3. The XSLT load check: TYPE_XSLT -> skips strict-dynamic block -> falls to source matching -> 'self' matches -> ALLOWED
  4. 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-dynamic AND 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

  1. Serve an XML page with CSP: Content-Security-Policy: script-src 'strict-dynamic' 'nonce-test' 'self'
  2. The XML page includes: <?xml-stylesheet type="text/xsl" href="transform.xsl"?>
  3. transform.xsl is a same-origin XSLT that restructures the page
  4. Expected (per spec): XSLT load blocked (strict-dynamic ignores 'self')
  5. 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.

Flags: sec-bounty?

Please attach a testcase to the bug that demonstrates that this issue actually happens and isn't just AI hallucination.

Flags: needinfo?(hanaputrarayhan)
Keywords: testcase-wanted
Group: firefox-core-security → core-security
Component: Security → DOM: Security
Product: Firefox → Core
Group: core-security → dom-core-security

(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 headers
  • index.html - Landing page with test descriptions
  • test.xml - XML page with XSLT reference (bug demo)
  • transform.xsl - XSLT that transforms the page
  • test_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.

Flags: needinfo?(hanaputrarayhan)

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.

Keywords: testcase-wanted
Blocks: csp-w3c-3

(In reply to Tom Schuster from comment #4)

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.

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.

Please respect my time and don't reply with AI generated walls of texts.

Assignee: nobody → tschuster
Status: UNCONFIRMED → NEW
Ever confirmed: true

(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

Keywords: ai-involved
Attached file (secure) —
Attached file (secure) —
Group: dom-core-security → core-security-release
Status: NEW → RESOLVED
Closed: 7 months ago
Resolution: --- → FIXED
Target Milestone: --- → 151 Branch

The patch landed in nightly and beta is affected.
:tschuster, is this bug important enough to require an uplift?

For more information, please visit BugBot documentation.

Flags: needinfo?(tschuster)
Attached file (secure) —
Attachment #9560716 - Flags: approval-mozilla-beta?

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
Attached file (secure) —
Attachment #9560717 - Flags: approval-mozilla-esr140?

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
Attachment #9560716 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
Flags: needinfo?(tschuster)
QA Whiteboard: [sec] [uplift] [qa-triage-done-c151/b150]
Attachment #9560717 - Flags: approval-mozilla-esr140? → approval-mozilla-esr140+
Flags: sec-bounty? → sec-bounty+
Whiteboard: [client-bounty-form] → [client-bounty-form][adv-main150+]
Whiteboard: [client-bounty-form][adv-main150+] → [client-bounty-form][adv-main150+][adv-esr140.10+]
Alias: CVE-2026-6771
Group: core-security-release
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: