Skip to content

Commit 1cedba7

Browse files
authored
An option for disabling unsafe inline (#22)
* Adding option for the dev to choose whether to allow unsafe eval/inline in their csp policy or not. Defaults to false for backwards compatibility * Updating README to reflect the new devAllowUnsafe option
1 parent a081bc2 commit 1cedba7

File tree

3 files changed

+153
-43
lines changed

3 files changed

+153
-43
lines changed

README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,11 @@ Finally, add the following tag to your HTML template where you would like to add
3939
This `CspHtmlWebpackPlugin` accepts 2 params with the following structure:
4040
* `{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.
4141
* `{object}` Additional Options (optional) - a flat object with the optional configuration options:
42-
* `{string}` hashingMethod - accepts 'sha256', 'sha384', 'sha512' - your node version must also accept this hashing method.
42+
* `{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.
4343
* `{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.
44+
* `{string}` hashingMethod - accepts 'sha256', 'sha384', 'sha512' - your node version must also accept this hashing method.
4445

45-
_Note: CSP usually runs on all files processed from HTML Webpack plugin, to disable it for a particular instance, set `disableCspPlugin` to `true(boolean)` in [HTML Webpack Plugins Options](https://github.com/jantimon/html-webpack-plugin#options)_
46+
_Note: CSP runs on all files created by HTMLWebpackPlugin. You can disable it for a particular instance by setting `disableCspPlugin` to `true` in the HTMLWebpackPlugin options
4647

4748
#### Default Policy:
4849

@@ -59,8 +60,9 @@ _Note: CSP usually runs on all files processed from HTML Webpack plugin, to disa
5960

6061
```
6162
{
62-
hashingMethod: 'sha256',
63+
devAllowUnsafe: false,
6364
enabled: true
65+
hashingMethod: 'sha256',
6466
}
6567
```
6668

@@ -72,8 +74,9 @@ new CspHtmlWebpackPlugin({
7274
'script-src': ["'unsafe-inline'", "'self'", "'unsafe-eval'"],
7375
'style-src': ["'unsafe-inline'", "'self'", "'unsafe-eval'"]
7476
}, {
75-
hashingMethod: 'sha256',
77+
devAllowUnsafe: false,
7678
enabled: true
79+
hashingMethod: 'sha256',
7780
})
7881
```
7982

plugin.js

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const defaultPolicy = {
1414
};
1515

1616
const defaultAdditionalOpts = {
17+
devAllowUnsafe: false,
1718
enabled: true,
1819
hashingMethod: 'sha256'
1920
};
@@ -76,6 +77,32 @@ class CspHtmlWebpackPlugin {
7677
return `'${this.opts.hashingMethod}-${hashed}'`;
7778
}
7879

80+
/**
81+
* Helper function to return the correct policy depending on whether the dev has allowed unsafe eval/inline or not
82+
* @param {object} $ - the Cheerio instance
83+
* @param {string} policyName - one of 'script-src' and 'style-src'
84+
* @param {string} selector - a Cheerio selector string for getting the hashable elements for this policy
85+
* @param {object} policyObj - the working CSP policy object
86+
* @param {object} userPolicyObj - the sanitized CSP policy object provided by the user
87+
* @return {object} the new policy for `policyName`
88+
*/
89+
createPolicyObj($, policyName, selector, policyObj, userPolicyObj) {
90+
// Wrapped in flatten([]) to handle both when policy is a string and an array
91+
const flattenedUserPolicy = flatten(userPolicyObj[policyName]);
92+
if (
93+
this.opts.devAllowUnsafe === true &&
94+
(flattenedUserPolicy.includes("'unsafe-inline'") ||
95+
flattenedUserPolicy.includes("'unsafe-eval'"))
96+
) {
97+
return userPolicyObj[policyName];
98+
}
99+
100+
const hashes = $(selector)
101+
.map((i, element) => this.hash($(element).html()))
102+
.get();
103+
return flatten([policyObj[policyName]]).concat(hashes);
104+
}
105+
79106
/**
80107
* Builds the CSP policy by flattening arrays into strings and appending all policies into a single string
81108
* @param policyObj
@@ -154,28 +181,6 @@ class CspHtmlWebpackPlugin {
154181
return compileCb(null, htmlPluginData);
155182
}
156183

157-
/**
158-
* Helper function for transforming script-src and style-src policies.
159-
* @param {object} $ - the Cheerio instance
160-
* @param {string} policyName - one of 'script-src' and 'style-src'
161-
* @param {string} selector - a Cheerio selector string for getting the hashable elements for this policy
162-
* @param {object} policyObj - the working CSP policy object
163-
* @param {object} userPolicyObj - the sanitized CSP policy object provided by the user
164-
* @return {object} the new policy for `policyName`
165-
*/
166-
createPolicyObj($, policyName, selector, policyObj, userPolicyObj) {
167-
// Wrapped in flatten([]) to handle both when policy is a string and an array
168-
const flattenedUserPolicy = flatten(userPolicyObj[policyName]);
169-
if (flattenedUserPolicy.includes("'unsafe-inline'")) {
170-
return userPolicyObj[policyName];
171-
}
172-
173-
const hashes = $(selector)
174-
.map((i, element) => this.hash($(element).html()))
175-
.get();
176-
return flatten([policyObj[policyName]]).concat(hashes);
177-
}
178-
179184
/**
180185
* Hooks into webpack to collect assets and hash them, build the policy, and add it into our HTML template
181186
* @param compiler

spec/plugin.spec.js

Lines changed: 119 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -177,8 +177,8 @@ describe('CspHtmlWebpackPlugin', () => {
177177
);
178178
});
179179

180-
describe("when the user-specified script-src policy contains 'unsafe-inline'", () => {
181-
it('skips the hashing of the scripts it finds', done => {
180+
describe("when the user-specified policy contains 'unsafe-inline' or 'unsafe-eval'", () => {
181+
it('skips the hashing of the scripts it finds if devAllowUnsafe is true', done => {
182182
const webpackConfig = {
183183
entry: path.join(__dirname, 'fixtures/index.js'),
184184
output: {
@@ -192,12 +192,17 @@ describe('CspHtmlWebpackPlugin', () => {
192192
template: path.join(__dirname, 'fixtures', 'with-js.html'),
193193
inject: 'body'
194194
}),
195-
new CspHtmlWebpackPlugin({
196-
'base-uri': ["'self'", 'https://slack.com'],
197-
'font-src': ["'self'", "'https://a-slack-edge.com'"],
198-
'script-src': ["'self'", "'unsafe-inline'"],
199-
'style-src': ["'self'"]
200-
})
195+
new CspHtmlWebpackPlugin(
196+
{
197+
'base-uri': ["'self'", 'https://slack.com'],
198+
'font-src': ["'self'", "'https://a-slack-edge.com'"],
199+
'script-src': ["'self'", "'unsafe-inline'"],
200+
'style-src': ["'self'"]
201+
},
202+
{
203+
devAllowUnsafe: true
204+
}
205+
)
201206
]
202207
};
203208

@@ -219,10 +224,55 @@ describe('CspHtmlWebpackPlugin', () => {
219224
done
220225
);
221226
});
222-
});
223227

224-
describe("when the user-specified style-src policy contains 'unsafe-inline'", () => {
225-
it('skips the hashing of the styles it finds', done => {
228+
it('continues hashing scripts if unsafe-inline/unsafe-eval is included, but devAllowUnsafe is false', done => {
229+
const webpackConfig = {
230+
entry: path.join(__dirname, 'fixtures/index.js'),
231+
output: {
232+
path: OUTPUT_DIR,
233+
filename: 'index.bundle.js'
234+
},
235+
mode: 'none',
236+
plugins: [
237+
new HtmlWebpackPlugin({
238+
filename: path.join(OUTPUT_DIR, 'index.html'),
239+
template: path.join(__dirname, 'fixtures', 'with-js.html'),
240+
inject: 'body'
241+
}),
242+
new CspHtmlWebpackPlugin(
243+
{
244+
'base-uri': ["'self'", 'https://slack.com'],
245+
'font-src': ["'self'", "'https://a-slack-edge.com'"],
246+
'script-src': ["'self'", "'unsafe-inline'"],
247+
'style-src': ["'self'"]
248+
},
249+
{
250+
devAllowUnsafe: false
251+
}
252+
)
253+
]
254+
};
255+
256+
testCspHtmlWebpackPlugin(
257+
webpackConfig,
258+
'index.html',
259+
(cspPolicy, _, doneFn) => {
260+
const expected =
261+
"base-uri 'self' https://slack.com;" +
262+
" object-src 'none';" +
263+
" script-src 'self' 'unsafe-inline' 'sha256-9nPWXYBnlIeJ9HmieIATDv9Ab5plt35XZiT48TfEkJI=';" +
264+
" style-src 'self';" +
265+
" font-src 'self' 'https://a-slack-edge.com'";
266+
267+
expect(cspPolicy).toEqual(expected);
268+
269+
doneFn();
270+
},
271+
done
272+
);
273+
});
274+
275+
it('skips the hashing of the styles it finds if devAllowUnsafe is true', done => {
226276
const webpackConfig = {
227277
entry: path.join(__dirname, 'fixtures/index.js'),
228278
output: {
@@ -236,12 +286,17 @@ describe('CspHtmlWebpackPlugin', () => {
236286
template: path.join(__dirname, 'fixtures', 'with-css.html'),
237287
inject: 'body'
238288
}),
239-
new CspHtmlWebpackPlugin({
240-
'base-uri': ["'self'", 'https://slack.com'],
241-
'font-src': ["'self'", "'https://a-slack-edge.com'"],
242-
'script-src': ["'self'"],
243-
'style-src': ["'self'", "'unsafe-inline'"]
244-
})
289+
new CspHtmlWebpackPlugin(
290+
{
291+
'base-uri': ["'self'", 'https://slack.com'],
292+
'font-src': ["'self'", "'https://a-slack-edge.com'"],
293+
'script-src': ["'self'"],
294+
'style-src': ["'self'", "'unsafe-inline'"]
295+
},
296+
{
297+
devAllowUnsafe: true
298+
}
299+
)
245300
]
246301
};
247302

@@ -263,6 +318,53 @@ describe('CspHtmlWebpackPlugin', () => {
263318
done
264319
);
265320
});
321+
322+
it('continues hashing scripts if unsafe-inline/unsafe-eval is included, but devAllowUnsafe is false', done => {
323+
const webpackConfig = {
324+
entry: path.join(__dirname, 'fixtures/index.js'),
325+
output: {
326+
path: OUTPUT_DIR,
327+
filename: 'index.bundle.js'
328+
},
329+
mode: 'none',
330+
plugins: [
331+
new HtmlWebpackPlugin({
332+
filename: path.join(OUTPUT_DIR, 'index.html'),
333+
template: path.join(__dirname, 'fixtures', 'with-css.html'),
334+
inject: 'body'
335+
}),
336+
new CspHtmlWebpackPlugin(
337+
{
338+
'base-uri': ["'self'", 'https://slack.com'],
339+
'font-src': ["'self'", "'https://a-slack-edge.com'"],
340+
'script-src': ["'self'"],
341+
'style-src': ["'self'", "'unsafe-inline'"]
342+
},
343+
{
344+
devAllowUnsafe: false
345+
}
346+
)
347+
]
348+
};
349+
350+
testCspHtmlWebpackPlugin(
351+
webpackConfig,
352+
'index.html',
353+
(cspPolicy, _, doneFn) => {
354+
const expected =
355+
"base-uri 'self' https://slack.com;" +
356+
" object-src 'none';" +
357+
" script-src 'self';" +
358+
" style-src 'self' 'unsafe-inline' 'sha256-MqG77yUiqBo4MMVZAl09WSafnQY4Uu3cSdZPKxaf9sQ=';" +
359+
" font-src 'self' 'https://a-slack-edge.com'";
360+
361+
expect(cspPolicy).toEqual(expected);
362+
363+
doneFn();
364+
},
365+
done
366+
);
367+
});
266368
});
267369

268370
it('removes the empty Content Security Policy meta tag if enabled is the bool false', done => {

0 commit comments

Comments
 (0)