Skip to content

Commit 320d745

Browse files
authored
Merge pull request #33913 from storybookjs/version-non-patch-from-10.3.0-alpha.10
Release: Prerelease 10.3.0-alpha.11
2 parents 0c091f9 + 2f3ccdd commit 320d745

File tree

20 files changed

+357
-55
lines changed

20 files changed

+357
-55
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## 10.2.12
2+
3+
- Core: Sanitize inputs for save from controls - [#33868](https://github.com/storybookjs/storybook/pull/33868), thanks @valentinpalkovic!
4+
- Telemetry: Add project age - [#33910](https://github.com/storybookjs/storybook/pull/33910), thanks @shilman!
5+
- Webpack: Improve performance of module-mocking plugins - [#33169](https://github.com/storybookjs/storybook/pull/33169), thanks @valentinpalkovic!
6+
17
## 10.2.11
28

39
- Addon-Vitest: Fix postinstall a11y installation - [#33888](https://github.com/storybookjs/storybook/pull/33888), thanks @valentinpalkovic!

CHANGELOG.prerelease.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
## 10.3.0-alpha.11
2+
3+
- Addon Pseudo-states: Process all nested css rules - [#33605](https://github.com/storybookjs/storybook/pull/33605), thanks @hpohlmeyer!
4+
- Core: Avoid hanging when inferring args for recursive calls on DOM elemens - [#33922](https://github.com/storybookjs/storybook/pull/33922), thanks @valentinpalkovic!
5+
- Core: Sanitize inputs for save from controls - [#33868](https://github.com/storybookjs/storybook/pull/33868), thanks @valentinpalkovic!
6+
- Telemetry: Add project age - [#33910](https://github.com/storybookjs/storybook/pull/33910), thanks @shilman!
7+
- Viewport: Prioritize story viewport globals and avoid user-global pollution - [#33849](https://github.com/storybookjs/storybook/pull/33849), thanks @ia319!
8+
19
## 10.3.0-alpha.10
210

311
- Addon-Vitest: Fix postinstall a11y installation - [#33888](https://github.com/storybookjs/storybook/pull/33888), thanks @valentinpalkovic!

code/addons/pseudo-states/src/preview/rewriteStyleSheet.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -198,24 +198,28 @@ const rewriteRuleContainer = (
198198
// @ts-expect-error We're adding this nonstandard property below
199199
numRewritten = cssRule.__pseudoStatesRewrittenCount;
200200
} else {
201-
if ('cssRules' in cssRule && (cssRule.cssRules as CSSRuleList).length) {
202-
numRewritten = rewriteRuleContainer(
203-
cssRule as CSSGroupingRule,
204-
rewriteLimit - count,
205-
forShadowDOM
206-
);
207-
} else {
208-
if (!('selectorText' in cssRule)) {
209-
continue;
210-
}
211-
const styleRule = cssRule as CSSStyleRule;
201+
let styleRule = cssRule as CSSStyleRule;
202+
203+
// Modify the rule, if it contains a pseudo state
204+
if ('selectorText' in styleRule) {
212205
if (matchOne.test(styleRule.selectorText)) {
213206
const newRule = rewriteRule(styleRule, forShadowDOM);
214207
ruleContainer.deleteRule(index);
215208
ruleContainer.insertRule(newRule, index);
209+
styleRule = ruleContainer.cssRules[index] as CSSStyleRule;
216210
numRewritten = 1;
217211
}
218212
}
213+
214+
// If it has nested rules, check them as well
215+
if ('cssRules' in styleRule && (styleRule.cssRules as CSSRuleList).length) {
216+
numRewritten = rewriteRuleContainer(
217+
styleRule as CSSGroupingRule,
218+
rewriteLimit - count,
219+
forShadowDOM
220+
);
221+
}
222+
219223
// @ts-expect-error We're adding this nonstandard property
220224
cssRule.__processed = true;
221225
// @ts-expect-error We're adding this nonstandard property
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type { Meta, StoryObj } from '@storybook/react-vite';
2+
3+
import { Button } from './NestedRules';
4+
5+
const meta = {
6+
title: 'NestedRules',
7+
component: Button,
8+
render: (args, context) => <Button {...args}>{context.name}</Button>,
9+
} satisfies Meta<typeof Button>;
10+
11+
export default meta;
12+
13+
type Story = StoryObj<typeof meta>;
14+
15+
export const NestedHover: Story = {
16+
parameters: {
17+
pseudo: { focusVisible: true },
18+
},
19+
// TODO: Use this test once the pseudostates addon uses the beforeEach API
20+
// play: async ({ canvas }) => {
21+
// const button = canvas.getByRole('button')!;
22+
// await expect(getComputedStyle(button).textDecorationLine).toBe('underline');
23+
// await expect(getComputedStyle(button).textDecorationColor).toBe('rgb(255, 0, 0)');
24+
// },
25+
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import React from 'react';
2+
3+
import './nested.css';
4+
5+
export const Button = (props: React.ButtonHTMLAttributes<HTMLButtonElement>) => (
6+
<button className="nested-focus-visible" {...props} />
7+
);
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
button {
2+
display: inline-block;
3+
cursor: pointer;
4+
border: 0;
5+
border-radius: 3em;
6+
background-color: #1ea7fd;
7+
padding: 11px 20px;
8+
color: white;
9+
font-weight: 700;
10+
font-size: 14px;
11+
line-height: 1;
12+
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
13+
}
14+
15+
.nested-focus-visible {
16+
&:focus-visible {
17+
@supports (color: color-mix(in lab, red, red)) {
18+
text-decoration: underline red;
19+
}
20+
}
21+
}

code/core/src/core-server/utils/get-new-story-file.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,42 @@ describe('get-new-story-file', () => {
171171
expect(storyFileContent).not.toContain(STORYBOOK_FN_PLACEHOLDER);
172172
});
173173

174+
it('should prevent XSS by escaping special characters in the component file name', async () => {
175+
const { storyFileContent } = await getNewStoryFile(
176+
{
177+
componentFilePath: "src/stories/Button';alert(document.domain);var a='.tsx",
178+
componentExportName: 'Button',
179+
componentIsDefaultExport: true,
180+
componentExportCount: 1,
181+
},
182+
{
183+
presets: {
184+
apply: (val: string) => {
185+
if (val === 'framework') {
186+
return Promise.resolve('@storybook/nextjs');
187+
}
188+
},
189+
},
190+
} as unknown as Options
191+
);
192+
193+
expect(storyFileContent).toMatchInlineSnapshot(`
194+
"import type { Meta, StoryObj } from '@storybook/nextjs';
195+
196+
import Buttonalert(documentDomain);varA=\\' from './Button\\';alert(document.domain);var a=\\'';
197+
198+
const meta = {
199+
component: Buttonalert(documentDomain);varA=\\',
200+
} satisfies Meta<typeof Buttonalert(documentDomain);varA=\\'>;
201+
202+
export default meta;
203+
204+
type Story = StoryObj<typeof meta>;
205+
206+
export const Default: Story = {};"
207+
`);
208+
});
209+
174210
it('should create a new story file (CSF factory)', async () => {
175211
const configDir = join(__dirname, '.storybook');
176212
const previewConfigPath = join(configDir, 'preview.ts');

code/core/src/core-server/utils/get-new-story-file.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
import { getCsfFactoryTemplateForNewStoryFile } from './new-story-templates/csf-factory-template';
2727
import { getJavaScriptTemplateForNewStoryFile } from './new-story-templates/javascript';
2828
import { getTypeScriptTemplateForNewStoryFile } from './new-story-templates/typescript';
29+
import { escapeForTemplate } from './safeString';
2930

3031
export async function getNewStoryFile(
3132
{
@@ -41,7 +42,7 @@ export async function getNewStoryFile(
4142

4243
const base = basename(componentFilePath);
4344
const extension = extname(componentFilePath);
44-
const basenameWithoutExtension = base.replace(extension, '');
45+
const basenameWithoutExtension = escapeForTemplate(base.replace(extension, ''));
4546
const dir = dirname(componentFilePath);
4647

4748
const { storyFileName, isTypescript, storyFileExtension } = getStoryMetadata(componentFilePath);
@@ -98,7 +99,9 @@ export async function getNewStoryFile(
9899
const storyFilePath = join(getProjectRoot(), dir);
99100
const relPath = relative(storyFilePath, previewConfigPath);
100101
const pathWithoutExt = relPath.replace(/\.(ts|js|mts|cts|tsx|jsx)$/, '');
101-
previewImportPath = pathWithoutExt.startsWith('.') ? pathWithoutExt : `./${pathWithoutExt}`;
102+
previewImportPath = escapeForTemplate(
103+
pathWithoutExt.startsWith('.') ? pathWithoutExt : `./${pathWithoutExt}`
104+
);
102105
}
103106
}
104107

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { escapeForTemplate } from './safeString';
4+
5+
describe('safeString', () => {
6+
describe('escapeForTemplate', () => {
7+
it('should escape backticks in template strings', () => {
8+
expect(escapeForTemplate('button`s.tsx')).toMatchInlineSnapshot('"button\\`s.tsx"');
9+
});
10+
11+
it('should escape dollar signs for template expressions', () => {
12+
expect(escapeForTemplate('button$file.tsx')).toMatchInlineSnapshot('"button\\$file.tsx"');
13+
});
14+
15+
it('should escape backslashes', () => {
16+
expect(escapeForTemplate('button\\file.tsx')).toMatchInlineSnapshot('"button\\\\file.tsx"');
17+
});
18+
19+
it('should escape quotes', () => {
20+
expect(escapeForTemplate("button's.tsx")).toMatchInlineSnapshot(`"button\\'s.tsx"`);
21+
expect(escapeForTemplate('button"s.tsx')).toMatchInlineSnapshot(`"button\\"s.tsx"`);
22+
});
23+
24+
it('should handle multiple special characters', () => {
25+
expect(escapeForTemplate('button`${file}\\path.tsx')).toMatchInlineSnapshot(
26+
`"button\\\`\\\${file}\\\\path.tsx"`
27+
);
28+
});
29+
30+
it('should preserve normal file paths', () => {
31+
expect(escapeForTemplate('./src/components/Button.tsx')).toMatchInlineSnapshot(
32+
'"./src/components/Button.tsx"'
33+
);
34+
});
35+
});
36+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* Escape special characters in a string for safe use within template literals in generated code.
3+
* This escapes backticks and template expression delimiters.
4+
*
5+
* @example
6+
*
7+
* ```ts
8+
* const fileName = "button's.tsx";
9+
* const template = `import Button from './${escapeForTemplate(fileName)}'`;
10+
* // Results in: import Button from './button\\'s.tsx'
11+
* ```
12+
*/
13+
export function escapeForTemplate(str: string): string {
14+
return str
15+
.replace(/\\/g, '\\\\') // Escape backslashes first
16+
.replace(/(['"$`])/g, '\\$&') // Then escape quotes, dollar signs, and backticks
17+
.replace(/[\n\r]/g, '\\$&'); // Then newlines
18+
}

0 commit comments

Comments
 (0)