Back to Blog
high SEVERITY4 min read

notice.html innerHTML Assignment in Browser Extension: CWE-79

A browser extension's content script fetched notice.html and assigned it directly to innerHTML, creating a DOM-based XSS vector if the extension package were compromised. The fix removes notice.html generation from the build process entirely and restructures the shadow DOM injection to use safe textContent assignment for styles only.

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published October 14, 2026•Reviewed October 14, 2026

Answer Summary

The affected code is the browser extension's content script, specifically the `notice()` function that displays notification UI to users. An attacker who compromised the extension package through supply chain attack, malicious insider, or Chrome Web Store compromise could inject arbitrary HTML/JavaScript that executes in the context of every web page the extension runs on. The fix removes notice.html from the build pipeline in build.mjs and restructures content.js to avoid any HTML parsing, using shadow DOM with textContent CSS injection instead. CWE-79 (Improper Neutralization of Input During Web Page Generation, 'Cross-site Scripting').

Vulnerability at a Glance

cweN/A
fixRemove HTML file generation, use shadow DOM with textContent for safe CSS injection
riskArbitrary code execution in web page context if extension package compromised
languageJavaScript
root causeinnerHTML assignment of fetched HTML without sanitization
vulnerabilityDOM-based Cross-Site Scripting (XSS)

Affected Versions

Affected not applicable (first-party code)
Fixed in not applicable (first-party code) — see fix commit
Ecosystem browser extension (Chrome/Firefox)
CVE / GHSA not assigned
CWE unknown

The Vulnerability Explained

Browser extensions operate with elevated privileges, and their content scripts run in the context of every web page users visit. The notice() function in this extension's content script was responsible for displaying notification UI by injecting elements into the page's DOM.

The vulnerable code fetched notice.html from the extension package and assigned it directly to innerHTML:

const html = await fetch(chrome.runtime.getURL('notice.html')).then(response => response.text());
// ... later assigned to shadow root innerHTML

While chrome-extension:// URLs are protected by the extension's Content Security Policy and cannot be intercepted by web pages, this pattern creates a critical vulnerability under a different threat model. If an attacker compromises the extension package—through supply chain attack, malicious insider, or Chrome Web Store compromise—the injected malicious HTML would execute with full access to every page the user visits.

The attack scenario works like this: an attacker with write access to the extension's build artifacts modifies notice.pug (the template source) to include a <script> tag or event handler. When the extension builds, build.mjs processes this through pug.renderFile(), producing a poisoned notice.html. Every user receiving this compromised extension version would then execute attacker-controlled JavaScript in the context of https://banking.example.com, https://mail.example.com, or any site they visit.

The real-world impact is severe: full DOM access, credential theft, session hijacking, and the ability to perform actions on behalf of the user on any website.

The Fix

The fix takes a defense-in-depth approach by eliminating the HTML file entirely and restructuring how the notification UI is constructed.

In build.mjs: The build loop was modified to exclude notice from the pug template rendering:

-for (const name of ['popup', 'options', 'prompt', 'notice']) writeFileSync(new URL(`${name}.html`, `file://${dist}/`), pug.renderFile(fileURLToPath(new URL(`${name}.pug`, import.meta.url)), { pretty: true }));
+for (const name of ['popup', 'options', 'prompt']) writeFileSync(new URL(`${name}.html`, `file://${dist}/`), pug.renderFile(fileURLToPath(new URL(`${name}.pug`, import.meta.url)), { pretty: true }));

In content.js: The vulnerable fetch and innerHTML assignment was replaced with direct shadow DOM construction:

const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'closed' });
const css = await fetch(chrome.runtime.getURL('notice.css')).then(response => response.text());
const style = document.createElement('style');
style.textContent = css;

The key security improvements: (1) no HTML parsing occurs—only CSS text is fetched and assigned via textContent, which cannot execute script; (2) the attack surface is reduced by removing notice.html from the extension package entirely; (3) the shadow DOM structure is maintained but populated through safe DOM APIs rather than HTML string injection.

Key Takeaways

  • Never assign fetched content to innerHTML in extension content scripts, even from chrome-extension:// URLs. The threat model includes package compromise, not just network interception.
  • CSS injection via textContent is safe; HTML injection via innerHTML is not. When styling is the only requirement, fetch CSS files and assign to style.textContent rather than parsing HTML.
  • Remove unused attack surface. The fix demonstrates that notice.html was unnecessary—eliminating the file entirely is more secure than sanitizing it.
  • Extension CSP protects against MITM but not against build-time compromise. Developers must threat-model their code against malicious insiders and supply chain attacks, not just external network attackers.
  • Shadow DOM isolation does not prevent XSS. The closed shadow root in the vulnerable code provided encapsulation from page scripts, but did nothing to stop malicious HTML from executing within that shadow boundary.

How Orbis AppSec Detected This

Source: The notice.html resource fetched via chrome.runtime.getURL() and retrieved through fetch().then(response => response.text())

Sink: Assignment to innerHTML of a shadow root, creating an HTML injection point in the content script execution context

Missing control: No sanitization or validation of the fetched HTML content before DOM insertion; no alternative safe construction methods used

CWE: CWE-79 (Improper Neutralization of Input During Web Page Generation, 'Cross-site Scripting')

Fix: Removed notice.html from the pug template build loop and restructured content.js to construct the shadow DOM programmatically with textContent CSS injection, eliminating all HTML parsing from the notification display path

Orbis AppSec detected this vulnerability automatically. Try Orbis AppSec on your repositories to find and fix issues like this.

Conclusion

This vulnerability illustrates how seemingly safe patterns—fetching from chrome-extension:// URLs with CSP protection—can mask serious security risks under alternative threat models. The fix demonstrates that the most secure code is often the code you don't write: by eliminating notice.html entirely and using programmatic DOM construction with textContent, the extension removes an entire class of injection vulnerabilities while maintaining identical functionality.

For developers building browser extensions, the lesson is clear: treat all HTML parsing as dangerous, even from your own package resources, and prefer textContent and programmatic element creation over innerHTML whenever possible.

Prevention and further reading

Frequently Asked Questions

Why was notice.html removed from the build entirely rather than just sanitizing its contents?

The fix eliminates the HTML file dependency completely. Since the notification UI only required CSS styling injected into a shadow root, the HTML file was unnecessary attack surface. Removing it from build.mjs's pug.renderFile loop closes the vulnerability by removing the fetch target.

Does the shadow DOM's closed mode provide any security benefit for this specific fix?

The closed shadow mode was already present in the vulnerable code. The security improvement comes from eliminating the `fetch(chrome.runtime.getURL('notice.html')).then(response => response.text())` call and its subsequent `innerHTML` assignment, not from shadow DOM isolation.

What specific threat model makes this vulnerability exploitable despite chrome-extension:// URLs being protected by CSP?

The exploit path requires compromise of the extension package itself—through supply chain attack, malicious insider, or Chrome Web Store compromise—rather than network interception. The chrome-extension:// scheme protects against MITM, not against a compromised origin server or build pipeline.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #42

Related Articles

critical

fast-xml-parser 4.5.0 XSS: DOCTYPE Entity Injection in XML.parse()

fast-xml-parser versions 4.5.0 and earlier fail to sanitize DOCTYPE entity declarations during XML parsing, allowing attackers to inject JavaScript through crafted XML payloads. The vulnerability permits reflected XSS in applications that process untrusted XML input. Upgrading to version 5.7.0 eliminates the unsafe entity expansion path.

high

sanitizeBangumiHtml() XSS Fix: insertAdjacentHTML RCE via JSON

A high-severity cross-site scripting vulnerability existed where untrusted JSON data from bangumis.json was rendered directly into the DOM using insertAdjacentHTML without sanitization. An attacker who could modify this external data source could execute arbitrary JavaScript in visitors' browsers. The fix introduces a dedicated sanitizeBangumiHtml() function that strips script tags, event handlers, and javascript: URLs before insertion.

high

form.js jQuery Selector Construction: CWE-79 Quote-Breaking Fix

A defense-in-depth fix in the form.js initialization routine eliminates a jQuery selector injection vector where user-controlled category data was concatenated directly into a string literal. The vulnerable pattern at lines 47-48 of the form handler could allow malicious content to break out of the selector's quoted context and execute arbitrary jQuery methods.

high

ItemPicker `_commitTraitInput` XSS via Unescaped Trait Chip Rendering

The `_commitTraitInput` function in ItemPicker accepted arbitrary user input for trait values without sanitization, then rendered those values directly into HTML chip elements through `_updateList`. An attacker could inject malicious JavaScript payloads that executed when trait chips were displayed. The fix applies a strict whitelist filter removing all non-alphanumeric characters except spaces and hyphens.

high

formatObject() Leaves localStorage Keys Unescaped Before innerHTML

The `formatObject()` pretty-printer used to build the key-configuration storage viewer interpolated object keys and array key/value pairs directly into an HTML string that is later assigned with `innerHTML`. Only string *values* were passed through `escapeHtml()`, so a crafted key name persisted in `localStorage` could execute script every time a user opened the storage viewer. The fix wraps the index-pair branch and the object-key branch in `escapeHtml()`, keeping the raw path only for non-colo

high

CWE-778: Tribute WebHookHandler Logs No Subscription Events

The Tribute payment webhook handler verified request signatures and then mutated subscription state — granting new subscriptions and cancelling existing ones — while logging nothing on success. Only error paths and a no-op test hook produced log lines, so a forged or replayed `NewSubscription` event that passed signature verification left no record of which `TelegramUserID` was affected. The fix adds structured `slog.Info` records at signature verification and at both state-changing branches.