diff --git a/code/core/package.json b/code/core/package.json index ecf921c3f9b4..12da97308aab 100644 --- a/code/core/package.json +++ b/code/core/package.json @@ -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", diff --git a/code/core/src/manager/components/sidebar/Brand.tsx b/code/core/src/manager/components/sidebar/Brand.tsx index 1d5483f94656..39750fbcf37a 100644 --- a/code/core/src/manager/components/sidebar/Brand.tsx +++ b/code/core/src/manager/components/sidebar/Brand.tsx @@ -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 }) => ({ @@ -44,17 +46,31 @@ export const Brand = withTheme(({ theme }) => { return null; } + const sanitizedTitle = DOMPurify.sanitize(title); + if (!url) { - return
; + return
; } - return ; + return ( + + ); } const logo = image ? {title} : ; if (url) { return ( - + {logo} ); diff --git a/code/core/src/manager/components/sidebar/__tests__/Brand.test.tsx b/code/core/src/manager/components/sidebar/__tests__/Brand.test.tsx new file mode 100644 index 000000000000..c0a7db4e22f8 --- /dev/null +++ b/code/core/src/manager/components/sidebar/__tests__/Brand.test.tsx @@ -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) => { + const theme = { ...ensure(themes.light), brand: brandTheme }; + return render( + + + + ); +}; + +describe('Brand – XSS sanitization', () => { + test('strips script tags from title when image is null', () => { + const { container } = renderBrand({ + image: null, + title: 'Hello', + url: '', + }); + expect(container.innerHTML).not.toContain('', + url: 'https://example.com', + }); + expect(container.innerHTML).not.toContain('