Skip to content

Commit 86e1d2d

Browse files
authored
Fine Grain control for hashes and nonces (#29)
* Changing devAllowUnsafe to be more fine-grain by allowing the dev to decide when to allow hashes and nonces * Updating readme to reflect new options
1 parent b6d8fca commit 86e1d2d

File tree

3 files changed

+290
-115
lines changed

3 files changed

+290
-115
lines changed

README.md

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ This plugin will generate meta content for your Content Security Policy tag and
1212

1313
All inline JS and CSS will be hashed, and inserted into the policy.
1414

15-
1615
## Installation
1716

1817
Install the plugin with npm:
18+
1919
```
2020
npm i --save-dev csp-html-webpack-plugin
2121
```
@@ -32,20 +32,26 @@ new CspHtmlWebpackPlugin()
3232
## Configuration
3333

3434
This `CspHtmlWebpackPlugin` accepts 2 params with the following structure:
35-
* `{object}` Policy (optional) - a flat object which defines your CSP policy. Valid keys and values can be found on the [MDN CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy) page. Values can either be a string or an array of strings.
36-
* `{object}` Additional Options (optional) - a flat object with the optional configuration options:
37-
* `{boolean}` devAllowUnsafe - if you as the developer want to allow `unsafe-inline`/`unsafe-eval` and _not_ include hashes for inline scripts. If any hashes are included in the policy, modern browsers ignore the `unsafe-inline` rule.
38-
* `{boolean|Function}` enabled - if false, or the function returns false, the empty CSP tag will be stripped from the html output.
39-
* The `htmlPluginData` is passed into the function as it's first param.
40-
* If `enabled` is set the false, it will disable generating a CSP for all instances of `HtmlWebpackPlugin` in your webpack config.
41-
* `{string}` hashingMethod - accepts 'sha256', 'sha384', 'sha512' - your node version must also accept this hashing method.
35+
36+
- `{object}` Policy (optional) - a flat object which defines your CSP policy. Valid keys and values can be found on the [MDN CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy) page. Values can either be a string or an array of strings.
37+
- `{object}` Additional Options (optional) - a flat object with the optional configuration options:
38+
- `{boolean|Function}` enabled - if false, or the function returns false, the empty CSP tag will be stripped from the html output.
39+
- The `htmlPluginData` is passed into the function as it's first param.
40+
- If `enabled` is set the false, it will disable generating a CSP for all instances of `HtmlWebpackPlugin` in your webpack config.
41+
- `{string}` hashingMethod - accepts 'sha256', 'sha384', 'sha512' - your node version must also accept this hashing method.
42+
- `{object}` hashEnabled - a `<string, boolean>` entry for which policy rules are allowed to include hashes
43+
- `{object}` nonceEnabled - a `<string, boolean>` entry for which policy rules are allowed to include nonces
4244

4345
The plugin also adds a new config option onto each `HtmlWebpackPlugin` instance:
44-
* `{object}` cspPlugin - an object containing the following properties:
45-
* `{boolean}` enabled - if false, the CSP tag will be removed from the HTML which this HtmlWebpackPlugin instance is generating.
46-
* `{object}` policy - A custom policy which should be applied only to this instance of the HtmlWebpackPlugin
4746

48-
Note that policies are merged in the following order:
47+
- `{object}` cspPlugin - an object containing the following properties:
48+
- `{boolean}` enabled - if false, the CSP tag will be removed from the HTML which this HtmlWebpackPlugin instance is generating.
49+
- `{object}` policy - A custom policy which should be applied only to this instance of the HtmlWebpackPlugin
50+
- `{object}` hashEnabled - a `<string, boolean>` entry for which policy rules are allowed to include hashes
51+
- `{object}` nonceEnabled - a `<string, boolean>` entry for which policy rules are allowed to include nonces
52+
53+
Note that policies and `hashEnabled` / `nonceEnabled` are merged in the following order:
54+
4955
```
5056
> HtmlWebpackPlugin cspPlugin.policy
5157
> CspHtmlWebpackPlugin policy
@@ -72,10 +78,19 @@ If 2 policies have the same key/policy rule, the former policy will override the
7278
devAllowUnsafe: false,
7379
enabled: true
7480
hashingMethod: 'sha256',
81+
hashEnabled: {
82+
'script-src': true,
83+
'style-src': true
84+
},
85+
nonceEnabled: {
86+
'script-src': true,
87+
'style-src': true
88+
}
7589
}
7690
```
7791

7892
#### Full Configuration with all options:
93+
7994
```
8095
new HtmlWebpackPlugin({
8196
cspPlugin: {
@@ -85,6 +100,14 @@ new HtmlWebpackPlugin({
85100
'object-src': "'none'",
86101
'script-src': ["'unsafe-inline'", "'self'", "'unsafe-eval'"],
87102
'style-src': ["'unsafe-inline'", "'self'", "'unsafe-eval'"]
103+
},
104+
hashEnabled: {
105+
'script-src': true,
106+
'style-src': true
107+
},
108+
nonceEnabled: {
109+
'script-src': true,
110+
'style-src': true
88111
}
89112
}
90113
});
@@ -98,6 +121,14 @@ new CspHtmlWebpackPlugin({
98121
devAllowUnsafe: false,
99122
enabled: true
100123
hashingMethod: 'sha256',
124+
hashEnabled: {
125+
'script-src': true,
126+
'style-src': true
127+
},
128+
nonceEnabled: {
129+
'script-src': true,
130+
'style-src': true
131+
}
101132
})
102133
```
103134

plugin.jest.js

Lines changed: 197 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ describe('CspHtmlWebpackPlugin', () => {
1515
.mockImplementationOnce(() => 'mockedbase64string-1')
1616
.mockImplementationOnce(() => 'mockedbase64string-2')
1717
.mockImplementationOnce(() => 'mockedbase64string-3')
18+
.mockImplementationOnce(() => 'mockedbase64string-4')
19+
.mockImplementationOnce(() => 'mockedbase64string-5')
20+
.mockImplementationOnce(() => 'mockedbase64string-6')
1821
.mockImplementation(
1922
() => new Error('Need to add more crypto.randomBytes mocks')
2023
);
@@ -429,85 +432,215 @@ describe('CspHtmlWebpackPlugin', () => {
429432
});
430433
});
431434
});
435+
});
432436

433-
describe('unsafe-inline / unsafe-eval', () => {
434-
it('skips the hashing / nonceing of the scripts and styles it finds if devAllowUnsafe is true', done => {
435-
const config = createWebpackConfig([
436-
new HtmlWebpackPlugin({
437-
filename: path.join(WEBPACK_OUTPUT_DIR, 'index.html'),
438-
template: path.join(
439-
__dirname,
440-
'test-utils',
441-
'fixtures',
442-
'with-script-and-style.html'
443-
)
444-
}),
445-
new CspHtmlWebpackPlugin(
446-
{
447-
'base-uri': ["'self'", 'https://slack.com'],
448-
'font-src': ["'self'", "'https://a-slack-edge.com'"],
449-
'script-src': ["'self'", "'unsafe-inline'"],
450-
'style-src': ["'self'", "'unsafe-eval'"]
451-
},
452-
{
453-
devAllowUnsafe: true
437+
describe('Hash / Nonce enabled check', () => {
438+
it("doesn't add hashes to any policy rule if that policy rule has been globally disabled", done => {
439+
const config = createWebpackConfig([
440+
new HtmlWebpackPlugin({
441+
filename: path.join(WEBPACK_OUTPUT_DIR, 'index-1.html'),
442+
template: path.join(
443+
__dirname,
444+
'test-utils',
445+
'fixtures',
446+
'with-script-and-style.html'
447+
)
448+
}),
449+
new HtmlWebpackPlugin({
450+
filename: path.join(WEBPACK_OUTPUT_DIR, 'index-2.html'),
451+
template: path.join(
452+
__dirname,
453+
'test-utils',
454+
'fixtures',
455+
'with-script-and-style.html'
456+
)
457+
}),
458+
new CspHtmlWebpackPlugin(
459+
{},
460+
{
461+
hashEnabled: {
462+
'script-src': false,
463+
'style-src': false
454464
}
465+
}
466+
)
467+
]);
468+
469+
webpackCompile(config, csps => {
470+
const expected1 =
471+
"base-uri 'self';" +
472+
" object-src 'none';" +
473+
" script-src 'unsafe-inline' 'self' 'unsafe-eval' 'nonce-mockedbase64string-1' 'nonce-mockedbase64string-2';" +
474+
" style-src 'unsafe-inline' 'self' 'unsafe-eval' 'nonce-mockedbase64string-3'";
475+
476+
const expected2 =
477+
"base-uri 'self';" +
478+
" object-src 'none';" +
479+
" script-src 'unsafe-inline' 'self' 'unsafe-eval' 'nonce-mockedbase64string-4' 'nonce-mockedbase64string-5';" +
480+
" style-src 'unsafe-inline' 'self' 'unsafe-eval' 'nonce-mockedbase64string-6'";
481+
482+
// no hashes in either one of the script-src or style-src policies
483+
expect(csps['index-1.html']).toEqual(expected1);
484+
expect(csps['index-2.html']).toEqual(expected2);
485+
486+
done();
487+
});
488+
});
489+
490+
it("doesn't add nonces to any policy rule if that policy rule has been globally disabled", done => {
491+
const config = createWebpackConfig([
492+
new HtmlWebpackPlugin({
493+
filename: path.join(WEBPACK_OUTPUT_DIR, 'index-1.html'),
494+
template: path.join(
495+
__dirname,
496+
'test-utils',
497+
'fixtures',
498+
'with-script-and-style.html'
455499
)
456-
]);
500+
}),
501+
new HtmlWebpackPlugin({
502+
filename: path.join(WEBPACK_OUTPUT_DIR, 'index-2.html'),
503+
template: path.join(
504+
__dirname,
505+
'test-utils',
506+
'fixtures',
507+
'with-script-and-style.html'
508+
)
509+
}),
510+
new CspHtmlWebpackPlugin(
511+
{},
512+
{
513+
nonceEnabled: {
514+
'script-src': false,
515+
'style-src': false
516+
}
517+
}
518+
)
519+
]);
457520

458-
webpackCompile(config, csps => {
459-
const expected =
460-
"base-uri 'self' https://slack.com;" +
461-
" object-src 'none';" +
462-
" script-src 'self' 'unsafe-inline';" +
463-
" style-src 'self' 'unsafe-eval';" +
464-
" font-src 'self' 'https://a-slack-edge.com'";
521+
webpackCompile(config, csps => {
522+
const expected1 =
523+
"base-uri 'self';" +
524+
" object-src 'none';" +
525+
" script-src 'unsafe-inline' 'self' 'unsafe-eval' 'sha256-ixjZMYNfWQWawUHioWOx2jBsTmfxucX7IlwsMt2jWvc=';" +
526+
" style-src 'unsafe-inline' 'self' 'unsafe-eval' 'sha256-MqG77yUiqBo4MMVZAl09WSafnQY4Uu3cSdZPKxaf9sQ='";
465527

466-
expect(csps['index.html']).toEqual(expected);
467-
done();
468-
});
528+
const expected2 =
529+
"base-uri 'self';" +
530+
" object-src 'none';" +
531+
" script-src 'unsafe-inline' 'self' 'unsafe-eval' 'sha256-ixjZMYNfWQWawUHioWOx2jBsTmfxucX7IlwsMt2jWvc=';" +
532+
" style-src 'unsafe-inline' 'self' 'unsafe-eval' 'sha256-MqG77yUiqBo4MMVZAl09WSafnQY4Uu3cSdZPKxaf9sQ='";
533+
534+
// no nonces in either one of the script-src or style-src policies
535+
expect(csps['index-1.html']).toEqual(expected1);
536+
expect(csps['index-2.html']).toEqual(expected2);
537+
538+
done();
469539
});
540+
});
470541

471-
it('continues hashing / nonceing scripts and styles if unsafe-inline/unsafe-eval is included, but devAllowUnsafe is false', done => {
472-
const config = createWebpackConfig([
473-
new HtmlWebpackPlugin({
474-
filename: path.join(WEBPACK_OUTPUT_DIR, 'index.html'),
475-
template: path.join(
476-
__dirname,
477-
'test-utils',
478-
'fixtures',
479-
'with-script-and-style.html'
480-
)
481-
}),
482-
new CspHtmlWebpackPlugin(
483-
{
484-
'base-uri': ["'self'", 'https://slack.com'],
485-
'font-src': ["'self'", "'https://a-slack-edge.com'"],
486-
'script-src': ["'self'", "'unsafe-inline'"],
487-
'style-src': ["'self'", "'unsafe-eval'"]
488-
},
489-
{
490-
devAllowUnsafe: false
542+
it("doesn't add hashes to a specific policy rule if that policy rule has been disabled for that instance of HtmlWebpackPlugin", done => {
543+
const config = createWebpackConfig([
544+
new HtmlWebpackPlugin({
545+
filename: path.join(WEBPACK_OUTPUT_DIR, 'index-no-hashes.html'),
546+
template: path.join(
547+
__dirname,
548+
'test-utils',
549+
'fixtures',
550+
'with-script-and-style.html'
551+
),
552+
cspPlugin: {
553+
hashEnabled: {
554+
'script-src': false,
555+
'style-src': false
491556
}
557+
}
558+
}),
559+
new HtmlWebpackPlugin({
560+
filename: path.join(WEBPACK_OUTPUT_DIR, 'index-hashes.html'),
561+
template: path.join(
562+
__dirname,
563+
'test-utils',
564+
'fixtures',
565+
'with-script-and-style.html'
492566
)
493-
]);
567+
}),
568+
new CspHtmlWebpackPlugin()
569+
]);
494570

495-
webpackCompile(config, csps => {
496-
const expected =
497-
"base-uri 'self' https://slack.com;" +
498-
" object-src 'none';" +
499-
" script-src 'self' 'unsafe-inline' 'sha256-ixjZMYNfWQWawUHioWOx2jBsTmfxucX7IlwsMt2jWvc=' 'nonce-mockedbase64string-1' 'nonce-mockedbase64string-2';" +
500-
" style-src 'self' 'unsafe-eval' 'sha256-MqG77yUiqBo4MMVZAl09WSafnQY4Uu3cSdZPKxaf9sQ=' 'nonce-mockedbase64string-3';" +
501-
" font-src 'self' 'https://a-slack-edge.com'";
571+
webpackCompile(config, csps => {
572+
const expectedNoHashes =
573+
"base-uri 'self';" +
574+
" object-src 'none';" +
575+
" script-src 'unsafe-inline' 'self' 'unsafe-eval' 'nonce-mockedbase64string-1' 'nonce-mockedbase64string-2';" +
576+
" style-src 'unsafe-inline' 'self' 'unsafe-eval' 'nonce-mockedbase64string-3'";
502577

503-
expect(csps['index.html']).toEqual(expected);
504-
done();
505-
});
578+
const expectedHashes =
579+
"base-uri 'self';" +
580+
" object-src 'none';" +
581+
" script-src 'unsafe-inline' 'self' 'unsafe-eval' 'sha256-ixjZMYNfWQWawUHioWOx2jBsTmfxucX7IlwsMt2jWvc=' 'nonce-mockedbase64string-4' 'nonce-mockedbase64string-5';" +
582+
" style-src 'unsafe-inline' 'self' 'unsafe-eval' 'sha256-MqG77yUiqBo4MMVZAl09WSafnQY4Uu3cSdZPKxaf9sQ=' 'nonce-mockedbase64string-6'";
583+
584+
// no hashes in index-no-hashes script-src or style-src policies
585+
expect(csps['index-no-hashes.html']).toEqual(expectedNoHashes);
586+
expect(csps['index-hashes.html']).toEqual(expectedHashes);
587+
588+
done();
589+
});
590+
});
591+
592+
it("doesn't add nonces to a specific policy rule if that policy rule has been disabled for that instance of HtmlWebpackPlugin", done => {
593+
const config = createWebpackConfig([
594+
new HtmlWebpackPlugin({
595+
filename: path.join(WEBPACK_OUTPUT_DIR, 'index-no-nonce.html'),
596+
template: path.join(
597+
__dirname,
598+
'test-utils',
599+
'fixtures',
600+
'with-script-and-style.html'
601+
),
602+
cspPlugin: {
603+
nonceEnabled: {
604+
'script-src': false,
605+
'style-src': false
606+
}
607+
}
608+
}),
609+
new HtmlWebpackPlugin({
610+
filename: path.join(WEBPACK_OUTPUT_DIR, 'index-nonce.html'),
611+
template: path.join(
612+
__dirname,
613+
'test-utils',
614+
'fixtures',
615+
'with-script-and-style.html'
616+
)
617+
}),
618+
new CspHtmlWebpackPlugin()
619+
]);
620+
621+
webpackCompile(config, csps => {
622+
const expectedNoNonce =
623+
"base-uri 'self';" +
624+
" object-src 'none';" +
625+
" script-src 'unsafe-inline' 'self' 'unsafe-eval' 'sha256-ixjZMYNfWQWawUHioWOx2jBsTmfxucX7IlwsMt2jWvc=';" +
626+
" style-src 'unsafe-inline' 'self' 'unsafe-eval' 'sha256-MqG77yUiqBo4MMVZAl09WSafnQY4Uu3cSdZPKxaf9sQ='";
627+
628+
const expectedNonce =
629+
"base-uri 'self';" +
630+
" object-src 'none';" +
631+
" script-src 'unsafe-inline' 'self' 'unsafe-eval' 'sha256-ixjZMYNfWQWawUHioWOx2jBsTmfxucX7IlwsMt2jWvc=' 'nonce-mockedbase64string-1' 'nonce-mockedbase64string-2';" +
632+
" style-src 'unsafe-inline' 'self' 'unsafe-eval' 'sha256-MqG77yUiqBo4MMVZAl09WSafnQY4Uu3cSdZPKxaf9sQ=' 'nonce-mockedbase64string-3'";
633+
634+
// no nonce in index-no-nonce script-src or style-src policies
635+
expect(csps['index-no-nonce.html']).toEqual(expectedNoNonce);
636+
expect(csps['index-nonce.html']).toEqual(expectedNonce);
637+
638+
done();
506639
});
507640
});
508641
});
509642

510-
describe('Enabled check', () => {
643+
describe('Plugin enabled check', () => {
511644
it("doesn't modify the html if enabled is the bool false", done => {
512645
const config = createWebpackConfig([
513646
new HtmlWebpackPlugin({

0 commit comments

Comments
 (0)