Skip to content

Commit 43b4e83

Browse files
authored
feat: support security.nonce for add nonce attribute on script tag (#3725)
* feat: support security.nonce for add nonce attribute on script tag * chore: add test case * docs: change changeset * feat: update nonce docs
1 parent 67a4c9b commit 43b4e83

File tree

45 files changed

+471
-30
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+471
-30
lines changed

.changeset/weak-weeks-mate.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@modern-js/builder-webpack-provider': patch
3+
'@modern-js/builder-rspack-provider': patch
4+
'@modern-js/builder-shared': patch
5+
'@modern-js/runtime': patch
6+
'@modern-js/app-tools': patch
7+
'@modern-js/prod-server': patch
8+
'@modern-js/builder': patch
9+
'@modern-js/types': patch
10+
'@modern-js/utils': patch
11+
'@modern-js/server-core': patch
12+
---
13+
14+
feat: support security.nonce for add nonce attribute on script tag
15+
feat: 支持 security.nonce 配置,为 script 标签添加 nonce 属性

packages/builder/builder-rspack-provider/src/config/defaults.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export const createDefaultConfig = () =>
2222
output: getDefaultOutputConfig(),
2323
tools: {},
2424
security: {
25+
nonce: '',
2526
// sri: false
2627
},
2728
performance: {
Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
1-
// TODO
2-
// eslint-disable-next-line @typescript-eslint/no-empty-interface
3-
export interface SecurityConfig {}
1+
// Todo support security.sri configuration
2+
/**
3+
* Currently, rspack does not support the security.sri configuration.
4+
* But it should because it's a shared configuration.
5+
*/
6+
// import type { SharedSecurityConfig } from '@modern-js/builder-shared';
7+
8+
// export type SecurityConfig = SharedSecurityConfig;
9+
10+
// export type NormalizedSecurityConfig = Required<SecurityConfig>;
11+
12+
export interface SecurityConfig {
13+
nonce?: string;
14+
}
415

516
export type NormalizedSecurityConfig = Required<SecurityConfig>;

packages/builder/builder-rspack-provider/tests/plugins/html.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,26 @@ describe('plugins/html', () => {
1919
expect(bundlerConfigs[0]).toMatchSnapshot();
2020
});
2121

22+
it('should register nonce plugin when using security.nonce', async () => {
23+
const builder = await createBuilder({
24+
plugins: [builderPluginEntry(), builderPluginHtml()],
25+
entry: {
26+
main: './src/main.ts',
27+
},
28+
builderConfig: {
29+
security: {
30+
nonce: 'test-nonce',
31+
},
32+
},
33+
});
34+
35+
const {
36+
origin: { bundlerConfigs },
37+
} = await builder.inspectConfig();
38+
39+
expect(matchPlugin(bundlerConfigs[0], 'HtmlNoncePlugin')).toBeDefined();
40+
});
41+
2242
it('should register crossorigin plugin when using html.crossorigin', async () => {
2343
const builder = await createBuilder({
2444
plugins: [builderPluginEntry(), builderPluginHtml()],
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type HtmlWebpackPlugin from 'html-webpack-plugin';
2+
import type { Compiler, WebpackPluginInstance } from 'webpack';
3+
4+
type NonceOptions = {
5+
nonce: string;
6+
HtmlPlugin: typeof HtmlWebpackPlugin;
7+
};
8+
9+
export class HtmlNoncePlugin implements WebpackPluginInstance {
10+
readonly name: string;
11+
12+
readonly nonce: string;
13+
14+
readonly HtmlPlugin: typeof HtmlWebpackPlugin;
15+
16+
constructor(options: NonceOptions) {
17+
const { nonce } = options;
18+
this.name = 'HtmlNoncePlugin';
19+
this.nonce = nonce;
20+
this.HtmlPlugin = options.HtmlPlugin;
21+
}
22+
23+
apply(compiler: Compiler): void {
24+
if (!this.nonce) {
25+
return;
26+
}
27+
28+
compiler.hooks.compilation.tap(this.name, compilation => {
29+
this.HtmlPlugin.getHooks(compilation).alterAssetTags.tapPromise(
30+
this.name,
31+
async alterAssetTags => {
32+
const {
33+
assetTags: { scripts },
34+
} = alterAssetTags;
35+
36+
scripts.forEach(script => (script.attributes.nonce = this.nonce));
37+
return alterAssetTags;
38+
},
39+
);
40+
});
41+
}
42+
}

packages/builder/builder-shared/src/plugins/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export { AutoSetRootFontSizePlugin } from './AutoSetRootFontSizePlugin';
22
export { HtmlTagsPlugin } from './HtmlTagsPlugin';
33
export type { HtmlTagsPluginOptions } from './HtmlTagsPlugin';
44
export { HtmlCrossOriginPlugin } from './HtmlCrossOriginPlugin';
5+
export { HtmlNoncePlugin } from './HtmlNoncePlugin';
56
export { HtmlAppIconPlugin } from './HtmlAppIconPlugin';
67
export { HtmlFaviconUrlPlugin, type FaviconUrls } from './HtmlFaviconUrlPlugin';
78
export { InlineChunkHtmlPlugin } from './InlineChunkHtmlPlugin';

packages/builder/builder-shared/src/types/config/security.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,10 @@ export interface SharedSecurityConfig {
1010
* verify the integrity of the introduced resource, thus preventing tampering with the downloaded resource.
1111
*/
1212
sri?: SriOptions | boolean;
13+
14+
/**
15+
* Adding an nonce attribute to sub-resources introduced by HTML allows the browser to
16+
* verify the nonce of the introduced resource, thus preventing xss.
17+
*/
18+
nonce?: string;
1319
}

packages/builder/builder-webpack-provider/src/config/defaults.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export const createDefaultConfig = () =>
2727
define: {},
2828
},
2929
output: getDefaultOutputConfig(),
30-
security: { sri: false, checkSyntax: false },
30+
security: { sri: false, checkSyntax: false, nonce: '' },
3131
experiments: {
3232
lazyCompilation: false,
3333
},

packages/builder/builder-webpack-provider/tests/plugins/html.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,22 @@ describe('plugins/html', () => {
5050
expect(await builder.matchWebpackPlugin('HtmlWebpackPlugin')).toBeFalsy();
5151
});
5252

53+
it('should register nonce plugin when using security.nonce', async () => {
54+
const builder = await createStubBuilder({
55+
plugins: [builderPluginEntry(), builderPluginHtml()],
56+
entry: {
57+
main: './src/main.ts',
58+
},
59+
builderConfig: {
60+
security: {
61+
nonce: 'test-nonce',
62+
},
63+
},
64+
});
65+
66+
expect(await builder.matchWebpackPlugin('HtmlNoncePlugin')).toBeTruthy();
67+
});
68+
5369
it('should register crossorigin plugin when using html.crossorigin', async () => {
5470
const builder = await createStubBuilder({
5571
plugins: [builderPluginEntry(), builderPluginHtml()],

packages/builder/builder/src/plugins/html.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,20 @@ export const builderPluginHtml = (): DefaultBuilderPlugin => ({
205205
}),
206206
);
207207

208+
if (config.security) {
209+
const { nonce } = config.security;
210+
211+
if (nonce) {
212+
const { HtmlNoncePlugin } = await import(
213+
'@modern-js/builder-shared'
214+
);
215+
216+
chain
217+
.plugin(CHAIN_ID.PLUGIN.HTML_NONCE)
218+
.use(HtmlNoncePlugin, [{ nonce, HtmlPlugin }]);
219+
}
220+
}
221+
208222
if (config.html) {
209223
const { appIcon, crossorigin } = config.html;
210224

0 commit comments

Comments
 (0)