Skip to content

[Bug]: dangerouslySetInnerHTML with unsanitized theme.brand.title allows XSS via theme configuration #34690

@tomohiro86

Description

@tomohiro86

Describe the bug

In code/core/src/manager/components/sidebar/Brand.tsx (lines 48 and 50), the theme.brand.title value is rendered directly using dangerouslySetInnerHTML without sanitization:

// Brand.tsx:41-51
// When image is explicitly set to null, enable custom HTML support
if (image === null) {
  if (title === null) {
    return null;
  }
  if (!url) {
    return <div dangerouslySetInnerHTML={{ __html: title }} />;
  }
  return <LogoLink href={url} target={targetValue} dangerouslySetInnerHTML={{ __html: title }} />;
}

While this is partially intentional (the comment says "enable custom HTML support"), it allows arbitrary HTML — including <script> tags — to be injected into the Storybook Manager UI through the theme configuration.

Attack scenario: A malicious or compromised npm package (e.g., a Storybook addon or a transitive dependency) that modifies the Storybook theme could inject arbitrary JavaScript into the Manager UI via theme.brand.title. This is a realistic threat in supply-chain attack scenarios.

Reproduction link

https://stackblitz.com/edit/storybook-new

Reproduction steps

  1. In .storybook/manager.ts, configure the theme with a script tag in brand.title:
import { addons } from 'storybook/manager-api';
import { create } from 'storybook/theming';

addons.setConfig({
  theme: create({
    base: 'light',
    brand: {
      image: null,
      title: '<img src=x onerror="alert(\'XSS via theme.brand.title\')">',
    },
  }),
});
  1. Start Storybook.
  2. Observe that the injected script executes in the Manager UI.

Expected behavior: theme.brand.title should be treated as plain text and escaped before rendering, or the documentation should clearly warn about the XSS risk of using image: null with HTML content in title.

System

Storybook version : 10.4.0-alpha.15
Affected file     : code/core/src/manager/components/sidebar/Brand.tsx (lines 48, 50)

Additional context

Suggested fix: Use textContent for plain text rendering by default, and opt-in to raw HTML only via an explicitly named prop (e.g., titleHTML) with a clear security warning in the docs.

// Safe default
return <div>{title}</div>;

// Opt-in for raw HTML with explicit prop
return <div dangerouslySetInnerHTML={{ __html: titleHTML }} />;

Alternatively, sanitize with a library such as DOMPurify before passing to dangerouslySetInnerHTML.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions