diff --git a/README.md b/README.md index 1e6e4d1..4cb0948 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ This plugin was forked from the wonderful work done by [Slack](https://github.com/slackhq/csp-html-webpack-plugin) but adds some key features: - [Subresource Integrity](http://www.w3.org/TR/SRI/) (SRI) is a security feature that enables browsers to verify that files they fetch are delivered without unexpected manipulation. Thanks to [webpack-subresource-integrity](https://www.npmjs.com/package/webpack-subresource-integrity) plugin. +- [Trusted Types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/trusted-types) support and use of [DOMPurify](https://www.npmjs.com/package/dompurify) to sanitize any `innerHTML` calls to prevent XSS - [PrimeReact](https://www.primefaces.org/primereact/) special handling for inline CSS styles. See [Issue #2423](https://github.com/primefaces/primereact/issues/2423) - Configure NONCE for pre-loaded scripts - Typescript definition @@ -94,6 +95,7 @@ This `CspHtmlWebpackPlugin` accepts 2 params with the following structure: - If `enabled` is set the false, it will disable generating a CSP for all instances of `HtmlWebpackPlugin` in your webpack config. - `{boolean}` integrityEnabled - Enable or disable SHA384 [Subresource Integrity](http://www.w3.org/TR/SRI/) - `{boolean}` primeReactEnabled - Enable or disable custom [PrimeReact](https://www.primefaces.org/primereact/) NONCE value added to the environment for inline styles. + - `{boolean}` trustedTypesEnabled - Enable or disable [Trusted Types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/trusted-types) handling which automatically adds DOMPurify to sanitize `innerHTML` calls to prevent XSS - `{string}` hashingMethod - accepts 'sha256', 'sha384', 'sha512' - your node version must also accept this hashing method. - `{object}` hashEnabled - a `` entry for which policy rules are allowed to include hashes - `{object}` nonceEnabled - a `` entry for which policy rules are allowed to include nonces @@ -104,6 +106,39 @@ This `CspHtmlWebpackPlugin` accepts 2 params with the following structure: - `$`: the `cheerio` object of the html file currently being processed - `compilation`: Internal webpack object to manipulate the build +## Trusted Types + +[Trusted Types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/trusted-types) is a newer CSP directive which adds XSS protection by preventing `innerHTML` without being trusted. + +To add Trusted Type support automatically to your application you would add the `require-trusted-types-for 'script'` CSP directive. + +```javascript +{ + 'base-uri': "'self'", + 'object-src': "'none'", + 'script-src': ["'strict-dynamic'"], + 'style-src': ["'self'"], + 'require-trusted-types-for': ["'script'"] +}; +``` + +If `trustedTypesEnabled=true` this plugin will automatically add a special script which executes before any other script to enable a default policy that sanitizes HTML using DOMPurify. + +```javascript +import DOMPurify from 'dompurify'; + +if (window.trustedTypes && window.trustedTypes.createPolicy) { // Feature testing + window.trustedTypes.createPolicy('default', { + createHTML: (string) => DOMPurify.sanitize(string, {RETURN_TRUSTED_TYPE: true}), + createScriptURL: string => string, // allow scripts + createScript: string => string // allow scripts + }); +}; +``` + +You will need to include DOMPurify and Trusted Types Polyfill using `npm install dompurify trusted-types` to your `package.json`. + + ## Appendix #### Default Policy: @@ -124,6 +159,7 @@ This `CspHtmlWebpackPlugin` accepts 2 params with the following structure: enabled: true, integrityEnabled: true, primeReactEnabled: true, + trustedTypesEnabled: true, hashingMethod: 'sha384', hashEnabled: { 'script-src': true, @@ -149,6 +185,7 @@ new CspHtmlWebpackPlugin({ enabled: true, integrityEnabled: true, primeReactEnabled: true, + trustedTypesEnabled: true, hashingMethod: 'sha384', hashEnabled: { 'script-src': true, diff --git a/package-lock.json b/package-lock.json index d3e6e44..02a7a6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "cheerio": "^1.0.0-rc.5", "lodash": "^4.17.20", + "webpack-inject-plugin": "^1.5.5", "webpack-subresource-integrity": "^5.0.0" }, "devDependencies": { @@ -1965,6 +1966,14 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "engines": { + "node": "*" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -2633,6 +2642,14 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "node_modules/emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/enhanced-resolve": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz", @@ -6248,6 +6265,30 @@ "node": ">=6.11.5" } }, + "node_modules/loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/loader-utils/node_modules/json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -6462,8 +6503,7 @@ "node_modules/minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "node_modules/ms": { "version": "2.1.2", @@ -8015,6 +8055,17 @@ } } }, + "node_modules/webpack-inject-plugin": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/webpack-inject-plugin/-/webpack-inject-plugin-1.5.5.tgz", + "integrity": "sha512-cYhj/3X6m19zmIEb/Y09/VjCf9SeL+/7Wv6YrUi/wBGFQPFMINEfzHBJV0qokeqvUwE23h8NzrTIrkHALZ9PaA==", + "dependencies": { + "loader-utils": "~1.2.3" + }, + "peerDependencies": { + "webpack": ">=4.0.0" + } + }, "node_modules/webpack-sources": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.2.tgz", @@ -9787,6 +9838,11 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==" + }, "boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -10300,6 +10356,11 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=" + }, "enhanced-resolve": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz", @@ -12966,6 +13027,26 @@ "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==" }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + }, + "dependencies": { + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "requires": { + "minimist": "^1.2.0" + } + } + } + }, "locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -13128,8 +13209,7 @@ "minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "ms": { "version": "2.1.2", @@ -14323,6 +14403,14 @@ } } }, + "webpack-inject-plugin": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/webpack-inject-plugin/-/webpack-inject-plugin-1.5.5.tgz", + "integrity": "sha512-cYhj/3X6m19zmIEb/Y09/VjCf9SeL+/7Wv6YrUi/wBGFQPFMINEfzHBJV0qokeqvUwE23h8NzrTIrkHALZ9PaA==", + "requires": { + "loader-utils": "~1.2.3" + } + }, "webpack-sources": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.2.tgz", diff --git a/package.json b/package.json index 1aa6dc7..c32cc75 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "dependencies": { "cheerio": "^1.0.0-rc.5", "lodash": "^4.17.20", + "webpack-inject-plugin": "^1.5.5", "webpack-subresource-integrity": "^5.0.0" }, "peerDependencies": { diff --git a/plugin.d.ts b/plugin.d.ts index 175f552..4f1b1fc 100644 --- a/plugin.d.ts +++ b/plugin.d.ts @@ -72,16 +72,24 @@ declare namespace CspHtmlWebpackPlugin { * param. * * If `enabled` is set the false, it will disable generating a CSP for * all instances of HtmlWebpackPlugin in your webpack config. + * @default true */ enabled?: boolean | ((htmlPluginData: HtmlPluginData) => boolean) | undefined; /** - * Enable or disable SHA384 subresource integrity + * Enable or disable SHA384 subresource integrity. + * @default true */ integrityEnabled?: boolean | undefined; /** * Enable or disable custom PrimeReact NONCE value added to the environment for inline styles. + * @default true */ primeReactEnabled?: boolean | undefined; + /** + * Enable or disable Trusted Types default policy to sanitize innerHTML with DOMPurify. + * @default true + */ + trustedTypesEnabled?: boolean | undefined; /** * The hashing method. Your node version must also accept this hashing * method. diff --git a/plugin.js b/plugin.js index 09a938e..fa56871 100644 --- a/plugin.js +++ b/plugin.js @@ -7,6 +7,7 @@ const isFunction = require('lodash/isFunction'); const get = require('lodash/get'); const webpack = require('webpack'); const { SubresourceIntegrityPlugin } = require('webpack-subresource-integrity'); +const InjectPlugin = require('webpack-inject-plugin').default; // Attempt to load HtmlWebpackPlugin@4 // Borrowed from https://github.com/waysact/webpack-subresource-integrity/blob/master/index.js @@ -59,6 +60,7 @@ const defaultAdditionalOpts = { enabled: true, integrityEnabled: true, primeReactEnabled: true, + trustedTypesEnabled: true, hashingMethod: 'sha384', hashEnabled: { 'script-src': true, @@ -392,6 +394,23 @@ class CspHtmlWebpackPlugin { if (this.opts.enabled && this.opts.integrityEnabled) { new SubresourceIntegrityPlugin().apply(compiler); } + + // add default TrustedTypes policy which uses DOMPurify to sanitize HTML + if ( + this.opts.enabled && + this.opts.trustedTypesEnabled && + this.cspPluginPolicy['require-trusted-types-for'] + ) { + const purifyScript = `import DOMPurify from 'dompurify'; +if (window.trustedTypes && window.trustedTypes.createPolicy) { // Feature testing + window.trustedTypes.createPolicy('default', { + createHTML: (string) => DOMPurify.sanitize(string, {RETURN_TRUSTED_TYPE: true}), + createScriptURL: string => string, // allow scripts + createScript: string => string // allow scripts + }); +}`; + new InjectPlugin(() => purifyScript).apply(compiler); + } } }