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

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 43 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ This plugin will generate meta content for your Content Security Policy tag and

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


## Installation

Install the plugin with npm:

```
npm i --save-dev csp-html-webpack-plugin
```
Expand All @@ -32,20 +32,26 @@ new CspHtmlWebpackPlugin()
## Configuration

This `CspHtmlWebpackPlugin` accepts 2 params with the following structure:
* `{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.
* `{object}` Additional Options (optional) - a flat object with the optional configuration options:
* `{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.
* `{boolean|Function}` enabled - if false, or the function returns false, the empty CSP tag will be stripped from the html output.
* The `htmlPluginData` is passed into the function as it's first param.
* If `enabled` is set the false, it will disable generating a CSP for all instances of `HtmlWebpackPlugin` in your webpack config.
* `{string}` hashingMethod - accepts 'sha256', 'sha384', 'sha512' - your node version must also accept this hashing method.

- `{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.
- `{object}` Additional Options (optional) - a flat object with the optional configuration options:
- `{boolean|Function}` enabled - if false, or the function returns false, the empty CSP tag will be stripped from the html output.
- The `htmlPluginData` is passed into the function as it's first param.
- If `enabled` is set the false, it will disable generating a CSP for all instances of `HtmlWebpackPlugin` in your webpack config.
- `{string}` hashingMethod - accepts 'sha256', 'sha384', 'sha512' - your node version must also accept this hashing method.
- `{object}` hashEnabled - a `<string, boolean>` entry for which policy rules are allowed to include hashes
- `{object}` nonceEnabled - a `<string, boolean>` entry for which policy rules are allowed to include nonces

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

Note that policies are merged in the following order:
- `{object}` cspPlugin - an object containing the following properties:
- `{boolean}` enabled - if false, the CSP tag will be removed from the HTML which this HtmlWebpackPlugin instance is generating.
- `{object}` policy - A custom policy which should be applied only to this instance of the HtmlWebpackPlugin
- `{object}` hashEnabled - a `<string, boolean>` entry for which policy rules are allowed to include hashes
- `{object}` nonceEnabled - a `<string, boolean>` entry for which policy rules are allowed to include nonces

Note that policies and `hashEnabled` / `nonceEnabled` are merged in the following order:

```
> HtmlWebpackPlugin cspPlugin.policy
> CspHtmlWebpackPlugin policy
Expand All @@ -72,10 +78,19 @@ If 2 policies have the same key/policy rule, the former policy will override the
devAllowUnsafe: false,
enabled: true
hashingMethod: 'sha256',
hashEnabled: {
'script-src': true,
'style-src': true
},
nonceEnabled: {
'script-src': true,
'style-src': true
}
}
```

#### Full Configuration with all options:

```
new HtmlWebpackPlugin({
cspPlugin: {
Expand All @@ -85,6 +100,14 @@ new HtmlWebpackPlugin({
'object-src': "'none'",
'script-src': ["'unsafe-inline'", "'self'", "'unsafe-eval'"],
'style-src': ["'unsafe-inline'", "'self'", "'unsafe-eval'"]
},
hashEnabled: {
'script-src': true,
'style-src': true
},
nonceEnabled: {
'script-src': true,
'style-src': true
}
}
});
Expand All @@ -98,6 +121,14 @@ new CspHtmlWebpackPlugin({
devAllowUnsafe: false,
enabled: true
hashingMethod: 'sha256',
hashEnabled: {
'script-src': true,
'style-src': true
},
nonceEnabled: {
'script-src': true,
'style-src': true
}
})
```

Expand Down
261 changes: 197 additions & 64 deletions plugin.jest.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ describe('CspHtmlWebpackPlugin', () => {
.mockImplementationOnce(() => 'mockedbase64string-1')
.mockImplementationOnce(() => 'mockedbase64string-2')
.mockImplementationOnce(() => 'mockedbase64string-3')
.mockImplementationOnce(() => 'mockedbase64string-4')
.mockImplementationOnce(() => 'mockedbase64string-5')
.mockImplementationOnce(() => 'mockedbase64string-6')
.mockImplementation(
() => new Error('Need to add more crypto.randomBytes mocks')
);
Expand Down Expand Up @@ -429,85 +432,215 @@ describe('CspHtmlWebpackPlugin', () => {
});
});
});
});

describe('unsafe-inline / unsafe-eval', () => {
it('skips the hashing / nonceing of the scripts and styles it finds if devAllowUnsafe is true', done => {
const config = createWebpackConfig([
new HtmlWebpackPlugin({
filename: path.join(WEBPACK_OUTPUT_DIR, 'index.html'),
template: path.join(
__dirname,
'test-utils',
'fixtures',
'with-script-and-style.html'
)
}),
new CspHtmlWebpackPlugin(
{
'base-uri': ["'self'", 'https://slack.com'],
'font-src': ["'self'", "'https://a-slack-edge.com'"],
'script-src': ["'self'", "'unsafe-inline'"],
'style-src': ["'self'", "'unsafe-eval'"]
},
{
devAllowUnsafe: true
describe('Hash / Nonce enabled check', () => {
it("doesn't add hashes to any policy rule if that policy rule has been globally disabled", done => {
const config = createWebpackConfig([
new HtmlWebpackPlugin({
filename: path.join(WEBPACK_OUTPUT_DIR, 'index-1.html'),
template: path.join(
__dirname,
'test-utils',
'fixtures',
'with-script-and-style.html'
)
}),
new HtmlWebpackPlugin({
filename: path.join(WEBPACK_OUTPUT_DIR, 'index-2.html'),
template: path.join(
__dirname,
'test-utils',
'fixtures',
'with-script-and-style.html'
)
}),
new CspHtmlWebpackPlugin(
{},
{
hashEnabled: {
'script-src': false,
'style-src': false
}
}
)
]);

webpackCompile(config, csps => {
const expected1 =
"base-uri 'self';" +
" object-src 'none';" +
" script-src 'unsafe-inline' 'self' 'unsafe-eval' 'nonce-mockedbase64string-1' 'nonce-mockedbase64string-2';" +
" style-src 'unsafe-inline' 'self' 'unsafe-eval' 'nonce-mockedbase64string-3'";

const expected2 =
"base-uri 'self';" +
" object-src 'none';" +
" script-src 'unsafe-inline' 'self' 'unsafe-eval' 'nonce-mockedbase64string-4' 'nonce-mockedbase64string-5';" +
" style-src 'unsafe-inline' 'self' 'unsafe-eval' 'nonce-mockedbase64string-6'";

// no hashes in either one of the script-src or style-src policies
expect(csps['index-1.html']).toEqual(expected1);
expect(csps['index-2.html']).toEqual(expected2);

done();
});
});

it("doesn't add nonces to any policy rule if that policy rule has been globally disabled", done => {
const config = createWebpackConfig([
new HtmlWebpackPlugin({
filename: path.join(WEBPACK_OUTPUT_DIR, 'index-1.html'),
template: path.join(
__dirname,
'test-utils',
'fixtures',
'with-script-and-style.html'
)
]);
}),
new HtmlWebpackPlugin({
filename: path.join(WEBPACK_OUTPUT_DIR, 'index-2.html'),
template: path.join(
__dirname,
'test-utils',
'fixtures',
'with-script-and-style.html'
)
}),
new CspHtmlWebpackPlugin(
{},
{
nonceEnabled: {
'script-src': false,
'style-src': false
}
}
)
]);

webpackCompile(config, csps => {
const expected =
"base-uri 'self' https://slack.com;" +
" object-src 'none';" +
" script-src 'self' 'unsafe-inline';" +
" style-src 'self' 'unsafe-eval';" +
" font-src 'self' 'https://a-slack-edge.com'";
webpackCompile(config, csps => {
const expected1 =
"base-uri 'self';" +
" object-src 'none';" +
" script-src 'unsafe-inline' 'self' 'unsafe-eval' 'sha256-ixjZMYNfWQWawUHioWOx2jBsTmfxucX7IlwsMt2jWvc=';" +
" style-src 'unsafe-inline' 'self' 'unsafe-eval' 'sha256-MqG77yUiqBo4MMVZAl09WSafnQY4Uu3cSdZPKxaf9sQ='";

expect(csps['index.html']).toEqual(expected);
done();
});
const expected2 =
"base-uri 'self';" +
" object-src 'none';" +
" script-src 'unsafe-inline' 'self' 'unsafe-eval' 'sha256-ixjZMYNfWQWawUHioWOx2jBsTmfxucX7IlwsMt2jWvc=';" +
" style-src 'unsafe-inline' 'self' 'unsafe-eval' 'sha256-MqG77yUiqBo4MMVZAl09WSafnQY4Uu3cSdZPKxaf9sQ='";

// no nonces in either one of the script-src or style-src policies
expect(csps['index-1.html']).toEqual(expected1);
expect(csps['index-2.html']).toEqual(expected2);

done();
});
});

it('continues hashing / nonceing scripts and styles if unsafe-inline/unsafe-eval is included, but devAllowUnsafe is false', done => {
const config = createWebpackConfig([
new HtmlWebpackPlugin({
filename: path.join(WEBPACK_OUTPUT_DIR, 'index.html'),
template: path.join(
__dirname,
'test-utils',
'fixtures',
'with-script-and-style.html'
)
}),
new CspHtmlWebpackPlugin(
{
'base-uri': ["'self'", 'https://slack.com'],
'font-src': ["'self'", "'https://a-slack-edge.com'"],
'script-src': ["'self'", "'unsafe-inline'"],
'style-src': ["'self'", "'unsafe-eval'"]
},
{
devAllowUnsafe: false
it("doesn't add hashes to a specific policy rule if that policy rule has been disabled for that instance of HtmlWebpackPlugin", done => {
const config = createWebpackConfig([
new HtmlWebpackPlugin({
filename: path.join(WEBPACK_OUTPUT_DIR, 'index-no-hashes.html'),
template: path.join(
__dirname,
'test-utils',
'fixtures',
'with-script-and-style.html'
),
cspPlugin: {
hashEnabled: {
'script-src': false,
'style-src': false
}
}
}),
new HtmlWebpackPlugin({
filename: path.join(WEBPACK_OUTPUT_DIR, 'index-hashes.html'),
template: path.join(
__dirname,
'test-utils',
'fixtures',
'with-script-and-style.html'
)
]);
}),
new CspHtmlWebpackPlugin()
]);

webpackCompile(config, csps => {
const expected =
"base-uri 'self' https://slack.com;" +
" object-src 'none';" +
" script-src 'self' 'unsafe-inline' 'sha256-ixjZMYNfWQWawUHioWOx2jBsTmfxucX7IlwsMt2jWvc=' 'nonce-mockedbase64string-1' 'nonce-mockedbase64string-2';" +
" style-src 'self' 'unsafe-eval' 'sha256-MqG77yUiqBo4MMVZAl09WSafnQY4Uu3cSdZPKxaf9sQ=' 'nonce-mockedbase64string-3';" +
" font-src 'self' 'https://a-slack-edge.com'";
webpackCompile(config, csps => {
const expectedNoHashes =
"base-uri 'self';" +
" object-src 'none';" +
" script-src 'unsafe-inline' 'self' 'unsafe-eval' 'nonce-mockedbase64string-1' 'nonce-mockedbase64string-2';" +
" style-src 'unsafe-inline' 'self' 'unsafe-eval' 'nonce-mockedbase64string-3'";

expect(csps['index.html']).toEqual(expected);
done();
});
const expectedHashes =
"base-uri 'self';" +
" object-src 'none';" +
" script-src 'unsafe-inline' 'self' 'unsafe-eval' 'sha256-ixjZMYNfWQWawUHioWOx2jBsTmfxucX7IlwsMt2jWvc=' 'nonce-mockedbase64string-4' 'nonce-mockedbase64string-5';" +
" style-src 'unsafe-inline' 'self' 'unsafe-eval' 'sha256-MqG77yUiqBo4MMVZAl09WSafnQY4Uu3cSdZPKxaf9sQ=' 'nonce-mockedbase64string-6'";

// no hashes in index-no-hashes script-src or style-src policies
expect(csps['index-no-hashes.html']).toEqual(expectedNoHashes);
expect(csps['index-hashes.html']).toEqual(expectedHashes);

done();
});
});

it("doesn't add nonces to a specific policy rule if that policy rule has been disabled for that instance of HtmlWebpackPlugin", done => {
const config = createWebpackConfig([
new HtmlWebpackPlugin({
filename: path.join(WEBPACK_OUTPUT_DIR, 'index-no-nonce.html'),
template: path.join(
__dirname,
'test-utils',
'fixtures',
'with-script-and-style.html'
),
cspPlugin: {
nonceEnabled: {
'script-src': false,
'style-src': false
}
}
}),
new HtmlWebpackPlugin({
filename: path.join(WEBPACK_OUTPUT_DIR, 'index-nonce.html'),
template: path.join(
__dirname,
'test-utils',
'fixtures',
'with-script-and-style.html'
)
}),
new CspHtmlWebpackPlugin()
]);

webpackCompile(config, csps => {
const expectedNoNonce =
"base-uri 'self';" +
" object-src 'none';" +
" script-src 'unsafe-inline' 'self' 'unsafe-eval' 'sha256-ixjZMYNfWQWawUHioWOx2jBsTmfxucX7IlwsMt2jWvc=';" +
" style-src 'unsafe-inline' 'self' 'unsafe-eval' 'sha256-MqG77yUiqBo4MMVZAl09WSafnQY4Uu3cSdZPKxaf9sQ='";

const expectedNonce =
"base-uri 'self';" +
" object-src 'none';" +
" script-src 'unsafe-inline' 'self' 'unsafe-eval' 'sha256-ixjZMYNfWQWawUHioWOx2jBsTmfxucX7IlwsMt2jWvc=' 'nonce-mockedbase64string-1' 'nonce-mockedbase64string-2';" +
" style-src 'unsafe-inline' 'self' 'unsafe-eval' 'sha256-MqG77yUiqBo4MMVZAl09WSafnQY4Uu3cSdZPKxaf9sQ=' 'nonce-mockedbase64string-3'";

// no nonce in index-no-nonce script-src or style-src policies
expect(csps['index-no-nonce.html']).toEqual(expectedNoNonce);
expect(csps['index-nonce.html']).toEqual(expectedNonce);

done();
});
});
});

describe('Enabled check', () => {
describe('Plugin enabled check', () => {
it("doesn't modify the html if enabled is the bool false", done => {
const config = createWebpackConfig([
new HtmlWebpackPlugin({
Expand Down
Loading