├── strict-csp-html-webpack-plugin ├── test-fixture │ ├── src │ │ ├── app.js │ │ ├── library1.js │ │ ├── library2.js │ │ ├── index.js │ │ └── template.html │ ├── dist │ │ ├── app.bundle.js │ │ ├── bundle.js │ │ ├── library1.bundle.js │ │ ├── library2.bundle.js │ │ └── index.html │ └── webpack.config.js ├── package.json ├── plugin.js ├── integration.test.js ├── __snapshots__ │ └── integration.test.js.snap └── README.md ├── undo-dev-setup.sh ├── strict-csp ├── jest.config.js ├── .eslintrc.js ├── tsconfig.json ├── package.json ├── README.md ├── index.test.ts ├── __snapshots__ │ └── index.test.ts.snap └── index.ts ├── dev-setup.sh ├── .gitignore ├── .github └── workflows │ ├── npm-publish-webpack.yml │ ├── npm-publish-strict-csp.yml │ └── ci.yml ├── CONTRIBUTING.md ├── README.md ├── DEVELOP.md └── LICENSE /strict-csp-html-webpack-plugin/test-fixture/src/app.js: -------------------------------------------------------------------------------- 1 | console.log('app'); -------------------------------------------------------------------------------- /strict-csp-html-webpack-plugin/test-fixture/dist/app.bundle.js: -------------------------------------------------------------------------------- 1 | console.log("app"); -------------------------------------------------------------------------------- /strict-csp-html-webpack-plugin/test-fixture/src/library1.js: -------------------------------------------------------------------------------- 1 | console.log('library 1'); -------------------------------------------------------------------------------- /strict-csp-html-webpack-plugin/test-fixture/src/library2.js: -------------------------------------------------------------------------------- 1 | console.log('library 2'); -------------------------------------------------------------------------------- /strict-csp-html-webpack-plugin/test-fixture/dist/bundle.js: -------------------------------------------------------------------------------- 1 | console.log("Hello from webpack"); -------------------------------------------------------------------------------- /strict-csp-html-webpack-plugin/test-fixture/dist/library1.bundle.js: -------------------------------------------------------------------------------- 1 | console.log("library 1"); -------------------------------------------------------------------------------- /strict-csp-html-webpack-plugin/test-fixture/dist/library2.bundle.js: -------------------------------------------------------------------------------- 1 | console.log("library 2"); -------------------------------------------------------------------------------- /strict-csp-html-webpack-plugin/test-fixture/src/index.js: -------------------------------------------------------------------------------- 1 | console.log('Hello from webpack'); 2 | -------------------------------------------------------------------------------- /undo-dev-setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd strict-csp-html-webpack-plugin && npm unlink 'strict-csp' && cd .. 4 | cd strict-csp && rm -rf node_modules && cd .. 5 | cd strict-csp-html-webpack-plugin && rm -rf node_modules && cd .. -------------------------------------------------------------------------------- /strict-csp/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | testPathIgnorePatterns: ['/node_modules/', '/build/'], 6 | }; 7 | -------------------------------------------------------------------------------- /dev-setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd strict-csp && npm install && npm run-script build && cd .. 4 | cd strict-csp && npm link && cd .. 5 | cd strict-csp-html-webpack-plugin && npm link 'strict-csp' && cd .. 6 | cd strict-csp-html-webpack-plugin && npm link && cd .. -------------------------------------------------------------------------------- /strict-csp-html-webpack-plugin/test-fixture/src/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Complex Test 5 | 6 | 7 | 8 |

Hello World

9 | 10 | -------------------------------------------------------------------------------- /strict-csp/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | plugins: [ 5 | '@typescript-eslint', 6 | ], 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:@typescript-eslint/recommended', 10 | ], 11 | }; 12 | -------------------------------------------------------------------------------- /strict-csp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "lib": ["es6"], 6 | "allowJs": true, 7 | "outDir": "build", 8 | "rootDir": ".", 9 | "strict": true, 10 | "declaration": true, 11 | "skipLibCheck": true, 12 | "noImplicitAny": true, 13 | "esModuleInterop": true, 14 | "resolveJsonModule": true 15 | }, 16 | "include": [ 17 | "index.ts" 18 | ], 19 | "exclude": [ 20 | "**/*.test.ts", 21 | "jest.config.js" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | 4 | # dependencies 5 | strict-csp-html-webpack-plugin/node_modules 6 | react-app/node_modules 7 | strict-csp/node_modules 8 | 9 | # testing 10 | react-app/coverage 11 | 12 | # built 13 | strict-csp/build 14 | react-app/build 15 | react-app/dist 16 | 17 | # misc 18 | .DS_Store 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | .eslintcache 24 | react-app/.pnp 25 | react-app/.pnp.js 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | react-app/ -------------------------------------------------------------------------------- /.github/workflows/npm-publish-webpack.yml: -------------------------------------------------------------------------------- 1 | name: Node.js webpack Package 2 | on: 3 | workflow_dispatch: 4 | release: 5 | types: [created] 6 | defaults: 7 | run: 8 | working-directory: strict-csp-html-webpack-plugin 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | # Setup .npmrc file to publish to npm 15 | - uses: actions/setup-node@v2 16 | with: 17 | node-version: '20.x' 18 | registry-url: 'https://registry.npmjs.org' 19 | - run: npm install 20 | - run: npm publish 21 | env: 22 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/npm-publish-strict-csp.yml: -------------------------------------------------------------------------------- 1 | name: Node.js strict-csp Package 2 | on: 3 | workflow_dispatch: 4 | release: 5 | types: [created] 6 | defaults: 7 | run: 8 | working-directory: strict-csp 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | # Setup .npmrc file to publish to npm 15 | - uses: actions/setup-node@v2 16 | with: 17 | node-version: '20.x' 18 | registry-url: 'https://registry.npmjs.org' 19 | - run: npm install 20 | - run: npm run build 21 | - run: npm publish 22 | env: 23 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /strict-csp-html-webpack-plugin/test-fixture/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const StrictCspHtmlWebpackPlugin = require(path.resolve(__dirname, '..')); 4 | 5 | module.exports = { 6 | context: __dirname, 7 | mode: 'production', 8 | entry: { 9 | library1: './src/library1.js', 10 | app: './src/app.js', 11 | library2: './src/library2.js', 12 | }, 13 | output: { 14 | path: path.resolve(__dirname, 'dist'), 15 | filename: '[name].bundle.js', 16 | }, 17 | plugins: [ 18 | new HtmlWebpackPlugin({ 19 | template: './src/template.html', 20 | }), 21 | new StrictCspHtmlWebpackPlugin(HtmlWebpackPlugin), 22 | ], 23 | }; 24 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | 7 | jobs: 8 | build-and-test: 9 | runs-on: ubuntu-latest 10 | 11 | strategy: 12 | matrix: 13 | node-version: [20.x, 22.x] 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | 23 | - name: Set up development environment 24 | run: sh ./dev-setup.sh 25 | 26 | - name: Run tests for strict-csp 27 | run: npm test 28 | working-directory: ./strict-csp 29 | 30 | - name: Run integration tests for webpack-plugin 31 | run: npm test 32 | working-directory: ./strict-csp-html-webpack-plugin 33 | 34 | - name: Tear down development environment 35 | # Always run this step, even if tests fail 36 | if: always() 37 | run: bash ./undo-dev-setup.sh -------------------------------------------------------------------------------- /strict-csp-html-webpack-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "strict-csp-html-webpack-plugin", 3 | "version": "1.0.2", 4 | "description": "A webpack plugin that adds a hash-based strict CSP to help protect your site against XSS attacks.", 5 | "main": "plugin.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/google/strict-csp.git", 9 | "directory": "strict-csp-html-webpack-plugin" 10 | }, 11 | "devDependencies": { 12 | "cheerio": "^1.1.2", 13 | "html-webpack-plugin": "^5.6.4", 14 | "jest": "^30.1.3", 15 | "webpack": "^5.101.3", 16 | "webpack-cli": "^6.0.1" 17 | }, 18 | "scripts": { 19 | "test": "cd .. && bash ./dev-setup.sh && cd ./strict-csp-html-webpack-plugin && jest && cd .. && bash ./undo-dev-setup.sh" 20 | }, 21 | "author": "Lukas Weichselbaum ", 22 | "contributors": [ 23 | "Maud Nalpas " 24 | ], 25 | "keywords": [ 26 | "csp", 27 | "content-security-policy", 28 | "security" 29 | ], 30 | "license": "Apache-2.0" 31 | } 32 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement (CLA). You (or your employer) retain the copyright to your 10 | contribution; this simply gives us permission to use and redistribute your 11 | contributions as part of the project. Head over to 12 | to see your current agreements on file or 13 | to sign a new one. 14 | 15 | You generally only need to submit a CLA once, so if you've already submitted one 16 | (even if it was for a different project), you probably don't need to do it 17 | again. 18 | 19 | ## Code Reviews 20 | 21 | All submissions, including submissions by project members, require review. We 22 | use GitHub pull requests for this purpose. Consult 23 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 24 | information on using pull requests. 25 | 26 | ## Community Guidelines 27 | 28 | This project follows 29 | [Google's Open Source Community Guidelines](https://opensource.google/conduct/). 30 | -------------------------------------------------------------------------------- /strict-csp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "strict-csp", 3 | "version": "1.1.2", 4 | "description": "Enables a hash-based strict Content Security Policy for static HTML files and single page applications.", 5 | "main": "build/index.js", 6 | "types": "build/index.d.ts", 7 | "scripts": { 8 | "build": "rimraf build && tsc", 9 | "auto-csp": "npm run build && node build/index.js", 10 | "lint": "eslint . --ext .ts", 11 | "tsc": "tsc", 12 | "test": "npm run build && tsc --noEmit && jest" 13 | }, 14 | "keywords": [ 15 | "csp", 16 | "content-security-policy", 17 | "security" 18 | ], 19 | "author": "Lukas Weichselbaum ", 20 | "contributors": [ 21 | "Maud Nalpas " 22 | ], 23 | "license": "Apache-2.0", 24 | "prepublish": "tsc", 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/google/strict-csp.git", 28 | "directory": "strict-csp" 29 | }, 30 | "dependencies": { 31 | "@types/cheerio": "^0.22.23", 32 | "cheerio": "^1.1.2" 33 | }, 34 | "devDependencies": { 35 | "@types/jest": "^30.0.0", 36 | "@types/node": "^20.0.0", 37 | "@typescript-eslint/eslint-plugin": "^4.10.0", 38 | "@typescript-eslint/parser": "^4.10.0", 39 | "eslint": "^7.15.0", 40 | "jest": "^30.1.3", 41 | "rimraf": "^3.0.2", 42 | "ts-jest": "^29.4.1", 43 | "typescript": "^4.1.3" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Glossary 2 | 3 | - CSP (content-security-policy): A layer of security that can be added to web apps as an HTTP header or meta tag. [Source: MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) 4 | - Strict CSP: A specific set of CSP directives that has been identified as an effective and deployable mitigation against XSS (cross-site scripting). XSS is one of the most widespread sedcurity exploits. [Source: w3c](https://w3c.github.io/webappsec-csp/#strict-csp). 5 | - SPA (single-page application): a web app implementation that loads a single web document. When different content needs to be shown, it updates the body content of that document. [Source: MDN](https://developer.mozilla.org/en-US/docs/Glossary/SPA) 6 | 7 | ## About this repo 8 | 9 | Two codebases are in this repo: 10 | 11 | - `strict-csp`: a **bundler-agnostic library**, that can be used to generate a CSP. It now includes support for Trusted Types and violation reporting. [Go to strict-csp](/strict-csp) 12 | 13 | - `strict-csp-html-webpack-plugin`: a **webpack plugin** that configures a strict, hash-based CSP for an SPA. It uses the `strict-csp` library to form a CSP and hooks into the popular `HtmlWebpackPlugin` to set up this CSP as a `meta` HTML tag. [Go to strict-csp-html-webpack-plugin](/strict-csp-html-webpack-plugin) 14 | 15 | Both of these are available as separate npm packages. 16 | 17 | ## Setup for development purposes 18 | 19 | See [DEVELOP.md](/DEVELOP.md). 20 | 21 | ## Resources 22 | * [Mitigate cross-site scripting (XSS) with a strict Content Security Policy (CSP)](https://web.dev/strict-csp/) 23 | -------------------------------------------------------------------------------- /strict-csp-html-webpack-plugin/plugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2021 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | const strictCspLib = require('strict-csp'); 18 | 19 | const defaultOptions = { 20 | enabled: true, 21 | trustedTypes: false, 22 | unsafeEval: false, 23 | reportUri: '', 24 | }; 25 | 26 | class StrictCspHtmlWebpackPlugin { 27 | /** 28 | * 29 | * @param {object} options Additional options for this module. 30 | */ 31 | constructor(htmlWebpackPlugin, options = {}) { 32 | this.htmlWebpackPlugin = htmlWebpackPlugin; 33 | this.options = { ...defaultOptions, ...options }; 34 | } 35 | 36 | /** 37 | * Processes HtmlWebpackPlugin's html data by adding the CSP 38 | * @param htmlPluginData 39 | * @param compileCb 40 | */ 41 | processCsp(compilation, htmlPluginData, compileCb) { 42 | if (this.options.enabled) { 43 | const { 44 | trustedTypes, 45 | enableTrustedTypes, 46 | enableTrustedTypesReportOnly, 47 | enableUnsafeEval, 48 | reportUri, 49 | } = this.options; 50 | 51 | const processor = new strictCspLib.StrictCsp(htmlPluginData.html, { 52 | trustedTypes: 53 | enableTrustedTypesReportOnly || trustedTypes === 'report-only' 54 | ? 'report-only' 55 | : enableTrustedTypes || trustedTypes, 56 | unsafeEval: enableUnsafeEval, 57 | reportUri: reportUri, 58 | browserFallbacks: true, // Fallbacks are always enabled in the plugin 59 | }); 60 | 61 | const { csp } = processor.process(); 62 | htmlPluginData.html = processor.serializeDomWithStrictCspMetaTag(csp); 63 | } 64 | 65 | return compileCb(null, htmlPluginData); 66 | } 67 | 68 | /** 69 | * Hooks into webpack to collect assets and hash them, build the policy, and add it into our HTML template 70 | * @param compiler 71 | */ 72 | apply(compiler) { 73 | compiler.hooks.compilation.tap( 74 | 'StrictCspHtmlWebpackPlugin', 75 | (compilation) => { 76 | const hook = 77 | typeof this.htmlWebpackPlugin.getHooks === 'function' 78 | ? this.htmlWebpackPlugin.getHooks(compilation).beforeEmit // html-webpack-plugin v4 and above 79 | : compilation.hooks.htmlWebpackPluginAfterHtmlProcessing; // html-webpack-plugin v3 80 | 81 | hook.tapAsync( 82 | 'StrictCspHtmlWebpackPlugin', 83 | this.processCsp.bind(this, compilation) 84 | ); 85 | } 86 | ); 87 | } 88 | } 89 | 90 | module.exports = StrictCspHtmlWebpackPlugin; 91 | -------------------------------------------------------------------------------- /strict-csp-html-webpack-plugin/integration.test.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | const cheerio = require('cheerio'); 5 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | const StrictCspHtmlWebpackPlugin = require(path.resolve(__dirname, './plugin.js')); 7 | 8 | const runWebpack = (options, done) => { 9 | const config = { 10 | context: __dirname, 11 | mode: 'production', 12 | entry: { 13 | library1: './test-fixture/src/library1.js', 14 | app: './test-fixture/src/app.js', 15 | library2: './test-fixture/src/library2.js', 16 | }, 17 | output: { 18 | path: path.resolve(__dirname, 'test-fixture/dist'), 19 | filename: '[name].bundle.js', 20 | }, 21 | plugins: [ 22 | new HtmlWebpackPlugin({ 23 | template: './test-fixture/src/template.html', 24 | }), 25 | new StrictCspHtmlWebpackPlugin(HtmlWebpackPlugin, options), 26 | ], 27 | }; 28 | 29 | webpack(config, (err, stats) => { 30 | if (err) { 31 | return done(err); 32 | } 33 | if (stats.hasErrors()) { 34 | return done(new Error(stats.toJson().errors.map((e) => e.message).join('\n'))); 35 | } 36 | 37 | const outputFile = path.resolve( 38 | __dirname, 39 | 'test-fixture/dist/index.html' 40 | ); 41 | const outputHtml = fs.readFileSync(outputFile, 'utf8'); 42 | done(null, outputHtml); 43 | }); 44 | }; 45 | 46 | describe('StrictCspHtmlWebpackPlugin Integration Test', () => { 47 | it('should build successfully without Trusted Types', (done) => { 48 | runWebpack({}, (err, outputHtml) => { 49 | if (err) return done(err); 50 | const $ = cheerio.load(outputHtml); 51 | const metaTag = $('meta[http-equiv="Content-Security-Policy"]'); 52 | expect(metaTag.length).toBe(1); 53 | const cspContent = metaTag.attr('content'); 54 | expect(cspContent).not.toContain('require-trusted-types-for'); 55 | expect(outputHtml).toMatchSnapshot(); 56 | done(); 57 | }); 58 | }); 59 | 60 | it('should build successfully with Trusted Types enabled', (done) => { 61 | runWebpack({ trustedTypes: true }, (err, outputHtml) => { 62 | if (err) return done(err); 63 | const $ = cheerio.load(outputHtml); 64 | const metaTag = $('meta[http-equiv="Content-Security-Policy"]'); 65 | expect(metaTag.length).toBe(1); 66 | const cspContent = metaTag.attr('content'); 67 | expect(cspContent).toContain("require-trusted-types-for 'script'"); 68 | expect(outputHtml).toMatchSnapshot(); 69 | done(); 70 | }); 71 | }); 72 | 73 | it('should build successfully with Trusted Types in report-only mode', (done) => { 74 | runWebpack( 75 | { 76 | trustedTypes: 'report-only', 77 | reportUri: 'https://example.com/report', 78 | }, 79 | (err, outputHtml) => { 80 | if (err) return done(err); 81 | const $ = cheerio.load(outputHtml); 82 | const metaTag = $('meta[http-equiv="Content-Security-Policy"]'); 83 | expect(metaTag.length).toBe(1); 84 | const cspContent = metaTag.attr('content'); 85 | expect(cspContent).toContain("require-trusted-types-for 'script'"); 86 | const loaderScript = $('script:not([src])').html(); 87 | expect(loaderScript).toContain('const generateAndSendReport'); 88 | expect(outputHtml).toMatchSnapshot(); 89 | done(); 90 | } 91 | ); 92 | }); 93 | }); -------------------------------------------------------------------------------- /strict-csp-html-webpack-plugin/test-fixture/dist/index.html: -------------------------------------------------------------------------------- 1 | Complex Test

Hello World

-------------------------------------------------------------------------------- /DEVELOP.md: -------------------------------------------------------------------------------- 1 | # DEVELOP 2 | 3 | ## Quickstart 4 | 5 | 1. Dev setup: 6 | 7 | ```bash 8 | sh ./dev-setup.sh 9 | ``` 10 | 11 | This will create some of the local symlinks you need (see details below). 12 | 13 | 2. Create a test React app and link it with your local plugin 14 | 15 | ```bash 16 | npx create-react-app react-app && cd react-app 17 | # make the local CSP plugin instance available in the React app 18 | # note that the symlink 'strict-csp-html-webpack-plugin' has been creaed at Step 1 19 | npm i --save strict-csp-html-webpack-plugin@beta 20 | npm link 'strict-csp-html-webpack-plugin' 21 | # eject so you can edit the webpack config to add CSP functionality to the React app 22 | npm run eject 23 | ``` 24 | 25 | (Note: if you need to unlink later: `cd react-app && npm unlink 'strict-csp-html-webpack-plugin' && cd ..`) 26 | 27 | 3. Add CSP functionality to the React app 28 | 29 | In react-app's `webpack.config.js`: 30 | 31 | ```javascript 32 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 33 | const StrictCspHtmlWebpackPlugin = require('strict-csp-html-webpack-plugin'); 34 | 35 | module.exports = function (webpackEnv) { 36 | return { 37 | // ... 38 | plugins: [ 39 | new HtmlWebpackPlugin( 40 | Object.assign( 41 | {} 42 | // ... HtmlWebpackPlugin config 43 | ) 44 | ), 45 | new StrictCspHtmlWebpackPlugin(HtmlWebpackPlugin), 46 | ], 47 | }; 48 | }; 49 | ``` 50 | 51 | 4. Startup: 52 | 53 | ```bash 54 | cd react-app && npm start 55 | ``` 56 | 57 | ✨ That's it. Open `http://localhost:{port}` and inspect `index.html`. Observe that react app's `index.html` includes a valid hash-based CSP in a meta tag. 58 | 59 | ### Developing 60 | 61 | #### When changing the library code (strict-csp) 62 | 63 | 🚨 Every time you change strict-csp code, you need to **rebuild it** so that the changes are picked up by the strict csp webpack plugin. Build like this: 64 | 65 | `cd strict-csp && npm run-script build && cd ..` 66 | 67 | Note: 68 | 69 | - If you've added new dependencies to strict-csp, also run `npm install` (as follows: `cd strict-csp && npm i && npm run-script build && cd ..`). 70 | - No need to `link` again here, this only needs to be done once. 71 | 72 | #### When changing the plugin code 73 | 74 | Every time you change locally the plugin code (`strictCspWebpackPlugin.js`), you need to restart the react app with `npm start` to see the changes. 75 | 76 | ## How the development setup works 77 | 78 | To develop this plugin locally, you need to create the symlinks as illustrated below. `dev-setup.sh` does this for you. 79 | `undo-dev-setup.sh` undoes this (this is convenient if you need to debug `dev-setup.sh` itself). 80 | 81 | ![image](https://user-images.githubusercontent.com/9762897/110346153-91087180-802f-11eb-96f9-fa79e9068dfb.png) 82 | 83 | Note: the **exact `html-webpack-plugin` instance** that `strict-csp-webpack-plugin` hooks into **must be referenced** by `strict-csp-webpack-plugin`, otherwise the hooking won't work and the CSP won't be set. It's a known thing with webpack and it's also the way other plugins that use `html-webpack-plugin` work. [Details](https://github.com/jantimon/html-webpack-plugin/issues/1091). 84 | 85 | ## What `dev-setup.sh` does 86 | 87 | - Builds the library, so that there's something to link to: 88 | `cd strict-csp && npm install && npm run-script build && cd ..` 89 | - Creates a symlink to the library: 90 | `cd strict-csp && npm link && cd ..` 91 | - Links to the library where needed: 92 | `cd strict-csp-html-webpack-plugin && npm link 'strict-csp' && cd ..` 93 | 94 | This is done only once. 95 | 96 | 🧐 Troubleshooting: if you get an error like "linked library not found", ensure that `main` in strict-csp's `package.json` points to a file that exists. 97 | 98 | ## To reset linking 99 | 100 | `sh ./undo-dev-setup.sh` 101 | 102 | ## To troubleshoot individual linking issues 103 | 104 | `npm uninstall` 105 | 106 | `npm ls --depth=0 --link=true` 107 | -------------------------------------------------------------------------------- /strict-csp/README.md: -------------------------------------------------------------------------------- 1 | # strict-csp 2 | 3 | [Available on npm](https://www.npmjs.com/package/strict-csp) 4 | 5 | ⚠️ This is experimental. Make sure to check [what's not supported](https://github.com/google/strict-csp/issues?q=is%3Aissue+is%3Aopen+label%3Afeature). 6 | 7 | ## What this library does: defense-in-depth against XSS 🛡 8 | 9 | _💡 Are you using webpack? Head over to [strict-csp-html-webpack-plugin](https://github.com/google/strict-csp/tree/main/strict-csp-html-webpack-plugin) instead. It uses this library under the hood to generate a CSP you can use in your webpack project!_ 10 | 11 | Cross-site scripting (XSS)—the ability to inject malicious scripts into a web application—has been one of the biggest web security vulnerabilities for over a decade. 12 | 13 | strict-csp is a **bundler-agnostic** library that helps protect your single-page application against XSS attacks. It does so by generating a [strict, hash-based Content-Security-Policy (CSP)](https://web.dev/strict-csp) for your web application. 14 | 15 | A strict CSP helps protect your site against XSS by preventing browsers from executing malicious scripts. 16 | 17 | ## Usage 18 | 19 | This library offers two primary workflows for applying a strict CSP. 20 | 21 | ### Workflow 1: HTTP Header (Recommended) 22 | 23 | The recommended and most secure approach is to set the CSP as an HTTP response header. The `.process()` method returns both the modified HTML and the CSP string needed for the header. 24 | 25 | ```javascript 26 | // Let's say `htmlString` is your SPA's html as a string. 27 | const processor = new StrictCsp(htmlString, { 28 | // Configuration options go here 29 | browserFallbacks: true, 30 | }); 31 | 32 | // Process the HTML and generate the CSP. 33 | const { html, csp } = processor.process(); 34 | 35 | // In your server: 36 | // 1. Set the CSP as an HTTP Header. 37 | // response.setHeader('Content-Security-Policy', csp); 38 | // 2. Serve the modified HTML. 39 | // response.send(html); 40 | ``` 41 | 42 | ### Workflow 2: Meta Tag (Alternative) 43 | 44 | If you cannot set HTTP headers, you can inject the CSP into a `` tag. 45 | 46 | ```javascript 47 | const processor = new StrictCsp(htmlString); 48 | 49 | // 1. Process the HTML to get the CSP string. 50 | // (We ignore the 'html' returned here as it will be outdated). 51 | const { csp } = processor.process(); 52 | 53 | // 2. Add the meta tag and get the final HTML. 54 | const finalHtml = processor.serializeDomWithStrictCspMetaTag(csp); 55 | 56 | // Serve the finalHtml. 57 | ``` 58 | 59 | ## Example with Trusted Types 60 | 61 | You can also use this library to configure [Trusted Types](https://web.dev/trusted-types) and set up violation reporting. The `.process()` method automatically handles injecting the necessary reporting scripts into the HTML and adding the required directives to the CSP. 62 | 63 | ```javascript 64 | const processor = new StrictCsp(htmlString, { 65 | // Enable Trusted Types in report-only mode 66 | trustedTypes: 'report-only', 67 | // Specify an endpoint for violation reports 68 | reportUri: 'https://your-reporting-endpoint.com/report', 69 | }); 70 | 71 | const { html, csp } = processor.process(); 72 | 73 | // The `html` now contains the TT reporting scripts. 74 | // The `csp` now contains the 'require-trusted-types-for' directive. 75 | ``` 76 | 77 | **Note on Report-Only Mode:** The `trustedTypes: 'report-only'` option works by injecting a script that simulates this mode on the client-side by creating a **default policy** (`trustedTypes.createPolicy('default', ...)`). This policy intercepts calls to dangerous DOM sinks, reports violations, but ultimately allows them to proceed. This is especially useful for static deployments (e.g., with a meta tag) where you cannot set the standard `Content-Security-Policy-Report-Only` HTTP header. 78 | 79 | ## Configuration Options 80 | 81 | You can pass a configuration object to the `StrictCsp` constructor: 82 | 83 | | Option | Type | What it does | 84 | | :--- | :--- | :--- | 85 | | `browserFallbacks` | `boolean` | (Default: `true`) When `true`, enables fallbacks for older browsers. This does not weaken the policy for modern browsers. | 86 | | `trustedTypes` | `boolean` \| `'report-only'` | (Default: `false`) When `true`, enforces [Trusted Types](https://web.dev/trusted-types). When `'report-only'`, it enables violation reporting without enforcement. | 87 | | `reportUri` | `string` | A URL where CSP and Trusted Types violation reports should be sent. | 88 | | `unsafeEval` | `boolean` | (Default: `false`) When `true`, adds `'unsafe-eval'` to the policy in case you cannot remove all uses of `eval()`. | 89 | 90 | ## How does this library work? 91 | 92 | Here's what the library does: 93 | 94 | 1. It finds all externally sourced scripts (`" 21 | `; 22 | 23 | exports[`StrictCspHtmlWebpackPlugin Integration Test should build successfully with Trusted Types in report-only mode 1`] = ` 24 | "Complex Test

Hello World

" 91 | `; 92 | 93 | exports[`StrictCspHtmlWebpackPlugin Integration Test should build successfully without Trusted Types 1`] = ` 94 | "Complex Test

Hello World

" 106 | `; 107 | -------------------------------------------------------------------------------- /strict-csp/index.test.ts: -------------------------------------------------------------------------------- 1 | // strict-csp/index.test.ts 2 | import { StrictCsp } from './index'; 3 | import * as crypto from 'crypto'; 4 | 5 | describe('StrictCsp.hashInlineScript', () => { 6 | it('should correctly calculate the sha256 hash of a simple script', () => { 7 | // Arrange 8 | const scriptContent = `console.log('hello');`; 9 | const expectedHash = crypto 10 | .createHash('sha256') 11 | .update(scriptContent, 'utf-8') 12 | .digest('base64'); 13 | 14 | // Act 15 | const result = StrictCsp.hashInlineScript(scriptContent); 16 | 17 | // Assert 18 | expect(result).toBe(`'sha256-${expectedHash}'`); 19 | }); 20 | 21 | it('should produce a consistent hash for an empty string', () => { 22 | const result = StrictCsp.hashInlineScript(''); 23 | expect(result).toBe("'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='"); 24 | }); 25 | 26 | it('should correctly handle scripts with leading/trailing whitespace', () => { 27 | // Whitespace is significant in hashes 28 | const scriptContent = ` 29 | alert('test'); 30 | `; 31 | const expectedHash = crypto 32 | .createHash('sha256') 33 | .update(scriptContent, 'utf-8') 34 | .digest('base64'); 35 | 36 | const result = StrictCsp.hashInlineScript(scriptContent); 37 | expect(result).toBe(`'sha256-${expectedHash}'`); 38 | }); 39 | 40 | it('should produce a known hash for a specific script', () => { 41 | const scriptContent = `console.log('Hello, World!');`; 42 | const expectedHash = `'sha256-VrXiRzNabZlVUzrPKgON5EtG2BuRUP8wULVkbIOqqkA='`; 43 | const result = StrictCsp.hashInlineScript(scriptContent); 44 | expect(result).toBe(expectedHash); 45 | }); 46 | }); 47 | 48 | describe('StrictCsp.process() CSP generation', () => { 49 | it('should generate a valid strict CSP policy', () => { 50 | const html = ``; 51 | const processor = new StrictCsp(html, { 52 | browserFallbacks: true, 53 | trustedTypes: false, 54 | unsafeEval: false, 55 | }); 56 | const { csp } = processor.process(); 57 | expect(csp).toMatchSnapshot(); 58 | }); 59 | 60 | it('should generate a CSP with no hashes for empty html', () => { 61 | const processor = new StrictCsp('', { 62 | browserFallbacks: true, 63 | trustedTypes: false, 64 | unsafeEval: false, 65 | }); 66 | const { csp } = processor.process(); 67 | expect(csp).toMatchSnapshot(); 68 | }); 69 | 70 | it('should generate a CSP with browser fallbacks disabled', () => { 71 | const html = ``; 72 | const processor = new StrictCsp(html, { 73 | browserFallbacks: false, 74 | }); 75 | const { csp } = processor.process(); 76 | expect(csp).toMatchSnapshot(); 77 | }); 78 | 79 | it('should generate a CSP with Trusted Types enabled', () => { 80 | const html = ``; 81 | const processor = new StrictCsp(html, { 82 | trustedTypes: true, 83 | }); 84 | const { csp } = processor.process(); 85 | expect(csp).toMatchSnapshot(); 86 | }); 87 | 88 | it('should generate a CSP with unsafe-eval enabled', () => { 89 | const html = ``; 90 | const processor = new StrictCsp(html, { 91 | unsafeEval: true, 92 | }); 93 | const { csp } = processor.process(); 94 | expect(csp).toMatchSnapshot(); 95 | }); 96 | 97 | it('should generate a CSP with all options enabled', () => { 98 | const html = ``; 99 | const processor = new StrictCsp(html, { 100 | browserFallbacks: true, 101 | trustedTypes: true, 102 | unsafeEval: true, 103 | }); 104 | const { csp } = processor.process(); 105 | expect(csp).toMatchSnapshot(); 106 | }); 107 | }); 108 | 109 | describe('StrictCsp end-to-end serialization', () => { 110 | it('should correctly refactor and add a CSP meta tag to a document', () => { 111 | const initialHtml = ` 112 | 113 | 114 | 115 | Test 116 | 117 | 118 | 119 | 120 | 121 | `; 122 | 123 | const processor = new StrictCsp(initialHtml, { 124 | browserFallbacks: true, 125 | }); 126 | const { csp } = processor.process(); 127 | processor.addMetaTag(csp); 128 | const finalHtml = processor.serializeDom(); 129 | 130 | expect(finalHtml).toMatchSnapshot(); 131 | }); 132 | 133 | it('should correctly refactor and add a CSP meta tag using the convenience method', () => { 134 | const initialHtml = ` 135 | 136 | 137 | 138 | Test 139 | 140 | 141 | 142 | 143 | 144 | `; 145 | 146 | const processor = new StrictCsp(initialHtml, { 147 | browserFallbacks: true, 148 | }); 149 | const { csp } = processor.process(); 150 | const finalHtml = processor.serializeDomWithStrictCspMetaTag(csp); 151 | 152 | expect(finalHtml).toMatchSnapshot(); 153 | }); 154 | 155 | it('should correctly preserve the type="module" attribute', () => { 156 | const initialHtml = ` 157 | 158 | 159 | 160 | 161 | 162 | `; 163 | 164 | const processor = new StrictCsp(initialHtml); 165 | const { csp } = processor.process(); 166 | processor.addMetaTag(csp); 167 | const finalHtml = processor.serializeDom(); 168 | 169 | expect(finalHtml).toMatchSnapshot(); 170 | }); 171 | }); 172 | 173 | describe('StrictCsp with TrustedTypes', () => { 174 | const baseHtml = ` 175 | 176 | 177 | 178 | Test 179 | 180 | 181 | 182 | `; 183 | 184 | it('should add a reporter script', () => { 185 | const processor = new StrictCsp(baseHtml, { 186 | trustedTypes: true, 187 | reportUri: 'https://example.com/report', 188 | }); 189 | const { html } = processor.process(); 190 | expect(html).toMatchSnapshot(); 191 | }); 192 | 193 | it('should add a report-only script', () => { 194 | const processor = new StrictCsp(baseHtml, { 195 | trustedTypes: 'report-only', 196 | reportUri: 'https://example.com/report', 197 | }); 198 | const { html } = processor.process(); 199 | expect(html).toMatchSnapshot(); 200 | }); 201 | 202 | it('should handle a missing reportUri for the reporter script', () => { 203 | const processor = new StrictCsp(baseHtml, { trustedTypes: true }); 204 | const { html } = processor.process(); 205 | expect(html).toMatchSnapshot(); 206 | }); 207 | 208 | it('should handle a missing reportUri for the report-only script', () => { 209 | const processor = new StrictCsp(baseHtml, { trustedTypes: 'report-only' }); 210 | const { html } = processor.process(); 211 | expect(html).toMatchSnapshot(); 212 | }); 213 | 214 | it('should refactor scripts with Trusted Types enabled', () => { 215 | const initialHtml = ` 216 | 217 | 218 | 219 | 220 | 221 | `; 222 | 223 | const processor = new StrictCsp(initialHtml, { trustedTypes: true }); 224 | const { html } = processor.process(); 225 | expect(html).toMatchSnapshot(); 226 | }); 227 | }); -------------------------------------------------------------------------------- /strict-csp-html-webpack-plugin/README.md: -------------------------------------------------------------------------------- 1 | # strict-csp-html-webpack-plugin 2 | 3 | [Available on npm](https://www.npmjs.com/package/strict-csp-html-webpack-plugin) 4 | 5 | ⚠️ This is experimental. Make sure to check [what's not supported](https://github.com/google/strict-csp/issues?q=is%3Aissue+is%3Aopen+label%3Afeature). Keep in mind that the `Report-Only` mode is not supported here since the policy is added via a meta tag (`Content-Security-Policy-Report-Only` is unfortunately not supported in meta tags). 6 | 7 | ## What this plugin does: defense-in-depth against XSS 🛡 8 | 9 | *💡 Not using webpack? Head over to [strict-csp](https://github.com/google/strict-csp/tree/main/strict-csp) instead. It's the bundler-agnostic library this webpack plugin is based on, and it enables you to easily set a strict CSP.* 10 | 11 | Cross-site scripting (XSS)—the ability to inject malicious scripts into a web application—has been one of the biggest web security vulnerabilities for over a decade. 12 | 13 | strict-csp-html-webpack-plugin helps protect your single-page application against XSS attacks. It does so by configuring a [strict Content-Security-Policy (CSP)](https://web.dev/strict-csp) for your web application. 14 | 15 | A strict CSP, added in the form of an HTML `meta` tag, looks as follows: 16 | 17 | ```html 18 | 21 | 22 | ``` 23 | 24 | ## Quickstart 🚀 25 | 26 | ### Step 1: install the plugin 27 | 28 | `npm i --save strict-csp-html-webpack-plugin@beta` 29 | 30 | (or with `yarn`) 31 | 32 | ### Step 2: Configure the plugin 33 | 34 | In your site's or app's `webpack.config.js`: 35 | 36 | ```javascript 37 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 38 | const StrictCspHtmlWebpackPlugin = require('strict-csp-html-webpack-plugin'); 39 | 40 | module.exports = function (webpackEnv) { 41 | return { 42 | // ... 43 | plugins: [ 44 | new HtmlWebpackPlugin( 45 | Object.assign( 46 | {} 47 | // ... HtmlWebpackPlugin config 48 | ) 49 | ), 50 | new StrictCspHtmlWebpackPlugin(HtmlWebpackPlugin), 51 | ], 52 | }; 53 | }; 54 | ``` 55 | 56 | ⚠️ If you have a React app created with create-react-app, you'll need to `eject` in order to configure and use this plugin (because you need access to the webpack config). 57 | 58 | ### Step 3: Restart the app 59 | 60 | - The app should run without errors (check the console). 61 | - Observe that a `meta` HTML tag has been added to the application's `index.html`, and that one inline script now loads all scripts. 62 | 63 | ✨ Your app is now protected from many XSS attacks. 64 | 65 | ## Configuring Trusted Types 66 | 67 | To enable Trusted Types and violation reporting, you can pass additional options to the plugin in your `webpack.config.js`: 68 | 69 | ```javascript 70 | new StrictCspHtmlWebpackPlugin(HtmlWebpackPlugin, { 71 | trustedTypes: 'report-only', // Recommended for testing 72 | reportUri: 'https://your-reporting-endpoint.com/report', 73 | }), 74 | ``` 75 | 76 | ## Options 77 | 78 | By default, strict-csp-html-webpack-plugin will set up a valid, strict, hash-based CSP. 79 | 80 | You can use additional options to configure the plugin: 81 | 82 | | Option | Default | What it does | 83 | | ----------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------- | 84 | | `enabled` | `true` | When `true`, activates the plugin. | 85 | | `trustedTypes` | `boolean` \| `'report-only'` | (Default: `false`) When `true`, enforces [Trusted Types](https://web.dev/trusted-types). When `'report-only'`, it enables violation reporting without enforcement. | 86 | | `reportUri` | `''` | The URI to send Trusted Types violation reports to. Requires `trustedTypes` to be set. | 87 | | `enableUnsafeEval` | `false` | When `true`, enables [unsafe-eval](https://web.dev/strict-csp/) in case you cannot remove all uses of `eval()`. | 88 | 89 | ## FAQ 90 | 91 | ### Does this plugin protect my users from XSS attacks? 92 | 93 | A CSP offers an *extra* layer of security (also called "defense-in-depth" technique) to mitigate XSS attacks. It's not a replacement for properly escaping user-controlled data and sanitizing user input. 94 | 95 | Now, this plugin sets a hash-based **strict** CSP. While this does remove several common XSS attack surfaces, it doesn't guarantee that your application is XSS-free. 96 | 97 | To cover most of the XSS attack surface, we recommend to also enable [Trusted Types](https://web.dev/trusted-types/) (DOM XSS). 98 | 99 | ### Where should I use this plugin? 100 | 101 | This plugin is best-suited for use in single-page applications that are served statically. 102 | If you are rendering HTML on the server-side, you will also have to consider stored and reflected XSS. In this case we recommend using a [nonce-based strict CSP](https://web.dev/strict-csp#step-1:-decide-if-you-need-a-nonce-or-hash-based-csp) instead. 103 | 104 | ### How does this plugin, **strict**-csp-html-webpack-plugin, differ from [csp-html-webpack-plugin](https://www.npmjs.com/package/csp-html-webpack-plugin)? 105 | 106 | This plugin **strict**-csp-html-webpack-plugin focuses on one thing: it mitigates XSS vulnerabilities. It does so by setting up a [strict CSP](https://web.dev/strict-csp), that is, an efficient defense-in-depth mechanism against XSS attacks. 107 | It automatically sets up a secure CSP and frees you from manual configurations. 108 | 109 | csp-html-webpack-plugin on the other hand, has a numbers of options to choose from. If you're using a CSP for other purposes than XSS mitigation, check out [csp-html-webpack-plugin](https://www.npmjs.com/package/csp-html-webpack-plugin). Note that at the moment, static nonces risk making csp-html-webpack-plugin's CSP bypassable, though this may be resolved in the future. 110 | 111 | ### I already have a CSP on my site, with an allowlist*. Should I consider using this plugin? 112 | *An allowlist CSP looks as follows: `default-src https://cdn.example https://site1.example https://site2.example;`. 113 | 114 | It depends. 115 | 116 | If you're using your allowlist CSP purely to load scripts coming from a certain origin, you can keep using it. 117 | 118 | But if you're relying on your allowlist CSP for XSS protection: migrate to the more secure strict CSP approach, and consider using this plugin to help you do so. 119 | 120 | Allowlist-based CSP are not recommended anymore for XSS protection, because don't efficiently protect sites against XSS attacks: [research has shown that they can be bypassed](https://research.google/pubs/pub45542/). 🥲 121 | They're also harder to maintain! 122 | 123 | Instead, strict CSPs are now recommended, because they're both [more secure and easier to maintain than allowlist-based CSPs](https://web.dev/strict-csp/#why-a-strict-csp-is-recommended-over-allowlist-csps). 124 | 125 | This plugin automatically adds a strict CSP to your application. 126 | 127 | ### Can this plugin slow down my site? 128 | 129 | See [issue #15](https://github.com/google/strict-csp/issues/15). 130 | 131 | ### How does a strict CSP compare with subresource integrity (SRI)? 132 | 133 | SRI can be used to ensure the integrity of scripts, e.g. to protect your site in case your CDN gets compromised. 134 | However, SRI will not mitigate XSS caused by an injection vulnerability *in your own site*. 135 | 136 | ### Why should sourced scripts be replaced by an inline script? 137 | 138 | A strict hash-based CSP allows certain scripts based on their hash. 139 | However, Firefox ([bug](https://bugzilla.mozilla.org/show_bug.cgi?id=1409200)) and Safari (bug) do not support hashes for externally-sourced scripts⏤only for inline scripts. 140 | Because this plugin aims at setting a CSP that helps protect your users in all browsers, it first transforms your externally-sourced scripts into an inline script. 141 | 142 | ### How does this plugin work? 143 | 144 | strict-csp-webpack-plugin uses the [strict-csp](https://github.com/google/strict-csp/tree/main/strict-csp) custom library to form a strict CSP and hooks into `HtmlWebpackPlugin` to set up this CSP as a `meta` HTML tag. 145 | 146 | Learn more about what the strict-csp library exactly does [here](https://github.com/google/strict-csp/tree/main/strict-csp). 147 | 148 | **TL;DR: this library automates the steps to [add a hash-based strict CSP to your site](https://web.dev/strict-csp/#adopting-a-strict-csp).** 149 | 150 | ## Resources 151 | * [Mitigate cross-site scripting (XSS) with a strict Content Security Policy (CSP)](https://web.dev/strict-csp/) 152 | 153 | 154 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /strict-csp/__snapshots__/index.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing 2 | 3 | exports[`StrictCsp end-to-end serialization should correctly preserve the type="module" attribute 1`] = ` 4 | " 5 | 6 | 7 | 8 | 9 | 21 | " 22 | `; 23 | 24 | exports[`StrictCsp end-to-end serialization should correctly refactor and add a CSP meta tag to a document 1`] = ` 25 | " 26 | 27 | 28 | 29 | Test 30 | 31 | 32 | 33 | 34 | 46 | " 47 | `; 48 | 49 | exports[`StrictCsp end-to-end serialization should correctly refactor and add a CSP meta tag using the convenience method 1`] = ` 50 | " 51 | 52 | 53 | 54 | Test 55 | 56 | 57 | 58 | 59 | 71 | " 72 | `; 73 | 74 | exports[`StrictCsp with TrustedTypes should add a report-only script 1`] = ` 75 | " 76 | 77 | 78 | 79 | Test 80 | 81 | 132 | 133 | " 134 | `; 135 | 136 | exports[`StrictCsp with TrustedTypes should add a reporter script 1`] = ` 137 | " 138 | 139 | 140 | 141 | Test 142 | 143 | 144 | 185 | " 186 | `; 187 | 188 | exports[`StrictCsp with TrustedTypes should handle a missing reportUri for the report-only script 1`] = ` 189 | " 190 | 191 | 192 | 193 | Test 194 | 195 | 220 | 221 | " 222 | `; 223 | 224 | exports[`StrictCsp with TrustedTypes should handle a missing reportUri for the reporter script 1`] = ` 225 | " 226 | 227 | 228 | 229 | Test 230 | 231 | 232 | 233 | " 234 | `; 235 | 236 | exports[`StrictCsp with TrustedTypes should refactor scripts with Trusted Types enabled 1`] = ` 237 | " 238 | 239 | 240 | 241 | 242 | 259 | " 260 | `; 261 | 262 | exports[`StrictCsp.process() CSP generation should generate a CSP with Trusted Types enabled 1`] = `"script-src 'strict-dynamic' 'sha256-tlts22Eu/seSWbAw80TfZJgYnelKmP4ds0Ijym8yNpY=' https: 'unsafe-inline';object-src 'none';base-uri 'self';require-trusted-types-for 'script';"`; 263 | 264 | exports[`StrictCsp.process() CSP generation should generate a CSP with all options enabled 1`] = `"script-src 'strict-dynamic' 'sha256-tlts22Eu/seSWbAw80TfZJgYnelKmP4ds0Ijym8yNpY=' https: 'unsafe-inline' 'unsafe-eval';object-src 'none';base-uri 'self';require-trusted-types-for 'script';"`; 265 | 266 | exports[`StrictCsp.process() CSP generation should generate a CSP with browser fallbacks disabled 1`] = `"script-src 'strict-dynamic' 'sha256-tlts22Eu/seSWbAw80TfZJgYnelKmP4ds0Ijym8yNpY=';object-src 'none';base-uri 'self';"`; 267 | 268 | exports[`StrictCsp.process() CSP generation should generate a CSP with no hashes for empty html 1`] = `"script-src 'strict-dynamic' https:;object-src 'none';base-uri 'self';"`; 269 | 270 | exports[`StrictCsp.process() CSP generation should generate a CSP with unsafe-eval enabled 1`] = `"script-src 'strict-dynamic' 'sha256-tlts22Eu/seSWbAw80TfZJgYnelKmP4ds0Ijym8yNpY=' https: 'unsafe-inline' 'unsafe-eval';object-src 'none';base-uri 'self';"`; 271 | 272 | exports[`StrictCsp.process() CSP generation should generate a valid strict CSP policy 1`] = `"script-src 'strict-dynamic' 'sha256-tlts22Eu/seSWbAw80TfZJgYnelKmP4ds0Ijym8yNpY=' https: 'unsafe-inline';object-src 'none';base-uri 'self';"`; 273 | -------------------------------------------------------------------------------- /strict-csp/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2021 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | import * as crypto from 'crypto'; 18 | import * as cheerio from 'cheerio'; 19 | 20 | /** CSP and Trusted Types configuration options. */ 21 | export interface StrictCspConfig { 22 | /** 23 | * Controls Trusted Types behavior. 24 | * - `true`: Enforces Trusted Types. 25 | * - `'report-only'`: Enables Trusted Types in report-only mode. 26 | * - `false` (default): Does not enable Trusted Types. 27 | */ 28 | trustedTypes?: boolean | 'report-only'; 29 | 30 | /** A URL where CSP and Trusted Types violation reports should be sent. */ 31 | reportUri?: string; 32 | 33 | /** 34 | * If true, adds `'unsafe-eval'` to the policy. Should only be used as a last 35 | * resort. Defaults to `false`. 36 | */ 37 | unsafeEval?: boolean; 38 | 39 | /** 40 | * If true, adds fallbacks for older browsers. Defaults to `true`. 41 | */ 42 | browserFallbacks?: boolean; 43 | } 44 | 45 | /** Module for enabling a hash-based strict Content Security Policy. */ 46 | export class StrictCsp { 47 | private static readonly HASH_FUNCTION = 'sha256'; 48 | private static readonly INLINE_SCRIPT_SELECTOR = 'script:not([src])'; 49 | private static readonly SOURCED_SCRIPT_SELECTOR = 'script[src]'; 50 | private $: cheerio.Root; 51 | private config: StrictCspConfig; 52 | private hashes: string[] = []; 53 | 54 | constructor(html: string, config: StrictCspConfig = {}) { 55 | this.$ = cheerio.load(html, { 56 | decodeEntities: false, 57 | _useHtmlParser2: true, 58 | xmlMode: false, 59 | }); 60 | this.config = { 61 | trustedTypes: false, 62 | unsafeEval: false, 63 | browserFallbacks: true, 64 | ...config, 65 | }; 66 | } 67 | 68 | serializeDom(): string { 69 | return this.$.root().html() || ''; 70 | } 71 | 72 | /** 73 | * Adds a CSP meta tag and serializes the DOM. 74 | * A convenience method for the meta tag workflow. 75 | * @param csp The Content-Security-Policy string. 76 | * @returns The processed HTML as a string. 77 | */ 78 | serializeDomWithStrictCspMetaTag(csp: string): string { 79 | this.addMetaTag(csp); 80 | return this.serializeDom(); 81 | } 82 | 83 | /** 84 | * Processes the HTML based on the configuration provided in the constructor. 85 | * @returns An object containing the modified HTML and the generated CSP 86 | * string. 87 | */ 88 | process(): {html: string; csp: string} { 89 | this.refactorSourcedScripts_(); 90 | this.hashes = this.hashAllInlineScripts_(); 91 | 92 | if (this.config.trustedTypes) { 93 | this.configureTrustedTypes_(); 94 | } 95 | 96 | const csp = this.generateCspString_(); 97 | 98 | return { 99 | html: this.serializeDom(), 100 | csp: csp, 101 | }; 102 | } 103 | 104 | private generateCspString_(): string { 105 | const hashes = this.hashes; 106 | const cspOptions = { 107 | enableBrowserFallbacks: this.config.browserFallbacks, 108 | enableTrustedTypes: !!this.config.trustedTypes, 109 | enableUnsafeEval: this.config.unsafeEval, 110 | }; 111 | let strictCspTemplate = { 112 | // 'strict-dynamic' allows hashed scripts to create new scripts. 113 | 'script-src': [`'strict-dynamic'`, ...hashes], 114 | // Restricts `object-src` to disable dangerous plugins like Flash. 115 | 'object-src': [`'none'`], 116 | // Restricts `base-uri` to block the injection of `` tags. This 117 | // prevents attackers from changing the locations of scripts loaded from 118 | // relative URLs. 119 | 'base-uri': [`'self'`], 120 | }; 121 | 122 | // Adds fallbacks for browsers not compatible to CSP3 and CSP2. 123 | // These fallbacks are ignored by modern browsers in presence of hashes, 124 | // and 'strict-dynamic'. 125 | if (cspOptions.enableBrowserFallbacks) { 126 | // Fallback for Safari. All modern browsers supporting strict-dynamic will 127 | // ignore the 'https:' fallback. 128 | strictCspTemplate['script-src'].push('https:'); 129 | // 'unsafe-inline' is only ignored in presence of a hash or nonce. 130 | if (hashes.length > 0) { 131 | strictCspTemplate['script-src'].push(`'unsafe-inline'`); 132 | } 133 | } 134 | 135 | // If enabled, dangerous DOM sinks will only accept typed objects instead of 136 | // strings. 137 | if (cspOptions.enableTrustedTypes) { 138 | strictCspTemplate = { 139 | ...strictCspTemplate, 140 | ...{ 'require-trusted-types-for': [`'script'`] }, 141 | }; 142 | } 143 | 144 | // If enabled, `eval()`-calls will be allowed, making the policy slightly 145 | // less secure. 146 | if (cspOptions.enableUnsafeEval) { 147 | strictCspTemplate['script-src'].push(`'unsafe-eval'`); 148 | } 149 | 150 | return Object.entries(strictCspTemplate) 151 | .map(([directive, values]) => { 152 | return `${directive} ${values.join(' ')};`; 153 | }) 154 | .join(''); 155 | } 156 | 157 | /** 158 | * Configures Trusted Types by adding the necessary reporting scripts. 159 | */ 160 | private configureTrustedTypes_(): void { 161 | if (this.config.trustedTypes === 'report-only') { 162 | const reportOnlyScript = StrictCsp.createReportOnlyModeScript( 163 | this.config.reportUri 164 | ); 165 | this.prependScriptToBody_(reportOnlyScript); 166 | } else { 167 | if (!this.config.reportUri) { 168 | this.appendScriptToBody_( 169 | `console.error("No reportUri provided. Trusted Types reports will not be sent to a remote endpoint.")` 170 | ); 171 | return; 172 | } 173 | const reporterScript = StrictCsp.createReporterScript( 174 | this.config.reportUri 175 | ); 176 | this.appendScriptToBody_(reporterScript); 177 | } 178 | } 179 | 180 | /** 181 | * Enables a CSP via a meta tag at the beginning of the document. 182 | * Warning: It's recommended to set CSP as HTTP response header instead of 183 | * using a meta tag. Injections before the meta tag will not be covered by CSP 184 | * and meta tags don't support CSP in report-only mode. 185 | * 186 | * @param csp A Content Security Policy string. 187 | */ 188 | addMetaTag(csp: string): void { 189 | let metaTag = this.$('meta[http-equiv="Content-Security-Policy"]'); 190 | if (!metaTag.length) { 191 | metaTag = cheerio.load('')( 192 | 'meta' 193 | ); 194 | metaTag.prependTo(this.$('head')); 195 | } 196 | metaTag.attr('content', csp); 197 | } 198 | 199 | /** 200 | * Creates a new script tag and adds it to the body element. 201 | * 202 | * @param script JS content of the script to be added. 203 | */ 204 | private appendScriptToBody_(script: string): void { 205 | const newScript = cheerio.load('