XSLT stylesheet response MIME / nosniff enforcement bypass
Categories
(Core :: DOM: Security, defect, P3)
Tracking
()
People
(Reporter: 5up3rh3i, Unassigned)
References
(Blocks 1 open bug)
Details
(Keywords: ai-involved, reporter-external, sec-low, Whiteboard: [client-bounty-form])
Attachments
(1 file)
|
9.91 KB,
text/x-python-script
|
Details |
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
Updated•3 months ago
|
Updated•3 months ago
|
Comment 1•3 months ago
|
||
The severity field is not set for this bug.
:freddy, could you have a look please?
For more information, please visit BugBot documentation.
Updated•2 months ago
|
Comment 2•2 months ago
|
||
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).
Comment 3•1 month ago
|
||
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.
Comment 5•27 days ago
|
||
The severity field for this bug is set to S4. However, the following bug duplicate has higher severity:
- Bug 286267: S3
:freddy, could you consider increasing the severity of this bug to S3?
For more information, please visit BugBot documentation.
Updated•3 days ago
|
Description
•