Back to Blog
high SEVERITY7 min read

How XSS via Incomplete HTML Escaping happens in JavaScript Browser Extensions and how to fix it

A high-severity cross-site scripting (XSS) vulnerability was discovered in `extension/lib/popup-response.js`, where the `esc()` HTML-escaping function failed to encode backtick characters. Because backticks are valid JavaScript template literal delimiters and can serve as event handler injection vectors in older browsers, this gap allowed attacker-controlled data to break out of safe HTML encoding and potentially execute arbitrary scripts. The fix adds a single `.replace(/\`/g, "`")` call to

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published August 26, 2026•Reviewed August 26, 2026

Answer Summary

This is a Cross-Site Scripting (XSS) vulnerability (CWE-79) in a JavaScript browser extension, specifically in the `esc()` function inside `extension/lib/popup-response.js`. The root cause is that the function encoded double quotes and single quotes but omitted backtick characters, which can act as JavaScript template literal delimiters or legacy event-handler injection vectors. The fix adds `.replace(/\`/g, "`")` to the escaping chain, ensuring backticks in attacker-controlled values are neutralized before being written to `innerHTML`. Any JavaScript code using `innerHTML` with user-supplied data must escape all HTML-significant characters, including backticks.

Vulnerability at a Glance

cweCWE-79
fixAdded .replace(/`/g, "`") to the esc() escaping chain in popup-response.js
riskAttacker-controlled data rendered via innerHTML can execute arbitrary scripts in the extension context
languageJavaScript
root causeThe esc() function escaped " and ' but not `, leaving a bypass path for template-literal-based injection
vulnerabilityCross-Site Scripting (XSS) via Incomplete HTML Escaping

How XSS via Incomplete HTML Escaping Happens in JavaScript Browser Extensions and How to Fix It

Introduction

The extension/lib/popup-response.js file is responsible for rendering API response data—headers, endpoint details, and body content—inside a browser extension's popup UI. To do this safely, the code relies on a helper function called esc() to sanitize values before they are written to the DOM via innerHTML. The security of every innerHTML assignment in the popup depends entirely on esc() doing its job correctly.

The problem? esc() was almost correct—but it missed one character: the backtick (`). That single omission created a meaningful XSS bypass in production code that processes attacker-influenced API response data.


The Vulnerability Explained

What esc() Was Supposed to Do

The esc() function at line 391 of popup-response.js is a custom HTML-escaping utility. Here is the vulnerable version:

// BEFORE (vulnerable)
function esc(s) {
  if (s == null) return "";
  const d = document.createElement("div");
  d.textContent = String(s);
  return d.innerHTML.replace(/"/g, """).replace(/'/g, "'");
}

The function uses a clever browser-native trick: it sets textContent on a throwaway <div> element, which causes the browser to automatically encode <, >, and & into their HTML entities. It then manually replaces " with &quot; and ' with &#39; to cover attribute-value contexts.

This covers most dangerous characters—but not backticks.

Why Backticks Matter

Backticks (`) are significant in two ways:

  1. JavaScript Template Literals: In modern JavaScript, ` delimits template literal strings. If a value containing a backtick is interpolated into an inline <script> block or an event handler attribute, it can break out of a string context.

  2. Legacy Attribute Delimiter: Some older browsers and HTML parsers have historically treated backticks as attribute value delimiters in certain edge cases, creating an injection path even in attribute contexts.

Because the extension popup renders data from API responses—including server-controlled HTTP headers, endpoint URLs, and response bodies—an attacker who controls a server the user is inspecting can craft a response that includes backtick-wrapped payloads.

A Concrete Attack Scenario

Suppose a malicious API server returns an HTTP header like:

X-Custom-Header: value`+alert(document.cookie)+`

If this header value is passed through the vulnerable esc() function and then inserted into an innerHTML context like:

headerCell.innerHTML = `<span class="header-value">${esc(headerValue)}</span>`;

The backtick is passed through unescaped. Depending on how the surrounding template literal or script context is structured, the backtick can terminate one string and open another, executing arbitrary JavaScript within the browser extension's privileged context.

Browser extensions often have access to sensitive APIs (cookies, storage, tabs, network requests). XSS in an extension popup is not merely cosmetic—it can mean full compromise of the extension's capabilities.


The Fix

The fix is surgical and precise: a single additional .replace() call added to the end of the esc() chain.

Before and After

// BEFORE (vulnerable) — popup-response.js:394
return d.innerHTML.replace(/"/g, "&quot;").replace(/'/g, "&#39;");

// AFTER (fixed) — popup-response.js:394
return d.innerHTML.replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/`/g, "&#96;");

Why This Works

The HTML entity &#96; is the numeric character reference for the backtick character. When a browser renders &#96; inside an HTML document, it displays a literal backtick—but the character has no syntactic meaning to the HTML parser or JavaScript engine. It cannot act as a template literal delimiter or an attribute boundary.

By adding .replace(/\/g, "`")to the chain, theesc()` function now neutralizes all four of the most commonly exploited character classes in HTML injection:

Character Encoded As Threat Neutralized
< &lt; Tag injection (via textContent trick)
> &gt; Tag injection (via textContent trick)
& &amp; Entity injection (via textContent trick)
" &quot; Double-quoted attribute breakout
' &#39; Single-quoted attribute breakout
` &#96; Template literal / legacy attribute breakout (new)

The change is scoped entirely to the esc() function, which means every call site that already used esc() is automatically protected without any other code changes.


Key Takeaways

  • The esc() function in popup-response.js was one character away from being correct. Escaping " and ' but not ` left a real bypass path for template-literal-based injection.
  • Browser extension XSS is higher-stakes than web page XSS. Extensions often have privileged access to browser APIs, cookies, and storage—making code execution in an extension popup particularly dangerous.
  • Custom escaping functions must be tested against every dangerous character, not just the most obvious ones. A missing backtick escape is exactly the kind of subtle gap that manual code review misses but automated scanners catch.
  • The fix was a one-line change, but its impact covers every innerHTML call site that relies on esc()—demonstrating the value of centralizing sanitization logic.
  • API response data is attacker-controlled data. Any value that originates from a server response—headers, URLs, body content—must be treated as untrusted input before being rendered in a UI.

How Orbis AppSec Detected This

  • Source: API response data (HTTP headers, endpoint values, response body) returned by a server the user is inspecting
  • Sink: innerHTML assignments throughout popup-response.js that use the esc() helper function
  • Missing control: The esc() function encoded " and ' but omitted the backtick character (`), leaving a bypass for template-literal-style injection
  • CWE: CWE-79 — Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
  • Fix: Added .replace(/\/g, "`")to theesc()return value inpopup-response.js` to encode backticks as their HTML numeric entity

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.


Conclusion

This vulnerability is a textbook example of why "almost correct" sanitization is not good enough. The esc() function in popup-response.js used a smart browser-native encoding trick and handled the most common dangerous characters—but the omission of backtick encoding left a real, exploitable gap in an application that processes attacker-influenced server data.

The fix is minimal: one additional .replace() call. But the lesson is broader: when you write a custom HTML escaping function, you must account for all characters that carry syntactic meaning in HTML and JavaScript contexts, including the often-overlooked backtick. Centralize your sanitization logic, test it explicitly for every dangerous character, and layer it with a Content Security Policy for defense in depth.

Security is not about being mostly right—it is about being completely right where it counts.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #7

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

Voice Assistant Widget XSS: Unsanitized Bot Messages Execute in

The voice assistant widget's `appendMessage` function had a critical cross-site scripting (XSS) vulnerability where bot messages were inserted directly into the DOM without sanitization, while user messages were escaped. An attacker controlling bot responses could inject and execute arbitrary JavaScript in the user's browser context. The fix applies HTML escaping to all message types uniformly.