11const path = require ( 'path' ) ;
2+ const crypto = require ( 'crypto' ) ;
23const HtmlWebpackPlugin = require ( 'html-webpack-plugin' ) ;
34const {
45 WEBPACK_OUTPUT_DIR ,
89const CspHtmlWebpackPlugin = require ( './plugin' ) ;
910
1011describe ( 'CspHtmlWebpackPlugin' , ( ) => {
12+ beforeEach ( ( ) => {
13+ jest
14+ . spyOn ( crypto , 'randomBytes' )
15+ . mockImplementationOnce ( ( ) => 'mockedbase64string-1' )
16+ . mockImplementationOnce ( ( ) => 'mockedbase64string-2' )
17+ . mockImplementationOnce ( ( ) => 'mockedbase64string-3' )
18+ . mockImplementation (
19+ ( ) => new Error ( 'Need to add more crypto.randomBytes mocks' )
20+ ) ;
21+ } ) ;
22+
23+ afterEach ( ( ) => {
24+ crypto . randomBytes . mockReset ( ) ;
25+ } ) ;
26+
1127 describe ( 'Error checking' , ( ) => {
1228 it ( 'throws an error if an invalid hashing method is used' , ( ) => {
1329 expect ( ( ) => {
@@ -20,10 +36,85 @@ describe('CspHtmlWebpackPlugin', () => {
2036 ) ;
2137 } ) . toThrow ( new Error ( `'invalid' is not a valid hashing method` ) ) ;
2238 } ) ;
39+
40+ describe ( 'validatePolicy' , ( ) => {
41+ [
42+ 'self' ,
43+ 'unsafe-inline' ,
44+ 'unsafe-eval' ,
45+ 'none' ,
46+ 'strict-dynamic' ,
47+ 'report-sample'
48+ ] . forEach ( source => {
49+ it ( `throws an error if '${ source } ' is not wrapped in apostrophes in an array defined policy` , done => {
50+ const config = createWebpackConfig ( [
51+ new HtmlWebpackPlugin ( {
52+ filename : path . join ( WEBPACK_OUTPUT_DIR , 'index.html' ) ,
53+ template : path . join (
54+ __dirname ,
55+ 'test-utils' ,
56+ 'fixtures' ,
57+ 'with-nothing.html'
58+ )
59+ } ) ,
60+ new CspHtmlWebpackPlugin ( {
61+ 'script-src' : [ source ]
62+ } )
63+ ] ) ;
64+
65+ webpackCompile (
66+ config ,
67+ ( _1 , _2 , _3 , errors ) => {
68+ expect ( errors [ 0 ] ) . toEqual (
69+ new Error (
70+ `CSP: policy for script-src contains ${ source } which should be wrapped in apostrophes`
71+ )
72+ ) ;
73+ done ( ) ;
74+ } ,
75+ {
76+ expectError : true
77+ }
78+ ) ;
79+ } ) ;
80+
81+ it ( `throws an error if '${ source } ' is not wrapped in apostrophes in a string defined policy` , done => {
82+ const config = createWebpackConfig ( [
83+ new HtmlWebpackPlugin ( {
84+ filename : path . join ( WEBPACK_OUTPUT_DIR , 'index.html' ) ,
85+ template : path . join (
86+ __dirname ,
87+ 'test-utils' ,
88+ 'fixtures' ,
89+ 'with-nothing.html'
90+ )
91+ } ) ,
92+ new CspHtmlWebpackPlugin ( {
93+ 'script-src' : source
94+ } )
95+ ] ) ;
96+
97+ webpackCompile (
98+ config ,
99+ ( _1 , _2 , _3 , errors ) => {
100+ expect ( errors [ 0 ] ) . toEqual (
101+ new Error (
102+ `CSP: policy for script-src contains ${ source } which should be wrapped in apostrophes`
103+ )
104+ ) ;
105+ done ( ) ;
106+ } ,
107+ {
108+ expectError : true
109+ }
110+ ) ;
111+ } ) ;
112+ } ) ;
113+ } ) ;
23114 } ) ;
24115
25- describe ( 'Adding sha checksums' , ( ) => {
26- it ( 'inserts the default policy, including sha-256 hashes of other inline scripts and styles found' , done => {
116+ describe ( 'Adding sha and nonce checksums' , ( ) => {
117+ it ( 'inserts the default policy, including sha-256 hashes of other inline scripts and styles found, and nonce hashes of external scripts found ' , done => {
27118 const config = createWebpackConfig ( [
28119 new HtmlWebpackPlugin ( {
29120 filename : path . join ( WEBPACK_OUTPUT_DIR , 'index.html' ) ,
@@ -41,8 +132,8 @@ describe('CspHtmlWebpackPlugin', () => {
41132 const expected =
42133 "base-uri 'self';" +
43134 " object-src 'none';" +
44- " script-src 'unsafe-inline' 'self' 'unsafe-eval' 'sha256-ixjZMYNfWQWawUHioWOx2jBsTmfxucX7IlwsMt2jWvc=';" +
45- " style-src 'unsafe-inline' 'self' 'unsafe-eval' 'sha256-MqG77yUiqBo4MMVZAl09WSafnQY4Uu3cSdZPKxaf9sQ='" ;
135+ " script-src 'unsafe-inline' 'self' 'unsafe-eval' 'sha256-ixjZMYNfWQWawUHioWOx2jBsTmfxucX7IlwsMt2jWvc=' 'nonce-mockedbase64string-1' 'nonce-mockedbase64string-2' ;" +
136+ " style-src 'unsafe-inline' 'self' 'unsafe-eval' 'sha256-MqG77yUiqBo4MMVZAl09WSafnQY4Uu3cSdZPKxaf9sQ=' 'nonce-mockedbase64string-3' " ;
46137
47138 expect ( csps [ 'index.html' ] ) . toEqual ( expected ) ;
48139 done ( ) ;
@@ -73,7 +164,7 @@ describe('CspHtmlWebpackPlugin', () => {
73164 const expected =
74165 "base-uri 'self' https://slack.com;" +
75166 " object-src 'none';" +
76- " script-src 'self';" +
167+ " script-src 'self' 'nonce-mockedbase64string-1' ;" +
77168 " style-src 'self';" +
78169 " font-src 'self' 'https://a-slack-edge.com';" +
79170 " connect-src 'self'" ;
@@ -83,7 +174,7 @@ describe('CspHtmlWebpackPlugin', () => {
83174 } ) ;
84175 } ) ;
85176
86- it ( 'handles string values for policies where the hash is appended' , done => {
177+ it ( 'handles string values for policies where hashes and nonces are appended' , done => {
87178 const config = createWebpackConfig ( [
88179 new HtmlWebpackPlugin ( {
89180 filename : path . join ( WEBPACK_OUTPUT_DIR , 'index.html' ) ,
@@ -104,14 +195,116 @@ describe('CspHtmlWebpackPlugin', () => {
104195 const expected =
105196 "base-uri 'self';" +
106197 " object-src 'none';" +
107- " script-src 'self' 'sha256-ixjZMYNfWQWawUHioWOx2jBsTmfxucX7IlwsMt2jWvc=';" +
108- " style-src 'self' 'sha256-MqG77yUiqBo4MMVZAl09WSafnQY4Uu3cSdZPKxaf9sQ='" ;
198+ " script-src 'self' 'sha256-ixjZMYNfWQWawUHioWOx2jBsTmfxucX7IlwsMt2jWvc=' 'nonce-mockedbase64string-1' 'nonce-mockedbase64string-2' ;" +
199+ " style-src 'self' 'sha256-MqG77yUiqBo4MMVZAl09WSafnQY4Uu3cSdZPKxaf9sQ=' 'nonce-mockedbase64string-3' " ;
109200
110201 expect ( csps [ 'index.html' ] ) . toEqual ( expected ) ;
111202 done ( ) ;
112203 } ) ;
113204 } ) ;
114205
206+ it ( "doesn't add nonces for scripts / styles generated where their host has already been defined in the CSP, and 'strict-dynamic' doesn't exist in the policy" , done => {
207+ const config = createWebpackConfig (
208+ [
209+ new HtmlWebpackPlugin ( {
210+ filename : path . join ( WEBPACK_OUTPUT_DIR , 'index.html' ) ,
211+ template : path . join (
212+ __dirname ,
213+ 'test-utils' ,
214+ 'fixtures' ,
215+ 'with-script-and-style.html'
216+ )
217+ } ) ,
218+ new CspHtmlWebpackPlugin ( {
219+ 'script-src' : [ "'self'" , 'https://my.cdn.com' ] ,
220+ 'style-src' : [ "'self'" ]
221+ } )
222+ ] ,
223+ 'https://my.cdn.com/'
224+ ) ;
225+
226+ webpackCompile ( config , ( csps , selectors ) => {
227+ const $ = selectors [ 'index.html' ] ;
228+ const expected =
229+ "base-uri 'self';" +
230+ " object-src 'none';" +
231+ " script-src 'self' https://my.cdn.com 'sha256-ixjZMYNfWQWawUHioWOx2jBsTmfxucX7IlwsMt2jWvc=' 'nonce-mockedbase64string-1';" +
232+ " style-src 'self' 'sha256-MqG77yUiqBo4MMVZAl09WSafnQY4Uu3cSdZPKxaf9sQ=' 'nonce-mockedbase64string-2'" ;
233+
234+ // csp should be defined properly
235+ expect ( csps [ 'index.html' ] ) . toEqual ( expected ) ;
236+
237+ // script with host not defined should have nonce defined, and correct
238+ expect ( $ ( 'script' ) [ 0 ] . attribs . src ) . toEqual (
239+ 'https://example.com/example.js'
240+ ) ;
241+ expect ( $ ( 'script' ) [ 0 ] . attribs . nonce ) . toEqual ( 'mockedbase64string-1' ) ;
242+
243+ // inline script, so no nonce
244+ expect ( $ ( 'script' ) [ 1 ] . attribs ) . toEqual ( { } ) ;
245+
246+ // script with host defined should not have a nonce
247+ expect ( $ ( 'script' ) [ 2 ] . attribs . src ) . toEqual (
248+ 'https://my.cdn.com/index.bundle.js'
249+ ) ;
250+ expect ( Object . keys ( $ ( 'script' ) [ 2 ] . attribs ) ) . not . toContain ( 'nonce' ) ;
251+
252+ done ( ) ;
253+ } ) ;
254+ } ) ;
255+
256+ it ( "continues to add nonces to scripts / styles even if the host has already been whitelisted due to 'strict-dynamic' existing in the policy" , done => {
257+ const config = createWebpackConfig (
258+ [
259+ new HtmlWebpackPlugin ( {
260+ filename : path . join ( WEBPACK_OUTPUT_DIR , 'index.html' ) ,
261+ template : path . join (
262+ __dirname ,
263+ 'test-utils' ,
264+ 'fixtures' ,
265+ 'with-script-and-style.html'
266+ )
267+ } ) ,
268+ new CspHtmlWebpackPlugin ( {
269+ 'script-src' : [ "'self'" , "'strict-dynamic'" , 'https://my.cdn.com' ] ,
270+ 'style-src' : [ "'self'" ]
271+ } )
272+ ] ,
273+ 'https://my.cdn.com/'
274+ ) ;
275+
276+ webpackCompile ( config , ( csps , selectors ) => {
277+ const $ = selectors [ 'index.html' ] ;
278+
279+ // 'strict-dynamic' should be at the end of the script-src here
280+ const expected =
281+ "base-uri 'self';" +
282+ " object-src 'none';" +
283+ " script-src 'self' https://my.cdn.com 'sha256-ixjZMYNfWQWawUHioWOx2jBsTmfxucX7IlwsMt2jWvc=' 'nonce-mockedbase64string-1' 'nonce-mockedbase64string-2' 'strict-dynamic';" +
284+ " style-src 'self' 'sha256-MqG77yUiqBo4MMVZAl09WSafnQY4Uu3cSdZPKxaf9sQ=' 'nonce-mockedbase64string-3'" ;
285+
286+ // csp should be defined properly
287+ expect ( csps [ 'index.html' ] ) . toEqual ( expected ) ;
288+
289+ // script with host not defined should have nonce defined, and correct
290+ expect ( $ ( 'script' ) [ 0 ] . attribs . src ) . toEqual (
291+ 'https://example.com/example.js'
292+ ) ;
293+ expect ( $ ( 'script' ) [ 0 ] . attribs . nonce ) . toEqual ( 'mockedbase64string-1' ) ;
294+
295+ // inline script, so no nonce
296+ expect ( $ ( 'script' ) [ 1 ] . attribs ) . toEqual ( { } ) ;
297+
298+ // script with host defined should also have a nonce
299+ expect ( $ ( 'script' ) [ 2 ] . attribs . src ) . toEqual (
300+ 'https://my.cdn.com/index.bundle.js'
301+ ) ;
302+ expect ( $ ( 'script' ) [ 2 ] . attribs . nonce ) . toEqual ( 'mockedbase64string-2' ) ;
303+
304+ done ( ) ;
305+ } ) ;
306+ } ) ;
307+
115308 describe ( 'HtmlWebpackPlugin defined policy' , ( ) => {
116309 it ( 'inserts a custom policy from a specific HtmlWebpackPlugin instance, if one is defined' , done => {
117310 const config = createWebpackConfig ( [
@@ -140,7 +333,7 @@ describe('CspHtmlWebpackPlugin', () => {
140333 const expected =
141334 "base-uri 'self' https://slack.com;" +
142335 " object-src 'none';" +
143- " script-src 'self';" +
336+ " script-src 'self' 'nonce-mockedbase64string-1' ;" +
144337 " style-src 'self';" +
145338 " font-src 'self' 'https://a-slack-edge.com';" +
146339 " connect-src 'self'" ;
@@ -179,7 +372,7 @@ describe('CspHtmlWebpackPlugin', () => {
179372 const expected =
180373 "base-uri 'self' https://slack.com;" + // this should be included as it's not defined in the HtmlWebpackPlugin instance
181374 " object-src 'none';" + // this comes from the default policy
182- " script-src 'unsafe-inline' 'self' 'unsafe-eval';" + // this comes from the default policy
375+ " script-src 'unsafe-inline' 'self' 'unsafe-eval' 'nonce-mockedbase64string-1' ;" + // this comes from the default policy
183376 " style-src 'unsafe-inline' 'self' 'unsafe-eval';" + // this comes from the default policy
184377 " font-src 'https://a-slack-edge.com' 'https://b-slack-edge.com'" ; // this should only include the HtmlWebpackPlugin instance policy
185378
@@ -221,13 +414,13 @@ describe('CspHtmlWebpackPlugin', () => {
221414 const expectedCustom =
222415 "base-uri 'self';" +
223416 " object-src 'none';" +
224- " script-src 'https://a-slack-edge.com';" +
417+ " script-src 'https://a-slack-edge.com' 'nonce-mockedbase64string-1' ;" +
225418 " style-src 'https://b-slack-edge.com'" ;
226419
227420 const expectedDefault =
228421 "base-uri 'self';" +
229422 " object-src 'none';" +
230- " script-src 'unsafe-inline' 'self' 'unsafe-eval';" +
423+ " script-src 'unsafe-inline' 'self' 'unsafe-eval' 'nonce-mockedbase64string-2' ;" +
231424 " style-src 'unsafe-inline' 'self' 'unsafe-eval'" ;
232425
233426 expect ( csps [ 'index-csp.html' ] ) . toEqual ( expectedCustom ) ;
@@ -238,7 +431,7 @@ describe('CspHtmlWebpackPlugin', () => {
238431 } ) ;
239432
240433 describe ( 'unsafe-inline / unsafe-eval' , ( ) => {
241- it ( 'skips the hashing of the scripts and styles it finds if devAllowUnsafe is true' , done => {
434+ it ( 'skips the hashing / nonceing of the scripts and styles it finds if devAllowUnsafe is true' , done => {
242435 const config = createWebpackConfig ( [
243436 new HtmlWebpackPlugin ( {
244437 filename : path . join ( WEBPACK_OUTPUT_DIR , 'index.html' ) ,
@@ -275,7 +468,7 @@ describe('CspHtmlWebpackPlugin', () => {
275468 } ) ;
276469 } ) ;
277470
278- it ( 'continues hashing scripts and styles if unsafe-inline/unsafe-eval is included, but devAllowUnsafe is false' , done => {
471+ it ( 'continues hashing / nonceing scripts and styles if unsafe-inline/unsafe-eval is included, but devAllowUnsafe is false' , done => {
279472 const config = createWebpackConfig ( [
280473 new HtmlWebpackPlugin ( {
281474 filename : path . join ( WEBPACK_OUTPUT_DIR , 'index.html' ) ,
@@ -303,8 +496,8 @@ describe('CspHtmlWebpackPlugin', () => {
303496 const expected =
304497 "base-uri 'self' https://slack.com;" +
305498 " object-src 'none';" +
306- " script-src 'self' 'unsafe-inline' 'sha256-ixjZMYNfWQWawUHioWOx2jBsTmfxucX7IlwsMt2jWvc=';" +
307- " style-src 'self' 'unsafe-eval' 'sha256-MqG77yUiqBo4MMVZAl09WSafnQY4Uu3cSdZPKxaf9sQ=';" +
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' ;" +
308501 " font-src 'self' 'https://a-slack-edge.com'" ;
309502
310503 expect ( csps [ 'index.html' ] ) . toEqual ( expected ) ;
@@ -446,7 +639,7 @@ describe('CspHtmlWebpackPlugin', () => {
446639 const expected =
447640 "base-uri 'self';" +
448641 " object-src 'none';" +
449- " script-src 'unsafe-inline' 'self' 'unsafe-eval';" +
642+ " script-src 'unsafe-inline' 'self' 'unsafe-eval' 'nonce-mockedbase64string-1' ;" +
450643 " style-src 'unsafe-inline' 'self' 'unsafe-eval'" ;
451644
452645 expect ( csps [ 'index.html' ] ) . toEqual ( expected ) ;
@@ -472,7 +665,7 @@ describe('CspHtmlWebpackPlugin', () => {
472665 const expected =
473666 "base-uri 'self';" +
474667 " object-src 'none';" +
475- " script-src 'unsafe-inline' 'self' 'unsafe-eval';" +
668+ " script-src 'unsafe-inline' 'self' 'unsafe-eval' 'nonce-mockedbase64string-1' ;" +
476669 " style-src 'unsafe-inline' 'self' 'unsafe-eval'" ;
477670
478671 expect ( csps [ 'index.html' ] ) . toEqual ( expected ) ;
@@ -492,7 +685,7 @@ describe('CspHtmlWebpackPlugin', () => {
492685 const expected =
493686 "base-uri 'self';" +
494687 " object-src 'none';" +
495- " script-src 'unsafe-inline' 'self' 'unsafe-eval';" +
688+ " script-src 'unsafe-inline' 'self' 'unsafe-eval' 'nonce-mockedbase64string-1' ;" +
496689 " style-src 'unsafe-inline' 'self' 'unsafe-eval'" ;
497690
498691 expect ( csps [ 'index.html' ] ) . toEqual ( expected ) ;
0 commit comments