Open Bug 2041923 Opened 3 months ago Updated 3 days ago

XSLT stylesheet response MIME / nosniff enforcement bypass

Categories

(Core :: DOM: Security, defect, P3)

defect

Tracking

()

UNCONFIRMED

People

(Reporter: 5up3rh3i, Unassigned)

References

(Blocks 1 open bug)

Details

(Keywords: ai-involved, reporter-external, sec-low, Whiteboard: [client-bounty-form])

Attachments

(1 file)

XSLT stylesheet response MIME / nosniff enforcement bypass

Summary

Firefox XSLT stylesheet loaders do not consistently enforce the real HTTP response MIME type and X-Content-Type-Options: nosniff before compiling a stylesheet. Responses served as text/plain, text/html, application/octet-stream, or with a missing Content-Type, together with nosniff, can still be compiled as active XSLT if the body is valid XSLT.

The same text/plain + nosniff resource is blocked when loaded as a normal script, proving the nosniff oracle is valid. When loaded as an XSLT stylesheet, it can transform the XML document and execute script in the result document.

PoC: xslt-stylesheet-mime-nosniff-bypass-poc.py

Affected Code

dom/xml/nsXMLContentSink.cpp
  nsXMLContentSink::LoadXSLStyleSheet()

dom/xslt/xslt/txMozillaXSLTProcessor.cpp
  txMozillaXSLTProcessor::LoadStyleSheet()
  TX_LoadSheet()

dom/xslt/xslt/txMozillaStylesheetCompiler.cpp
  txCompileObserver::startLoad()
  txCompileObserver::loadURI()
  txSyncCompileObserver::loadURI()

Main async path:

xml-stylesheet PI / HTTP Link / xsl:include
  -> TX_LoadSheet
  -> txCompileObserver::startLoad()
  -> NS_NewChannelWithTriggeringPrincipal(... TYPE_XSLT ...)
  -> channel->SetContentType("text/xml")
  -> txStylesheetSink / XML parser
  -> txStylesheetCompiler

Web API sync include path:

XSLTProcessor.importStylesheet()
  -> xsl:include
  -> txSyncCompileObserver::loadURI()
  -> nsSyncLoadService::LoadDocument(... TYPE_XSLT ...)
  -> handleNode(document, compiler)

Root Cause

The async XSLT loader calls:

channel->SetContentType("text/xml")

This compatibility hint makes the parser treat the stylesheet as XML. Dynamic validation shows that even when the real HTTP response is:

Content-Type: text/plain / text/html / application/octet-stream / missing
X-Content-Type-Options: nosniff
body: valid XSLT

the stylesheet is still compiled and participates in the transform.

The Web API sync include path does not use that async startLoad() call, but it also fails to apply a fail-closed real response MIME + nosniff gate before handleNode(document, compiler).

Broken invariant:

When a response declares nosniff, an active XSLT stylesheet load must not treat
low-privilege MIME types such as text/plain, text/html, application/octet-stream,
or missing Content-Type as executable transform stylesheets.

Reachable Path

Same-origin:

XML document
  -> <?xml-stylesheet type="text/xsl" href="/plain-nosniff.xsl"?>
  -> stylesheet response:
       Content-Type: text/plain
       X-Content-Type-Options: nosniff
       body: valid XSLT
  -> XSLT compiler parses stylesheet
  -> result document generated
  -> result script executes

HTTP Link header:

XML response:
  Link: </plain-nosniff.xsl>; rel=stylesheet; type="text/xsl"

include/import:

main.xsl is valid text/xsl
  -> <xsl:include href="/child-plain-nosniff.xsl">
  -> child response text/plain + nosniff
  -> child template still compiled and executed

Trigger Conditions

1. The target uses XML + XSLT transform.
2. The stylesheet or child stylesheet response body is valid XSLT.
3. The response MIME is text/plain / text/html / application/octet-stream / missing Content-Type.
4. The response has X-Content-Type-Options: nosniff.
5. The stylesheet can be loaded under the current same-origin/CORS policy.

For cross-origin loads, CORS still applies. This is a MIME/nosniff bypass, not a CORS bypass.

Dynamic Validation / Log Summary

Environment:

Firefox
Headless
Behavior oracle: whether MIME/nosniff blocks active load

Same-origin MIME / nosniff:

{
  "script_open_ran": true,
  "script_plain_nosniff_ran": false,
  "script_nosniff_oracle_valid": true,
  "xslt_open_ran": true,
  "xslt_plain_nosniff_ran": true,
  "xslt_octet_nosniff_ran": true,
  "xslt_html_nosniff_ran": true,
  "xslt_no_content_type_nosniff_ran": true,
  "xslt_plain_nosniff_executed": true,
  "xslt_octet_nosniff_executed": true,
  "xslt_html_nosniff_executed": true,
  "xslt_no_content_type_nosniff_executed": true
}

HTTP Link header XSLT:

{
  "link_html_nosniff_ran": true,
  "link_octet_nosniff_ran": true,
  "link_open_ran": true,
  "link_plain_nosniff_ran": true,
  "link_wrong_mime_nosniff_executed": true,
  "script_nosniff_oracle_valid": true,
  "script_open_ran": true,
  "script_plain_nosniff_ran": false
}

xsl:include child stylesheet:

{
  "script_open_ran": true,
  "script_plain_nosniff_ran": false,
  "script_nosniff_oracle_valid": true,
  "include_open_ran": true,
  "include_plain_nosniff_ran": true,
  "include_octet_nosniff_ran": true,
  "include_html_nosniff_ran": true,
  "include_wrong_mime_nosniff_executed": true
}

Web API XSLTProcessor.importStylesheet() sync include:

{
  "script_nosniff_oracle_valid": true,
  "webapi_include_open_ok": true,
  "webapi_include_plain_nosniff_ok": true,
  "webapi_include_octet_nosniff_ok": true,
  "webapi_include_html_nosniff_ok": true,
  "webapi_include_wrong_mime_nosniff_executed": true
}

Cross-origin + CORS boundary:

{
  "cross_xsl_acao_requested": true,
  "cross_xsl_acao_ran": true,
  "cross_plain_acao_requested": true,
  "cross_plain_acao_ran": true,
  "cross_octet_acao_requested": true,
  "cross_octet_acao_ran": true,
  "cross_xsl_noacao_requested": true,
  "cross_xsl_noacao_ran": false,
  "cross_origin_cors_oracle_valid": true,
  "cross_origin_plain_nosniff_executed_with_acao": true,
  "cross_origin_octet_nosniff_executed_with_acao": true
}

Upload downgrade threat model:

{
  "script_upload_requested": true,
  "script_upload_executed": false,
  "xslt_upload_requested": true,
  "xslt_upload_exfiltrated_secret": true,
  "exfil_secret": "upload-nosniff-secret-20260521",
  "security_risk_observed": true
}

This shows that the same text/plain + nosniff uploaded file does not execute as a normal script, but can act as an active XSLT stylesheet, read XML source data, and exfiltrate it through the result document.

Exploitability

bug:
  yes. Active XSLT stylesheet loading does not correctly enforce the real response MIME + nosniff gate.

crash:
  no.

exploit primitive:
  yes. A low-privilege MIME attacker-controlled resource can be upgraded into an active XSLT transform.

real exploitable vulnerability:
  yes, an active content MIME confusion / nosniff policy bypass.

Limitations:

This is not memory corruption.
Cross-origin loads are still subject to CORS.
The target must use XML + XSLT rendering or Web API importStylesheet/include patterns.

Patch Analysis / Fix Guidance

Recommended fixes:

1. Before compiling XSLT, check the real HTTP response Content-Type.
2. When X-Content-Type-Options: nosniff is present, allow only explicit XSLT/XML stylesheet MIME types.
3. Do not let channel->SetContentType("text/xml") override the real HTTP response for security decisions.
4. Cover async xml-stylesheet PI, HTTP Link header, and xsl:include/import paths.
5. Cover the Web API importStylesheet sync include/import path as well.

Regression tests:

XML source -> XSLT stylesheet:
  text/xsl + nosniff -> transform allowed
  text/plain + nosniff -> blocked
  text/html + nosniff -> blocked
  application/octet-stream + nosniff -> blocked
  missing Content-Type + nosniff -> blocked
Flags: sec-bounty?
Group: firefox-core-security → dom-core-security
Component: Security → DOM: Security
Product: Firefox → Core

The severity field is not set for this bug.
:freddy, could you have a look please?

For more information, please visit BugBot documentation.

Flags: needinfo?(fbraun)
Severity: -- → S4
Flags: needinfo?(fbraun)
Priority: -- → P3

Firefox's XSLT stylesheet loader calls channel->SetContentType("text/xml") (confirmed in dom/xslt/xslt/txMozillaStylesheetCompiler.cpp:412) and has no nosniff check anywhere in the XSLT loading path. This allows resources served with X-Content-Type-Options: nosniff and wrong MIME types (text/plain, text/html, application/octet-stream) to be compiled and executed as XSLT stylesheets, bypassing the nosniff security boundary. XSLT can execute JavaScript and exfiltrate data. However, exploitation requires the target site to use XML+XSLT rendering (uncommon), and cross-origin loads are still blocked by CORS. The attack surface is limited to same-origin scenarios or CORS-permissive cross-origin resources. Fits sec-moderate as a sec-high-class bypass (active content execution via nosniff circumvention) constrained to an unusual target configuration (XML+XSLT sites).

Keywords: sec-moderate

I'm downgrading the severity of this to low and opening it up. I'm going to forward-dupe Bug 286267 - a 21 year old bug - to this one as this contains an attack vector where this becomes a concern... although a fairly obscure one requiring quite some circumstances.

Group: dom-core-security
Flags: sec-bounty? → sec-bounty-
Keywords: sec-moderatesec-low
Duplicate of this bug: 286267

The severity field for this bug is set to S4. However, the following bug duplicate has higher severity:

:freddy, could you consider increasing the severity of this bug to S3?

For more information, please visit BugBot documentation.

Flags: needinfo?(fbraun)
Blocks: mimesniff
Severity: S4 → S3
Flags: needinfo?(fbraun)
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Creator:
Created:
Updated:
Size: