Skip to content

Commit 67426d5

Browse files
authored
Adding ability to overwrite the default processFn method with a custom one. (#58)
* Allowing developer to define a custom processFn New option in additionalOpts to allow the developer to use the built CSP in any way they want to. `processFn` can either be defined on the CspHtmlWebpackPlugin, or in individual HtmlWebpackPlugin instances * Updating readme to reflect new option, and making it more clear that the full configuration is not needed to enable this plugin
1 parent f05fd49 commit 67426d5

File tree

3 files changed

+157
-31
lines changed

3 files changed

+157
-31
lines changed

README.md

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ This `CspHtmlWebpackPlugin` accepts 2 params with the following structure:
4141
- `{string}` hashingMethod - accepts 'sha256', 'sha384', 'sha512' - your node version must also accept this hashing method.
4242
- `{object}` hashEnabled - a `<string, boolean>` entry for which policy rules are allowed to include hashes
4343
- `{object}` nonceEnabled - a `<string, boolean>` entry for which policy rules are allowed to include nonces
44+
- `{Function}` processFn - allows the developer to overwrite the default method of what happens to the CSP after it has been created
45+
- Parameters are:
46+
- `builtPolicy`: a `string` containing the completed policy;
47+
- `htmlPluginData`: the `HtmlWebpackPlugin` `object`;
48+
- `$`: the `cheerio` object of the html file currently being processed
4449

4550
The plugin also adds a new config option onto each `HtmlWebpackPlugin` instance:
4651

@@ -49,6 +54,13 @@ The plugin also adds a new config option onto each `HtmlWebpackPlugin` instance:
4954
- `{object}` policy - A custom policy which should be applied only to this instance of the HtmlWebpackPlugin
5055
- `{object}` hashEnabled - a `<string, boolean>` entry for which policy rules are allowed to include hashes
5156
- `{object}` nonceEnabled - a `<string, boolean>` entry for which policy rules are allowed to include nonces
57+
- `{Function}` processFn - allows the developer to overwrite the default method of what happens to the CSP after it has been created
58+
- Parameters are:
59+
- `builtPolicy`: a `string` containing the completed policy;
60+
- `htmlPluginData`: the `HtmlWebpackPlugin` `object`;
61+
- `$`: the `cheerio` object of the html file currently being processed
62+
63+
#### Order of Precedence:
5264

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

@@ -60,6 +72,9 @@ Note that policies and `hashEnabled` / `nonceEnabled` are merged in the followin
6072

6173
If 2 policies have the same key/policy rule, the former policy will override the latter policy. Entries in a specific rule will not be merged; they will be replaced.
6274

75+
This is useful if you need different policy rules / processing functions for different `HtmlWebpackPlugin` instances
76+
in the same webpack config.
77+
6378
#### Default Policy:
6479

6580
```
@@ -84,12 +99,16 @@ If 2 policies have the same key/policy rule, the former policy will override the
8499
nonceEnabled: {
85100
'script-src': true,
86101
'style-src': true
87-
}
102+
},
103+
processFn: defaultProcessFn
88104
}
89105
```
90106

91107
#### Full Configuration with all options:
92108

109+
Note that you don't have to include the same section in both `HtmlWebpackPlugin` and `CspHtmlWebpackPlugin`.
110+
See the [Order of Precedence](#order-of-precedence) section above.
111+
93112
```
94113
new HtmlWebpackPlugin({
95114
cspPlugin: {
@@ -107,7 +126,8 @@ new HtmlWebpackPlugin({
107126
nonceEnabled: {
108127
'script-src': true,
109128
'style-src': true
110-
}
129+
},
130+
processFn: defaultProcessFn
111131
}
112132
});
113133
@@ -126,7 +146,8 @@ new CspHtmlWebpackPlugin({
126146
nonceEnabled: {
127147
'script-src': true,
128148
'style-src': true
129-
}
149+
},
150+
processFn: defaultProcessFn
130151
})
131152
```
132153

plugin.jest.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -852,4 +852,91 @@ describe('CspHtmlWebpackPlugin', () => {
852852
});
853853
});
854854
});
855+
856+
describe('Custom process function', () => {
857+
it('Allows the process function to be overwritten', done => {
858+
const processFn = jest.fn();
859+
const builtPolicy = `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'`;
860+
861+
const config = createWebpackConfig([
862+
new HtmlWebpackPlugin({
863+
filename: path.join(WEBPACK_OUTPUT_DIR, 'index.html'),
864+
template: path.join(
865+
__dirname,
866+
'test-utils',
867+
'fixtures',
868+
'with-script-and-style.html'
869+
)
870+
}),
871+
new CspHtmlWebpackPlugin(
872+
{},
873+
{
874+
processFn
875+
}
876+
)
877+
]);
878+
879+
webpackCompile(config, csps => {
880+
// we've overwritten the default processFn, which writes the policy into the html file
881+
// so it won't exist in this object anymore.
882+
expect(csps['index.html']).toBeUndefined();
883+
884+
// The processFn should receive the built policy as it's first arg
885+
expect(processFn).toHaveBeenCalledWith(
886+
builtPolicy,
887+
expect.anything(),
888+
expect.anything()
889+
);
890+
891+
done();
892+
});
893+
});
894+
895+
it('only overwrites the processFn for the HtmlWebpackInstance where it has been defined', done => {
896+
const processFn = jest.fn();
897+
const index1BuiltPolicy = `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'`;
898+
const index2BuiltPolicy = `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'`;
899+
900+
const config = createWebpackConfig([
901+
new HtmlWebpackPlugin({
902+
filename: path.join(WEBPACK_OUTPUT_DIR, 'index-1.html'),
903+
template: path.join(
904+
__dirname,
905+
'test-utils',
906+
'fixtures',
907+
'with-script-and-style.html'
908+
),
909+
cspPlugin: {
910+
processFn
911+
}
912+
}),
913+
new HtmlWebpackPlugin({
914+
filename: path.join(WEBPACK_OUTPUT_DIR, 'index-2.html'),
915+
template: path.join(
916+
__dirname,
917+
'test-utils',
918+
'fixtures',
919+
'with-script-and-style.html'
920+
)
921+
}),
922+
new CspHtmlWebpackPlugin()
923+
]);
924+
925+
webpackCompile(config, csps => {
926+
// it won't exist in the html file since we overwrote processFn
927+
expect(csps['index-1.html']).toBeUndefined();
928+
// processFn wasn't overwritten here, so this should be added to the html file as normal
929+
expect(csps['index-2.html']).toEqual(index2BuiltPolicy);
930+
931+
// index-1.html should have used our custom function defined
932+
expect(processFn).toHaveBeenCalledWith(
933+
index1BuiltPolicy,
934+
expect.anything(),
935+
expect.anything()
936+
);
937+
938+
done();
939+
});
940+
});
941+
});
855942
});

plugin.js

Lines changed: 46 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,31 @@ try {
1818
}
1919
}
2020

21+
/**
22+
* The default function for adding the CSP to the head of a document
23+
* Can be overwritten to allow the developer to process the CSP in their own way
24+
* @param {string} builtPolicy
25+
* @param {object} htmlPluginData
26+
* @param {object} $
27+
*/
28+
const defaultProcessFn = (builtPolicy, htmlPluginData, $) => {
29+
let metaTag = $('meta[http-equiv="Content-Security-Policy"]');
30+
31+
// Add element if it doesn't exist.
32+
if (!metaTag.length) {
33+
metaTag = cheerio.load('<meta http-equiv="Content-Security-Policy">')(
34+
'meta'
35+
);
36+
metaTag.prependTo($('head'));
37+
}
38+
39+
// build the policy into the context attr of the csp meta tag
40+
metaTag.attr('content', builtPolicy);
41+
42+
// eslint-disable-next-line no-param-reassign
43+
htmlPluginData.html = $.html();
44+
};
45+
2146
const defaultPolicy = {
2247
'base-uri': "'self'",
2348
'object-src': "'none'",
@@ -35,7 +60,8 @@ const defaultAdditionalOpts = {
3560
nonceEnabled: {
3661
'script-src': true,
3762
'style-src': true
38-
}
63+
},
64+
processFn: defaultProcessFn
3965
};
4066

4167
class CspHtmlWebpackPlugin {
@@ -93,6 +119,13 @@ class CspHtmlWebpackPlugin {
93119
...get(htmlPluginData, 'plugin.options.cspPlugin.nonceEnabled', {})
94120
});
95121

122+
// 3. Get the processFn for this HtmlWebpackPlugin instance.
123+
this.processFn = get(
124+
htmlPluginData,
125+
'plugin.options.cspPlugin.processFn',
126+
this.opts.processFn || defaultProcessFn
127+
);
128+
96129
return compileCb(null, htmlPluginData);
97130
}
98131

@@ -285,16 +318,6 @@ class CspHtmlWebpackPlugin {
285318
return compileCb(null, htmlPluginData);
286319
}
287320

288-
let metaTag = $('meta[http-equiv="Content-Security-Policy"]');
289-
290-
// Add element if it doesn't exist.
291-
if (!metaTag.length) {
292-
metaTag = cheerio.load('<meta http-equiv="Content-Security-Policy">')(
293-
'meta'
294-
);
295-
metaTag.prependTo($('head'));
296-
}
297-
298321
// get all nonces for script and style tags
299322
const scriptNonce = this.setNonce($, 'script-src', 'script[src]');
300323
const styleNonce = this.setNonce($, 'style-src', 'link[rel="stylesheet"]');
@@ -303,24 +326,19 @@ class CspHtmlWebpackPlugin {
303326
const scriptShas = this.getShas($, 'script-src', 'script:not([src])');
304327
const styleShas = this.getShas($, 'style-src', 'style:not([href])');
305328

306-
// build the policy into the context attr of the csp meta tag
307-
metaTag.attr(
308-
'content',
309-
this.buildPolicy({
310-
...this.policy,
311-
'script-src': flatten([this.policy['script-src']]).concat(
312-
scriptShas,
313-
scriptNonce
314-
),
315-
'style-src': flatten([this.policy['style-src']]).concat(
316-
styleShas,
317-
styleNonce
318-
)
319-
})
320-
);
329+
const builtPolicy = this.buildPolicy({
330+
...this.policy,
331+
'script-src': flatten([this.policy['script-src']]).concat(
332+
scriptShas,
333+
scriptNonce
334+
),
335+
'style-src': flatten([this.policy['style-src']]).concat(
336+
styleShas,
337+
styleNonce
338+
)
339+
});
321340

322-
// eslint-disable-next-line no-param-reassign
323-
htmlPluginData.html = $.html();
341+
this.processFn(builtPolicy, htmlPluginData, $);
324342

325343
return compileCb(null, htmlPluginData);
326344
}

0 commit comments

Comments
 (0)