Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions code/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -232,9 +232,11 @@
"@storybook/icons": "^2.0.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/user-event": "^14.6.1",
"@types/dompurify": "^3.2.0",
"@vitest/expect": "3.2.4",
"@vitest/spy": "3.2.4",
"@webcontainer/env": "^1.1.1",
"dompurify": "^3.4.2",
"esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0",
"open": "^10.2.0",
"recast": "^0.23.5",
Expand Down
22 changes: 19 additions & 3 deletions code/core/src/manager/components/sidebar/Brand.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import React from 'react';

import { StorybookLogo } from 'storybook/internal/components';

import DOMPurify from 'dompurify';

import { styled, withTheme } from 'storybook/theming';

export const StorybookLogoStyled = styled(StorybookLogo)(({ theme }) => ({
Expand Down Expand Up @@ -44,17 +46,31 @@ export const Brand = withTheme(({ theme }) => {
return null;
}

const sanitizedTitle = DOMPurify.sanitize(title);

if (!url) {
return <div dangerouslySetInnerHTML={{ __html: title }} />;
return <div dangerouslySetInnerHTML={{ __html: sanitizedTitle }} />;
}
return <LogoLink href={url} target={targetValue} dangerouslySetInnerHTML={{ __html: title }} />;
return (
<LogoLink
href={url}
target={targetValue}
rel={targetValue === '_blank' ? 'noopener noreferrer' : undefined}
dangerouslySetInnerHTML={{ __html: sanitizedTitle }}
/>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
);
}

const logo = image ? <Img src={image} alt={title} /> : <StorybookLogoStyled alt={title} />;

if (url) {
return (
<LogoLink title={title} href={url} target={targetValue}>
<LogoLink
title={title}
href={url}
target={targetValue}
rel={targetValue === '_blank' ? 'noopener noreferrer' : undefined}
>
{logo}
</LogoLink>
);
Expand Down
58 changes: 58 additions & 0 deletions code/core/src/manager/components/sidebar/__tests__/Brand.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// @vitest-environment happy-dom
import { render } from '@testing-library/react';
import { describe, expect, test } from 'vitest';

import React from 'react';

import { ThemeProvider, ensure, themes } from 'storybook/theming';

import { Brand } from '../Brand.tsx';

const renderBrand = (brandTheme: Record<string, unknown>) => {
const theme = { ...ensure(themes.light), brand: brandTheme };
return render(
<ThemeProvider theme={theme as any}>
<Brand />
</ThemeProvider>
);
};

describe('Brand – XSS sanitization', () => {
test('strips script tags from title when image is null', () => {
const { container } = renderBrand({
image: null,
title: 'Hello<script>alert("xss")</script>',
url: '',
});
expect(container.innerHTML).not.toContain('<script>');
expect(container.innerHTML).toContain('Hello');
});

test('strips inline event handlers from title when image is null', () => {
const { container } = renderBrand({
image: null,
title: '<img src=x onerror="alert(1)">',
url: '',
});
expect(container.innerHTML).not.toContain('onerror');
});

test('preserves safe inline HTML in title when image is null', () => {
const { container } = renderBrand({
image: null,
title: '<strong>My Brand</strong>',
url: '',
});
expect(container.innerHTML).toContain('<strong>My Brand</strong>');
});

test('sanitizes title when rendered inside a link', () => {
const { container } = renderBrand({
image: null,
title: 'Brand<script>alert(1)</script>',
url: 'https://example.com',
});
expect(container.innerHTML).not.toContain('<script>');
expect(container.innerHTML).toContain('Brand');
});
});
25 changes: 24 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9692,6 +9692,15 @@ __metadata:
languageName: node
linkType: hard

"@types/dompurify@npm:^3.2.0":
version: 3.2.0
resolution: "@types/dompurify@npm:3.2.0"
dependencies:
dompurify: "npm:*"
checksum: 10c0/e83fec586ffbb1b43f7c8479f2a99c223814d240db912d53fb126aaeea52a4091deceeb75632b5d76004d88ef0fbf444984887b121dece6fa69f4172ed4b2b07
languageName: node
linkType: hard

"@types/ejs@npm:^3.1.1, @types/ejs@npm:^3.1.5":
version: 3.1.5
resolution: "@types/ejs@npm:3.1.5"
Expand Down Expand Up @@ -10280,7 +10289,7 @@ __metadata:
languageName: node
linkType: hard

"@types/trusted-types@npm:^2.0.2":
"@types/trusted-types@npm:^2.0.2, @types/trusted-types@npm:^2.0.7":
version: 2.0.7
resolution: "@types/trusted-types@npm:2.0.7"
checksum: 10c0/4c4855f10de7c6c135e0d32ce462419d8abbbc33713b31d294596c0cc34ae1fa6112a2f9da729c8f7a20707782b0d69da3b1f8df6645b0366d08825ca1522e0c
Expand Down Expand Up @@ -15590,6 +15599,18 @@ __metadata:
languageName: node
linkType: hard

"dompurify@npm:*, dompurify@npm:^3.4.2":
version: 3.4.2
resolution: "dompurify@npm:3.4.2"
dependencies:
"@types/trusted-types": "npm:^2.0.7"
dependenciesMeta:
"@types/trusted-types":
optional: true
checksum: 10c0/23ab3ab079480a49e1426c4ee7279e393913fa7f2193c4142a5be54d4163dbdd969558675876c6b8bbcbf2ad5d1bc3e00bf902acf142fff12e50274a65ae95b3
languageName: node
linkType: hard

"domutils@npm:^2.0.0, domutils@npm:^2.5.2, domutils@npm:^2.8.0":
version: 2.8.0
resolution: "domutils@npm:2.8.0"
Expand Down Expand Up @@ -28869,6 +28890,7 @@ __metadata:
"@types/cross-spawn": "npm:^6.0.6"
"@types/detect-port": "npm:^1.3.0"
"@types/diff": "npm:^5.0.9"
"@types/dompurify": "npm:^3.2.0"
"@types/ejs": "npm:^3.1.1"
"@types/js-yaml": "npm:^4.0.5"
"@types/node": "npm:^22.19.1"
Expand Down Expand Up @@ -28901,6 +28923,7 @@ __metadata:
detect-indent: "npm:^7.0.1"
detect-port: "npm:^1.6.1"
diff: "npm:^8.0.2"
dompurify: "npm:^3.4.2"
downshift: "npm:^9.0.4"
ejs: "npm:^3.1.10"
empathic: "npm:^2.0.0"
Expand Down