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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ This `CspHtmlWebpackPlugin` accepts 2 params with the following structure:
- `{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
- `{Function}` processFn - allows the developer to overwrite the default method of what happens to the CSP after it has been created
- Parameters are:
- `builtPolicy`: a `string` containing the completed policy;
- `htmlPluginData`: the `HtmlWebpackPlugin` `object`;
- `$`: the `cheerio` object of the html file currently being processed

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

Expand All @@ -49,6 +54,13 @@ The plugin also adds a new config option onto each `HtmlWebpackPlugin` instance:
- `{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
- `{Function}` processFn - allows the developer to overwrite the default method of what happens to the CSP after it has been created
- Parameters are:
- `builtPolicy`: a `string` containing the completed policy;
- `htmlPluginData`: the `HtmlWebpackPlugin` `object`;
- `$`: the `cheerio` object of the html file currently being processed

#### Order of Precedence:

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

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

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.

This is useful if you need different policy rules / processing functions for different `HtmlWebpackPlugin` instances
in the same webpack config.

#### Default Policy:

```
Expand All @@ -84,12 +99,16 @@ If 2 policies have the same key/policy rule, the former policy will override the
nonceEnabled: {
'script-src': true,
'style-src': true
}
},
processFn: defaultProcessFn
}
```

#### Full Configuration with all options:

Note that you don't have to include the same section in both `HtmlWebpackPlugin` and `CspHtmlWebpackPlugin`.
See the [Order of Precedence](#order-of-precedence) section above.

```
new HtmlWebpackPlugin({
cspPlugin: {
Expand All @@ -107,7 +126,8 @@ new HtmlWebpackPlugin({
nonceEnabled: {
'script-src': true,
'style-src': true
}
},
processFn: defaultProcessFn
}
});

Expand All @@ -126,7 +146,8 @@ new CspHtmlWebpackPlugin({
nonceEnabled: {
'script-src': true,
'style-src': true
}
},
processFn: defaultProcessFn
})
```

Expand Down
87 changes: 87 additions & 0 deletions plugin.jest.js
Original file line number Diff line number Diff line change
Expand Up @@ -852,4 +852,91 @@ describe('CspHtmlWebpackPlugin', () => {
});
});
});

describe('Custom process function', () => {
it('Allows the process function to be overwritten', done => {
const processFn = jest.fn();
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'`;

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(
{},
{
processFn
}
)
]);

webpackCompile(config, csps => {
// we've overwritten the default processFn, which writes the policy into the html file
// so it won't exist in this object anymore.
expect(csps['index.html']).toBeUndefined();

// The processFn should receive the built policy as it's first arg
expect(processFn).toHaveBeenCalledWith(
builtPolicy,
expect.anything(),
expect.anything()
);

done();
});
});

it('only overwrites the processFn for the HtmlWebpackInstance where it has been defined', done => {
const processFn = jest.fn();
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'`;
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'`;

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'
),
cspPlugin: {
processFn
}
}),
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()
]);

webpackCompile(config, csps => {
// it won't exist in the html file since we overwrote processFn
expect(csps['index-1.html']).toBeUndefined();
// processFn wasn't overwritten here, so this should be added to the html file as normal
expect(csps['index-2.html']).toEqual(index2BuiltPolicy);

// index-1.html should have used our custom function defined
expect(processFn).toHaveBeenCalledWith(
index1BuiltPolicy,
expect.anything(),
expect.anything()
);

done();
});
});
});
});
74 changes: 46 additions & 28 deletions plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,31 @@ try {
}
}

/**
* The default function for adding the CSP to the head of a document
* Can be overwritten to allow the developer to process the CSP in their own way
* @param {string} builtPolicy
* @param {object} htmlPluginData
* @param {object} $
*/
const defaultProcessFn = (builtPolicy, htmlPluginData, $) => {
let metaTag = $('meta[http-equiv="Content-Security-Policy"]');

// Add element if it doesn't exist.
if (!metaTag.length) {
metaTag = cheerio.load('<meta http-equiv="Content-Security-Policy">')(
'meta'
);
metaTag.prependTo($('head'));
}

// build the policy into the context attr of the csp meta tag
metaTag.attr('content', builtPolicy);

// eslint-disable-next-line no-param-reassign
htmlPluginData.html = $.html();
};

const defaultPolicy = {
'base-uri': "'self'",
'object-src': "'none'",
Expand All @@ -35,7 +60,8 @@ const defaultAdditionalOpts = {
nonceEnabled: {
'script-src': true,
'style-src': true
}
},
processFn: defaultProcessFn
};

class CspHtmlWebpackPlugin {
Expand Down Expand Up @@ -93,6 +119,13 @@ class CspHtmlWebpackPlugin {
...get(htmlPluginData, 'plugin.options.cspPlugin.nonceEnabled', {})
});

// 3. Get the processFn for this HtmlWebpackPlugin instance.
this.processFn = get(
htmlPluginData,
'plugin.options.cspPlugin.processFn',
this.opts.processFn || defaultProcessFn
);

return compileCb(null, htmlPluginData);
}

Expand Down Expand Up @@ -285,16 +318,6 @@ class CspHtmlWebpackPlugin {
return compileCb(null, htmlPluginData);
}

let metaTag = $('meta[http-equiv="Content-Security-Policy"]');

// Add element if it doesn't exist.
if (!metaTag.length) {
metaTag = cheerio.load('<meta http-equiv="Content-Security-Policy">')(
'meta'
);
metaTag.prependTo($('head'));
}

// get all nonces for script and style tags
const scriptNonce = this.setNonce($, 'script-src', 'script[src]');
const styleNonce = this.setNonce($, 'style-src', 'link[rel="stylesheet"]');
Expand All @@ -303,24 +326,19 @@ class CspHtmlWebpackPlugin {
const scriptShas = this.getShas($, 'script-src', 'script:not([src])');
const styleShas = this.getShas($, 'style-src', 'style:not([href])');

// build the policy into the context attr of the csp meta tag
metaTag.attr(
'content',
this.buildPolicy({
...this.policy,
'script-src': flatten([this.policy['script-src']]).concat(
scriptShas,
scriptNonce
),
'style-src': flatten([this.policy['style-src']]).concat(
styleShas,
styleNonce
)
})
);
const builtPolicy = this.buildPolicy({
...this.policy,
'script-src': flatten([this.policy['script-src']]).concat(
scriptShas,
scriptNonce
),
'style-src': flatten([this.policy['style-src']]).concat(
styleShas,
styleNonce
)
});

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

return compileCb(null, htmlPluginData);
}
Expand Down