├── .github └── workflows │ └── run_unit_tests.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── VERSION ├── docs ├── code-of-conduct.md ├── contributing.md └── developing.md ├── dom └── package.json ├── fixup.sh ├── integration_tests ├── basic_import │ ├── README.md │ ├── karma.conf.js │ ├── main.ts │ ├── package.json │ └── tsconfig.json ├── import_fully_specified_webpack │ ├── README.md │ ├── package.json │ └── src │ │ └── index.js └── jest │ ├── babel.config.json │ ├── jest.config.js │ ├── package.json │ ├── src │ ├── index.spec.ts │ └── index.ts │ └── tsconfig.json ├── karma.conf.js ├── package.json ├── restricted ├── legacy │ └── package.json └── reviewed │ └── package.json ├── run_integration.sh ├── src ├── README.md ├── builders │ ├── attribute_builders.ts │ ├── document_fragment_builders.ts │ ├── html_builders.ts │ ├── html_sanitizer │ │ ├── README.md │ │ ├── css │ │ │ ├── allowlists.ts │ │ │ ├── css_isolation.ts │ │ │ ├── sanitizer.ts │ │ │ ├── serializer.ts │ │ │ ├── serializer_test_data.ts │ │ │ ├── tokenizer.ts │ │ │ └── tokens.ts │ │ ├── default_css_sanitizer.ts │ │ ├── html_sanitizer.ts │ │ ├── html_sanitizer_builder.ts │ │ ├── inert_fragment.ts │ │ ├── no_clobber.ts │ │ ├── resource_url_policy.ts │ │ └── sanitizer_table │ │ │ ├── default_sanitizer_table.ts │ │ │ └── sanitizer_table.ts │ ├── resource_url_builders.ts │ ├── script_builders.ts │ ├── sensitive_attributes.ts │ ├── style_sheet_builders.ts │ └── url_builders.ts ├── dom │ ├── elements │ │ ├── anchor.ts │ │ ├── area.ts │ │ ├── base.ts │ │ ├── button.ts │ │ ├── element.ts │ │ ├── embed.ts │ │ ├── form.ts │ │ ├── iframe.ts │ │ ├── input.ts │ │ ├── link.ts │ │ ├── object.ts │ │ ├── script.ts │ │ ├── style.ts │ │ ├── svg.ts │ │ └── svg_use.ts │ ├── globals │ │ ├── document.ts │ │ ├── dom_parser.ts │ │ ├── fetch.ts │ │ ├── global.ts │ │ ├── location.ts │ │ ├── range.ts │ │ ├── service_worker_container.ts │ │ ├── url.ts │ │ ├── window.ts │ │ └── worker.ts │ ├── index.ts │ └── xss-dom-remediation.md ├── environment │ └── dev.ts ├── index.ts ├── internals │ ├── attribute_impl.ts │ ├── html_impl.ts │ ├── pure.ts │ ├── resource_url_impl.ts │ ├── script_impl.ts │ ├── secrets.ts │ ├── string_literal.ts │ ├── style_sheet_impl.ts │ ├── trusted_types.ts │ └── trusted_types_typings.d.ts └── restricted │ ├── README.md │ ├── legacy.ts │ └── reviewed.ts ├── test ├── builders │ ├── attribute_builders_test.ts │ ├── document_fragment_builders_test.ts │ ├── html_builders_test.ts │ ├── html_sanitizer │ │ ├── css │ │ │ ├── css_isolation_test.ts │ │ │ ├── sanitizer_test.ts │ │ │ ├── serializer_test.ts │ │ │ └── tokenizer_test.ts │ │ ├── html_sanitizer_builder_test.ts │ │ ├── html_sanitizer_test.ts │ │ ├── inert_fragment_test.ts │ │ ├── no_clobber_test.ts │ │ └── sanitizer_table │ │ │ └── sanitizer_table_test.ts │ ├── resource_url_builders_test.ts │ ├── script_builders_test.ts │ ├── style_sheet_builders_test.ts │ └── url_builders_test.ts ├── dom │ ├── elements │ │ ├── base_test.ts │ │ ├── element_test.ts │ │ ├── embed_test.ts │ │ ├── link_test.ts │ │ ├── script_test.ts │ │ ├── svg_test.ts │ │ └── svg_use_test.ts │ └── globals │ │ ├── document_test.ts │ │ ├── dom_parser_test.ts │ │ ├── fetch_test.ts │ │ ├── global_test.ts │ │ ├── range_test.ts │ │ ├── url_test.ts │ │ ├── window_test.ts │ │ └── worker_test.ts ├── internals │ ├── impl_test.ts │ ├── secrets_test.ts │ ├── string_literal_test.ts │ └── trusted_types_test.ts ├── restricted │ ├── legacy_test.ts │ └── reviewed_test.ts └── testing │ ├── conversions.ts │ ├── internal │ ├── xss_detector.ts │ └── xss_detector_test.ts │ └── testvectors │ ├── attribute_contracts_test_vectors.ts │ ├── javascript_url_sanitizer_test_vectors.ts │ └── url_test_vectors.ts ├── tsconfig-base.json ├── tsconfig-cjs.json ├── tsconfig.json └── yarn.lock /.github/workflows/run_unit_tests.yml: -------------------------------------------------------------------------------- 1 | name: run-unit-tests 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | paths: 7 | - 'src/**' 8 | - 'test/**' 9 | - 'integration_tests/**' 10 | jobs: 11 | run_unit_tests: 12 | runs-on: ubuntu-latest 13 | timeout-minutes: 5 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: actions/setup-node@v3 17 | with: 18 | node-version: ~20.9.0 19 | - run: | 20 | yarn install --frozen-lockfile 21 | yarn test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /dist 3 | /node_modules 4 | /yarn.lock 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.2.0][1.2.0] - 2025-02-13 4 | 5 | ### Changed 6 | 7 | - Support `application/octet-stream` as a MIME type in objectUrlFromSafeSource 8 | - Support certain font MIME types in objectUrlFromSafeSource 9 | - Support certain font MIME types in objectUrlFromSafeSource 10 | - Support additional image and audio MIME type formats in 11 | objectUrlFromSafeSource 12 | 13 | ## [1.1.0][1.1.0] - 2025-02-06 14 | 15 | ### Added 16 | 17 | - Implement `setElementAttribute` and document it 18 | - Add a `.withOpenShadow` `CssSanitizerBuilder` option 19 | - Add "controlslist" to the list of globally permitted attributes 20 | - Add a CHANGELOG.md that follows https://common-changelog.org/ 21 | 22 | ### Changed 23 | 24 | - Downgrade the global attribute contracts for "cite" and "poster" attributes 25 | in the sanitizer 26 | - Extend `allowDataAttributes` from the `HtmlSanitizerBuilder` to allow any 27 | `data-*` attributes 28 | 29 | ## [1.0.1][1.0.1] - 2025-01-03 30 | 31 | *Initial release.* 32 | 33 | [1.2.0]: https://github.com/google/safevalues/releases/tag/v1.2.0 34 | [1.1.0]: https://github.com/google/safevalues/releases/tag/v1.1.0 35 | [1.0.1]: https://github.com/google/safevalues/releases/tag/v1.0.1 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # safevalues 2 | 3 | Safevalues is a library to help you prevent Cross-Site Scripting vulnerabilities 4 | in TypeScript (and JavaScript). It is meant to be used together with 5 | [safety-web](https://github.com/google/safety-web) to provide strong security 6 | guarantees and help you deploy 7 | [TrustedTypes](https://w3c.github.io/trusted-types/dist/spec/) and 8 | other CSP restrictions in your applications. Google has used these components 9 | together to reduce DOM XSS ([paper](https://research.google/pubs/pub49950/)), 10 | and we hope it will be useful in your codebase. 11 | 12 | ## Features 13 | 14 | ### Policy definition for building safe-by-construction Trusted Types 15 | 16 | Trusted Types is a browser API that enables developers to control the values 17 | that can be assigned to XSS sinks. Developers need to define a Trusted Types 18 | policy to build these values, and then the Trusted Types API constraints these 19 | policies. 20 | 21 | The Trusted Types API is not opinionated on what *should be* considered safe. It 22 | only acts as a tool for developers to mark values they can *trust*. 23 | 24 | `safevalues` in contrast, defines functions that make security decisions on what 25 | is safe (by construction, via escaping or sanitization), so that developers who 26 | are not security experts don't need to. 27 | 28 | `safevalues` produces Trusted Types (through its own policy) when available. 29 | 30 | ### Additional types and functions for sinks not covered by Trusted Types 31 | 32 | Some DOM APIs are not covered by Trusted Types, but can also be abused; leading 33 | to XSS or other security issues. Alternative security mechanisms such as the 34 | `unsafe-inline` CSP protection can help to secure these APIs, but not all 35 | browsers or apps support them. 36 | 37 | `safevalues` defines additional types, builders, and setters to help protect 38 | these sinks. 39 | 40 | ### DOM sink wrappers 41 | 42 | To build a Trusted Types-compatible app and surface potential violations at 43 | compile time, we recommend that you lint your code with 44 | [safety-web](https://github.com/google/safety-web). safety-web bans certain DOM 45 | APIs. `safevalues` defines wrappers around these APIs which lets you assign 46 | Trusted Types with them. 47 | 48 | Some wrappers don't require a particular type, but sanitize the argument they 49 | get before they assign it to the DOM sink (e.g. `setLocationHref` from 50 | `safevalues/dom`). 51 | 52 | ### Trusted Types polyfills 53 | 54 | Whenever possible, `safevalues` uses Trusted Types to build its values, in order 55 | to benefit from the runtime protection of Trusted Types. When Trusted Types is 56 | not available, `safevalues` transparently defines its own types and your app 57 | will continue to work. 58 | 59 | ## Known issues 60 | 61 | ### ReferenceError: Can't find variable: process 62 | 63 | When using a bundler that performs dead-code elimination, you must ensure that 64 | `process.env.NODE_ENV` is declared globally with either a value of `development` 65 | or `production`. This is done in Webpack by 66 | [specifying a mode](https://webpack.js.org/guides/production/#specify-the-mode), 67 | in Terser using the 68 | [--define flag](https://webpack.js.org/guides/production/#specify-the-mode) and 69 | in Rollup using the 70 | [rollup-plugin-define plugin](https://www.npmjs.com/package/rollup-plugin-define#usage). 71 | See ([#212](https://github.com/google/safevalues/issues/212)). 72 | 73 | -------------------------------------------------------------------------------- 74 | 75 | [Read on](https://github.com/google/safevalues/tree/main/src) for more 76 | information on our APIs. 77 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.2.0 -------------------------------------------------------------------------------- /docs/code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Google Open Source Community Guidelines 2 | 3 | At Google, we recognize and celebrate the creativity and collaboration of open 4 | source contributors and the diversity of skills, experiences, cultures, and 5 | opinions they bring to the projects and communities they participate in. 6 | 7 | Every one of Google's open source projects and communities are inclusive 8 | environments, based on treating all individuals respectfully, regardless of 9 | gender identity and expression, sexual orientation, disabilities, 10 | neurodiversity, physical appearance, body size, ethnicity, nationality, race, 11 | age, religion, or similar personal characteristic. 12 | 13 | We value diverse opinions, but we value respectful behavior more. 14 | 15 | Respectful behavior includes: 16 | 17 | * Being considerate, kind, constructive, and helpful. 18 | * Not engaging in demeaning, discriminatory, harassing, hateful, sexualized, 19 | or physically threatening behavior, speech, and imagery. 20 | * Not engaging in unwanted physical contact. 21 | 22 | Some Google open source projects [may adopt][] an explicit project code of 23 | conduct, which may have additional detailed expectations for participants. Most 24 | of those projects will use our [modified Contributor Covenant][]. 25 | 26 | [may adopt]: https://opensource.google/docs/releasing/preparing/#conduct 27 | [modified Contributor Covenant]: https://opensource.google/docs/releasing/template/CODE_OF_CONDUCT/ 28 | 29 | ## Resolve peacefully 30 | 31 | We do not believe that all conflict is necessarily bad; healthy debate and 32 | disagreement often yields positive results. However, it is never okay to be 33 | disrespectful. 34 | 35 | If you see someone behaving disrespectfully, you are encouraged to address the 36 | behavior directly with those involved. Many issues can be resolved quickly and 37 | easily, and this gives people more control over the outcome of their dispute. If 38 | you are unable to resolve the matter for any reason, or if the behavior is 39 | threatening or harassing, report it. We are dedicated to providing an 40 | environment where participants feel welcome and safe. 41 | 42 | ## Reporting problems 43 | 44 | Some Google open source projects may adopt a project-specific code of conduct. 45 | In those cases, a Google employee will be identified as the Project Steward, who 46 | will receive and handle reports of code of conduct violations. In the event that 47 | a project hasn’t identified a Project Steward, you can report problems by 48 | emailing opensource@google.com. 49 | 50 | We will investigate every complaint, but you may not receive a direct response. 51 | We will use our discretion in determining when and how to follow up on reported 52 | incidents, which may range from not taking action to permanent expulsion from 53 | the project and project-sponsored spaces. We will notify the accused of the 54 | report and provide them an opportunity to discuss it before any action is taken. 55 | The identity of the reporter will be omitted from the details of the report 56 | supplied to the accused. In potentially harmful situations, such as ongoing 57 | harassment or threats to anyone's safety, we may take action without notice. 58 | 59 | *This document was adapted from the [IndieWeb Code of Conduct][] and can also be 60 | found at .* 61 | 62 | [IndieWeb Code of Conduct]: https://indieweb.org/code-of-conduct 63 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | This project doesn't accept external contributions yet. 4 | -------------------------------------------------------------------------------- /docs/developing.md: -------------------------------------------------------------------------------- 1 | # Development notes 2 | 3 | ## Stripping out development code 4 | 5 | Error messages and development-time checks are stripped from production binaries 6 | to keep the code size footprint of safevalues small. This is achieved by using 7 | the `process.env.NODE_ENV !== 'production'` compile-time constant from the `environment.ts` module. To 8 | increase readability, the `process.env.NODE_ENV !== 'production'` constant should always be used in a 9 | dedicated `if` statement without other conditions. 10 | 11 | ### Error messages 12 | 13 | Error messages should only be present in development mode. Use an `if` statement 14 | to achieve this as follows: 15 | 16 | ```typescript 17 | import {process.env.NODE_ENV !== 'production'} from './environment'; 18 | 19 | let message = ''; 20 | if (process.env.NODE_ENV !== 'production') { 21 | message = 'Verbose error message'; 22 | } 23 | throw new Error(message); 24 | ``` 25 | 26 | ### Development-time checks 27 | 28 | Security and development checks that only involve compile-time assertions or 29 | values that have no downstream effects should only be present in development 30 | mode. Use an `if` statement to achieve this as follows: 31 | 32 | ```typescript 33 | import {process.env.NODE_ENV !== 'production'} from './environment'; 34 | 35 | if (process.env.NODE_ENV !== 'production') { 36 | assertIsTemplateObject(input); 37 | } 38 | ``` 39 | 40 | Place the `if` statement as close to the public API surface as possible to 41 | maximize the amount of code that is stripped out. 42 | -------------------------------------------------------------------------------- /dom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dom", 3 | "description": "Safe DOM API wrappers", 4 | "license": "Apache-2.0", 5 | "main": "../dist/cjs/dom/index.js", 6 | "module": "../dist/mjs/dom/index.js", 7 | "types": "../dist/mjs/dom/index.d.ts" 8 | } 9 | -------------------------------------------------------------------------------- /fixup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Adds package.json files to cjs/mjs subtrees 3 | 4 | VERSION=$(cat VERSION) 5 | 6 | echo "{ 7 | \"type\": \"commonjs\", 8 | \"version\": \"${VERSION}\" 9 | }" > dist/cjs/package.json 10 | 11 | echo "{ 12 | \"type\": \"module\", 13 | \"version\": \"${VERSION}\" 14 | }" > dist/mjs/package.json 15 | 16 | rm -rf dist/mjs/test 17 | mv dist/mjs/src/* dist/mjs 18 | rmdir dist/mjs/src 19 | 20 | rm -rf dist/cjs/test 21 | mv dist/cjs/src/* dist/cjs 22 | rmdir dist/cjs/src 23 | 24 | # Copy back the manual .d.ts files that tsc doesn't put in dist (https://stackoverflow.com/questions/56018167/typescript-does-not-copy-d-ts-files-to-build) 25 | cp src/internals/trusted_types_typings.d.ts dist/cjs/internals/ 26 | cp src/internals/trusted_types_typings.d.ts dist/mjs/internals/ 27 | -------------------------------------------------------------------------------- /integration_tests/basic_import/README.md: -------------------------------------------------------------------------------- 1 | # For tests only 2 | 3 | Dummy project that depends on safevalues to test that the npm packaging works. 4 | -------------------------------------------------------------------------------- /integration_tests/basic_import/karma.conf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | module.exports = (config) => { 8 | config.set({ 9 | frameworks: ['jasmine', 'karma-typescript'], 10 | plugins: [ 11 | 'karma-jasmine', 12 | 'karma-chrome-launcher', 13 | 'karma-firefox-launcher', 14 | 'karma-typescript', 15 | 'karma-spec-reporter', 16 | ], 17 | karmaTypescriptConfig: { 18 | tsconfig: './tsconfig.json', 19 | compilerOptions: { 20 | module: 'commonjs', 21 | }, 22 | bundlerOptions: { 23 | transforms: [ 24 | require("karma-typescript-es6-transform")() 25 | ] 26 | } 27 | }, 28 | files: [ 29 | {pattern: 'main.ts'}, 30 | ], 31 | preprocessors: { 32 | '**/*.ts': 'karma-typescript', 33 | }, 34 | reporters: ['spec', 'karma-typescript'], 35 | colors: true, 36 | logLevel: config.LOG_INFO, 37 | autoWatch: true, 38 | browsers: ['Chrome', 'Firefox'], 39 | singleRun: false, 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /integration_tests/basic_import/main.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import {htmlEscape} from 'safevalues'; 8 | import {setElementInnerHtml} from 'safevalues/dom'; 9 | import { 10 | htmlSafeByReview, 11 | styleSheetSafeByReview, 12 | } from 'safevalues/restricted/reviewed'; 13 | 14 | function doSomething() { 15 | const html = htmlEscape('hello '); 16 | console.log(html.toString()); 17 | return 'alright'; 18 | } 19 | 20 | describe('doSomething', () => { 21 | it('works', () => { 22 | const status = doSomething(); 23 | expect(status).toEqual('alright'); 24 | }); 25 | }); 26 | 27 | describe('safevalues/restricted/reviewed', () => { 28 | it('can be referenced', () => { 29 | expect( 30 | styleSheetSafeByReview('hello', {justification: 'test'}).toString(), 31 | ).toEqual('hello'); 32 | }); 33 | }); 34 | 35 | describe('safevalues/dom', () => { 36 | it('can be referenced', () => { 37 | const e = document.createElement('div'); 38 | setElementInnerHtml( 39 | e, 40 | htmlSafeByReview('

hello

', {justification: 'test'}), 41 | ); 42 | expect(e.innerHTML).toEqual('

hello

'); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /integration_tests/basic_import/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "integration_test", 3 | "version": "0.1.8", 4 | "description": "NPM module integration tests", 5 | "repository": "https://github.com/google/safevalues", 6 | "author": "ISE Hardening", 7 | "license": "Apache-2.0", 8 | "publishConfig": { 9 | "registry": "https://wombat-dressing-room.appspot.com" 10 | }, 11 | "main": "index.js", 12 | "types": "index.d.ts", 13 | "scripts": { 14 | "build": "yarn && tsc", 15 | "ibuild": "yarn && tsc --watch", 16 | "test": "yarn build && karma start --browsers ChromeHeadless,FirefoxHeadless --single-run", 17 | "itest": "yarn build && karma start --browsers ChromeHeadless,FirefoxHeadless" 18 | }, 19 | "devDependencies": { 20 | "@types/jasmine": "^3.6.2", 21 | "@types/node": "*", 22 | "@types/trusted-types": "^1.0.6", 23 | "jasmine-core": "^3.6.0", 24 | "karma": "^6.3.17", 25 | "karma-chrome-launcher": "^3.1.0", 26 | "karma-firefox-launcher": "^2.1.2", 27 | "karma-jasmine": "^4.0.1", 28 | "karma-spec-reporter": "^0.0.32", 29 | "karma-typescript": "^5.2.0", 30 | "typescript": "^4.1.2", 31 | "karma-typescript-es6-transform": "*" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /integration_tests/basic_import/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "es2015", 5 | "lib": [ 6 | "es2017", 7 | "dom" 8 | ], 9 | "types": [ 10 | "jasmine", 11 | "node", 12 | "trusted-types" 13 | ], 14 | "declaration": true, 15 | "declarationMap": true, 16 | "sourceMap": true, 17 | "inlineSources": true, 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noImplicitReturns": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "noImplicitAny": true, 24 | "noImplicitThis": true, 25 | "moduleResolution": "node", 26 | "outDir": "dist/" 27 | }, 28 | "include": [ 29 | "main.ts", 30 | ], 31 | "exclude": [ 32 | "dist/", 33 | "node_modules/" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /integration_tests/import_fully_specified_webpack/README.md: -------------------------------------------------------------------------------- 1 | # For tests only 2 | 3 | Fake project that depends on safevalues and uses settings that require all files 4 | (including dependencies) to use full file extension when they import by path. 5 | This constraint is increasingly common in OSS. Webpack requires this by default. 6 | Other popular tools like create-react-app built on it also do. 7 | 8 | Examples: 9 | 10 | - 11 | - 12 | -------------------------------------------------------------------------------- /integration_tests/import_fully_specified_webpack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test_import_fully_specified_webpack", 3 | "version": "0.1.0", 4 | "description": "Webpack setup that requires a fully specified import with .js extensions", 5 | "license": "Apache-2.0", 6 | "scripts": { 7 | "build": "webpack --mode=development" 8 | }, 9 | "devDependencies": { 10 | "webpack": "^5.92.1", 11 | "webpack-cli": "^5.1.4" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /integration_tests/import_fully_specified_webpack/src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import {sanitizeHtml} from 'safevalues'; 8 | 9 | console.log(sanitizeHtml('hello world').toString()); 10 | -------------------------------------------------------------------------------- /integration_tests/jest/babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "plugins": ["@babel/plugin-transform-modules-commonjs"] 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /integration_tests/jest/jest.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | module.exports = { 8 | preset: 'ts-jest', 9 | testEnvironment: 'jsdom', 10 | transform: { 11 | '^.+\\.tsx?$': 'ts-jest', 12 | '^.+\\.js$': 'babel-jest' 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /integration_tests/jest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "safevalues-import", 3 | "version": "1.0.0", 4 | "description": "test only project set up with Jest", 5 | "license": "Apache-2.0", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "jest" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/danrevah/safevalues-import.git" 13 | }, 14 | "author": "", 15 | "bugs": { 16 | "url": "https://github.com/danrevah/safevalues-import/issues" 17 | }, 18 | "homepage": "https://github.com/danrevah/safevalues-import#readme", 19 | "devDependencies": { 20 | "@babel/plugin-transform-modules-commonjs": "^7.15.0", 21 | "@types/node": "^16.7.1", 22 | "@types/trusted-types": "^2.0.2", 23 | "jest-environment-jsdom-fifteen": "^1.0.2", 24 | "jest-environment-jsdom-sixteen": "^2.0.0" 25 | }, 26 | "dependencies": { 27 | "@types/jest": "^27.0.0", 28 | "babel-jest": "^27.0.6", 29 | "jest": "^27.0.0", 30 | "ts-jest": "^27.0.0", 31 | "typescript": "^3.9.10" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /integration_tests/jest/src/index.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import {foo} from './index'; 8 | 9 | describe('#Foo', () => { 10 | it('Bar', () => { 11 | expect(foo().toString()).toEqual(''); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /integration_tests/jest/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import {SafeHtml} from 'safevalues'; 8 | import {htmlSafeByReview} from 'safevalues/restricted/reviewed'; 9 | 10 | export function foo(): SafeHtml { 11 | return htmlSafeByReview('', {justification: 'Jest demo'}); 12 | } 13 | -------------------------------------------------------------------------------- /integration_tests/jest/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "sourceMap": true, 7 | "allowJs": true, 8 | "strict": true, 9 | "incremental": true, 10 | "resolveJsonModule": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | module.exports = (config) => { 8 | config.set({ 9 | frameworks: ['jasmine', 'karma-typescript'], 10 | plugins: [ 11 | 'karma-jasmine', 12 | 'karma-chrome-launcher', 13 | 'karma-firefox-launcher', 14 | 'karma-typescript', 15 | 'karma-spec-reporter', 16 | ], 17 | karmaTypescriptConfig: { 18 | tsconfig: './tsconfig.json', 19 | compilerOptions: { 20 | module: 'commonjs', 21 | }, 22 | }, 23 | files: [ 24 | {pattern: 'src/**/*.ts'}, 25 | {pattern: 'test/**/*.ts'}, 26 | ], 27 | preprocessors: { 28 | '**/*.ts': 'karma-typescript', 29 | }, 30 | reporters: ['spec', 'karma-typescript'], 31 | colors: true, 32 | logLevel: config.LOG_INFO, 33 | autoWatch: true, 34 | browsers: ['Chrome', 'Firefox'], 35 | singleRun: false, 36 | client: { 37 | jasmine: { 38 | failSpecWithNoExpectations: true, 39 | }, 40 | }, 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "safevalues", 3 | "version": "1.2.0", 4 | "description": "Safe builders for Trusted Types values", 5 | "repository": "https://github.com/google/safevalues", 6 | "author": "ISE Web Hardening Team", 7 | "license": "Apache-2.0", 8 | "publishConfig":{ 9 | "registry":"https://wombat-dressing-room.appspot.com" 10 | }, 11 | "main": "dist/cjs/index.js", 12 | "module": "dist/mjs/index.js", 13 | "types": "dist/mjs/index.d.ts", 14 | "exports": { 15 | ".": { 16 | "import": "./dist/mjs/index.js", 17 | "require": "./dist/cjs/index.js", 18 | "types": "./dist/mjs/index.d.ts" 19 | }, 20 | "./restricted/*": { 21 | "import": "./dist/mjs/restricted/*.js", 22 | "require": "./dist/cjs/restricted/*.js", 23 | "types": "./dist/mjs/restricted/*.d.ts" 24 | }, 25 | "./dom": { 26 | "import": "./dist/mjs/dom/index.js", 27 | "require": "./dist/cjs/dom/index.js", 28 | "types": "./dist/mjs/dom/index.d.ts" 29 | } 30 | }, 31 | "sideEffects": false, 32 | "files": [ 33 | "dist/", 34 | "dom/", 35 | "restricted/" 36 | ], 37 | "scripts": { 38 | "clean": "rm -rf ./dist/*", 39 | "build": "yarn clean && yarn build:esm && yarn build:cjs && yarn build:fixup", 40 | "build:esm": "yarn && tsc -p ./tsconfig.json", 41 | "build:cjs": "yarn && tsc -p ./tsconfig-cjs.json", 42 | "build:fixup": "./fixup.sh", 43 | "test": "yarn build && karma start --browsers ChromeHeadless,FirefoxHeadless --single-run", 44 | "itest": "yarn build && karma start --browsers ChromeHeadless,FirefoxHeadless", 45 | "prepack": "yarn test" 46 | }, 47 | "devDependencies": { 48 | "@types/jasmine": "^3.6.2", 49 | "@types/node": "*", 50 | "jasmine-core": "^3.6.0", 51 | "karma": "^6.3.17", 52 | "karma-chrome-launcher": "^3.1.0", 53 | "karma-firefox-launcher": "^2.1.2", 54 | "karma-jasmine": "^4.0.1", 55 | "karma-spec-reporter": "^0.0.32", 56 | "karma-typescript": "^5.2.0", 57 | "typescript": "^4.1.2" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /restricted/legacy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "restricted/legacy", 3 | "description": "Restricted APIs, used to mark legacy usages of unsafe APIs", 4 | "license": "Apache-2.0", 5 | "main": "../../dist/cjs/restricted/legacy.js", 6 | "module": "../../dist/mjs/restricted/legacy.js", 7 | "types": "../../dist/mjs/restricted/legacy.d.ts" 8 | } -------------------------------------------------------------------------------- /restricted/reviewed/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "restricted/reviewed", 3 | "description": "Restricted APIs, used to mark reviewed usages of unsafe APIs for which no safe equivalent exists.", 4 | "license": "Apache-2.0", 5 | "main": "../../dist/cjs/restricted/reviewed.js", 6 | "module": "../../dist/mjs/restricted/reviewed.js", 7 | "types": "../../dist/mjs/restricted/reviewed.d.ts" 8 | } -------------------------------------------------------------------------------- /run_integration.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | # Remove any stale version of safevalues from the cache 5 | yarn cache clean --all 6 | 7 | yarn 8 | yarn build 9 | yarn test 10 | # Packs safevalues using a local build 11 | yarn pack --filename safevalues.local.tgz 12 | 13 | # Use the local version of safevalues to run integration tests 14 | (cd integration_tests/basic_import/ && yarn add ../../safevalues.local.tgz && yarn test) 15 | (cd integration_tests/import_fully_specified_webpack && yarn add ../../safevalues.local.tgz && yarn build) 16 | (cd integration_tests/jest/ && yarn add ../../safevalues.local.tgz && yarn test) 17 | -------------------------------------------------------------------------------- /src/builders/attribute_builders.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import '../environment/dev.js'; 8 | import { 9 | createAttributePrefixInternal, 10 | SafeAttributePrefix, 11 | } from '../internals/attribute_impl.js'; 12 | import {assertIsTemplateObject} from '../internals/string_literal.js'; 13 | 14 | import {SECURITY_SENSITIVE_ATTRIBUTES} from './sensitive_attributes.js'; 15 | 16 | /** 17 | * Creates a SafeAttributePrefix object from a template literal with no 18 | * interpolations for attributes that share a common prefix guaranteed to be not 19 | * security sensitive. 20 | * 21 | * The template literal is a prefix that makes it obvious this attribute is not 22 | * security sensitive. If it doesn't, this function will throw. 23 | */ 24 | export function safeAttrPrefix( 25 | templ: TemplateStringsArray, 26 | ): SafeAttributePrefix { 27 | if (process.env.NODE_ENV !== 'production') { 28 | assertIsTemplateObject(templ, 0); 29 | } 30 | 31 | const attrPrefix = templ[0].toLowerCase(); 32 | 33 | if (process.env.NODE_ENV !== 'production') { 34 | if (attrPrefix.indexOf('on') === 0 || 'on'.indexOf(attrPrefix) === 0) { 35 | throw new Error( 36 | `Prefix '${templ[0]}' does not guarantee the attribute ` + 37 | `to be safe as it is also a prefix for event handler attributes` + 38 | `Please use 'addEventListener' to set event handlers.`, 39 | ); 40 | } 41 | 42 | SECURITY_SENSITIVE_ATTRIBUTES.forEach((sensitiveAttr) => { 43 | if (sensitiveAttr.indexOf(attrPrefix) === 0) { 44 | throw new Error( 45 | `Prefix '${templ[0]}' does not guarantee the attribute ` + 46 | `to be safe as it is also a prefix for ` + 47 | `the security sensitive attribute '${sensitiveAttr}'. ` + 48 | `Please use native or safe DOM APIs to set the attribute.`, 49 | ); 50 | } 51 | }); 52 | } 53 | 54 | return createAttributePrefixInternal(attrPrefix); 55 | } 56 | -------------------------------------------------------------------------------- /src/builders/document_fragment_builders.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import '../environment/dev.js'; 8 | import { 9 | createHtmlInternal, 10 | SafeHtml, 11 | unwrapHtml, 12 | } from '../internals/html_impl.js'; 13 | import {assertIsTemplateObject} from '../internals/string_literal.js'; 14 | 15 | /** 16 | * Creates a DocumentFragment object from a template literal (without any 17 | * embedded expressions) using the document context (HTML). 18 | * 19 | * Note: use svgFragment instead to create a DocumentFragment belonging to the 20 | * SVG namespace. 21 | * 22 | * This function is a template literal tag function. It should be called with 23 | * a template literal that does not contain any expressions. For example, 24 | * htmlFragment`foo`; 25 | * 26 | * @param templateObj This contains the literal part of the template literal. 27 | */ 28 | export function htmlFragment( 29 | templateObj: TemplateStringsArray, 30 | ): DocumentFragment { 31 | if (process.env.NODE_ENV !== 'production') { 32 | assertIsTemplateObject(templateObj, 0); 33 | } 34 | const range = document.createRange(); 35 | return range.createContextualFragment( 36 | unwrapHtml(createHtmlInternal(templateObj[0])) as string, 37 | ); 38 | } 39 | 40 | /** 41 | * Creates a DocumentFragment object from a template literal (without any 42 | * embedded expressions), with an SVG context. 43 | * 44 | * This function is a template literal tag function. It should be called with 45 | * a template literal that does not contain any expressions. For example, 46 | * svgFragment`foo`; 47 | * 48 | * @param templateObj This contains the literal part of the template literal. 49 | */ 50 | export function svgFragment( 51 | templateObj: TemplateStringsArray, 52 | ): DocumentFragment { 53 | if (process.env.NODE_ENV !== 'production') { 54 | assertIsTemplateObject(templateObj, 0); 55 | } 56 | const svgElem = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); 57 | const range = document.createRange(); 58 | range.selectNodeContents(svgElem); 59 | return range.createContextualFragment( 60 | unwrapHtml(createHtmlInternal(templateObj[0])) as string, 61 | ); 62 | } 63 | 64 | /** Converts HTML markup into a node. */ 65 | export function htmlToNode(html: SafeHtml): Node { 66 | const range = document.createRange(); 67 | const fragment = range.createContextualFragment(unwrapHtml(html) as string); 68 | if (fragment.childNodes.length === 1) { 69 | return fragment.childNodes[0]; 70 | } else { 71 | return fragment; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/builders/html_sanitizer/README.md: -------------------------------------------------------------------------------- 1 | # HTML Sanitizer 2 | 3 | ## Overview 4 | 5 | This directory contains the implementation of an HTML sanitizer which is 6 | compatible with the safevalues types (and by extension, with Trusted Types). 7 | This HTML sanitizer is configured with a policy that defines how HTML tags and 8 | attributes are sanitized. 9 | 10 | safevalues' `sanitizeHtml`, `sanitizeHtmlToFragment`, and 11 | `sanitizeHtmlAssertUnchanged` functions use an HTML sanitizer with a default 12 | base sanitization policy. 13 | 14 | The Sanitizer builder API lets you define Sanitizer instances that ban 15 | additional elements and attributes on top of the default policy. There is no 16 | builder available that lets you create arbitrarily looser policies than the 17 | default policy. At most, `style`, `id`, `data-*`, and `class` attributes can be 18 | allowed on top of the default policy. 19 | 20 | ## Sanitizer methods 21 | 22 | All sanitizer instances (default and custom built) expose 3 methods: 23 | 24 | * `sanitize(html: string): SafeHtml` sanitizes a string following the policy, 25 | returning `SafeHtml`. 26 | 27 | * `sanitizeToFragment(html: string): DocumentFragment` performs the same 28 | sanitization as `sanitize`, but returns a `DocumentFragment`. 29 | 30 | This method should be preferred over `sanitize` when the result is assigned 31 | to a Node DOM API like `Node.appendChild`. Doing so is more efficient than 32 | assigning `SafeHtml` to `Element.innerHTML` as it saves an HTML 33 | serialization/deserialization. 34 | 35 | * `sanitizeAssertUnchanged(html: string): SafeHtml` is similar to `sanitize` 36 | but throws if parts of the input is sanitized away. 37 | 38 | ## Default sanitizer usage 39 | 40 | ```typescript 41 | import {sanitizeHtml} from 'safevalues'; 42 | import {documentWrite} from 'safevalues/dom'; 43 | 44 | /** 45 | * Shows an HTML error snippet coming from an untrusted source. 46 | */ 47 | function showError(errorSnippet: string) { 48 | documentWrite(document, sanitizeHtml(`
Reported error:
${errorSnippet}
`)); 49 | } 50 | ``` 51 | 52 | ## Default policy 53 | 54 | The default policy defines allowed tags and attributes (sometimes conditioned to 55 | the tag they're used in). The policy is allowlist based. The policy is defined 56 | in accordance with the 57 | [sanitizer_table](https://github.com/google/safevalues/blob/main/src/builders/html_sanitizer/sanitizer_table/default_sanitizer_table.ts) 58 | declaration: 59 | 60 | A tag is allowed if and only if: 61 | 62 | * it's part of the 63 | [`allowedElements`](https://github.com/google/safevalues/blob/main/src/builders/html_sanitizer/sanitizer_table/default_sanitizer_table.ts#L17) 64 | 65 | OR 66 | 67 | * it has a key in the 68 | [`elementPolicies`](https://github.com/google/safevalues/blob/main/src/builders/html_sanitizer/sanitizer_table/default_sanitizer_table.ts#L35) 69 | 70 | An attribute is allowed if and only if: 71 | 72 | * it's 73 | [globally allowed](https://github.com/google/safevalues/blob/main/src/builders/html_sanitizer/sanitizer_table/default_sanitizer_table.ts#L98) 74 | or has a 75 | [global attribute policy](https://github.com/google/safevalues/blob/main/src/builders/html_sanitizer/sanitizer_table/default_sanitizer_table.ts#L199) 76 | 77 | OR 78 | 79 | * it's 80 | [allowed for the element](https://github.com/google/safevalues/blob/main/src/builders/html_sanitizer/sanitizer_table/default_sanitizer_table.ts#L35) 81 | being considered 82 | 83 | Attribute values are preserved, sanitized normalized or dropped following the 84 | policy (`AttributePolicyAction`). 85 | 86 | ## Sanitizer builder API 87 | 88 | The sanitizer builder API can be used to ban additional elements and attributes. 89 | Example: 90 | 91 | ```typescript 92 | // Only allows
elements 93 | const sanitizer = new HtmlSanitizerBuilder() 94 | .onlyAllowElements(new Set(['article'])) 95 | .build(); 96 | ``` 97 | 98 | If needed, `style`, `id`, `data-`, or `class` attributes can be allowed. 99 | Example: 100 | 101 | ```typescript 102 | // Allows some data-* attributes 103 | const sanitizer = new HtmlSanitizerBuilder() 104 | .allowDataAttributes(['data-foo', 'data-bar']) 105 | .build(); 106 | 107 | // Allow any data-* attributes 108 | const sanitizer = new HtmlSanitizerBuilder() 109 | .allowDataAttributes() 110 | .build(); 111 | ``` 112 | 113 | ```typescript 114 | // Allow class and id attributes 115 | const sanitizer = new HtmlSanitizerBuilder() 116 | .allowClassAttributes() 117 | .allowIdAttributes() 118 | .build(); 119 | ``` 120 | -------------------------------------------------------------------------------- /src/builders/html_sanitizer/css/css_isolation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | /** 8 | * @fileoverview Exports a stylesheet that isolates the sanitized content from 9 | * the rest of the page. 10 | * 11 | * One of the design goals of the CSS sanitizer is to ensure that the sanitized 12 | * content cannot affect the rest of the page. For example, the sanitized 13 | * content should not be able to cover other elements on the page, or to show up 14 | * outside of the sanitized container. 15 | * 16 | * This file exports a stylesheet that makes this design goal a reality. Check 17 | * out css_isolation_test.ts to see specific examples of what this stylesheet 18 | * attempts to prevent. 19 | */ 20 | 21 | /** 22 | * A set of CSS properties that isolate the sanitized content from the rest of 23 | * the page. 24 | * 25 | * * `display:inline-block`, `clip-path:inset(0)` and `overflow:hidden` - ensure 26 | * that the sanitized content cannot cover other elements on the page. 27 | * * `vertical-align:top` - fixes a quirk when `overflow:hidden` is used on 28 | * inline elements and they are not aligned correctly. See 29 | * https://stackoverflow.com/questions/30182800/css-overflowhidden-with-displayinline-block 30 | * for details. 31 | * * `text-decoration:inherit` - ensures that the sanitized content inherits 32 | * the text decoration from the parent element, which is not the default for 33 | * `display:inline-block`. See 34 | * https://www.w3.org/TR/2022/CRD-css-text-decor-3-20220505/#:~:text=Note%20that%20text%20decorations%20are%20not%20propagated%20to%20any%20out%2Dof%2Dflow%20descendants%2C%20nor%20to%20the%20contents%20of%20atomic%20inline%2Dlevel%20descendants%20such%20as%20inline%20blocks 35 | * for details. 36 | */ 37 | export const CSS_ISOLATION_PROPERTIES = 38 | 'display:inline-block;clip-path:inset(0);overflow:hidden;vertical-align:top;text-decoration:inherit'; 39 | 40 | /** 41 | * A stylesheet that isolates the sanitized content from the rest of the page. 42 | */ 43 | export const CSS_ISOLATION_STYLESHEET: ':host{display:inline-block;clip-path:inset(0);overflow:hidden;vertical-align:top;text-decoration:inherit}' = `:host{${CSS_ISOLATION_PROPERTIES}}`; 44 | -------------------------------------------------------------------------------- /src/builders/html_sanitizer/css/serializer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | /** 8 | * @fileoverview Exports methods for serializing CSS tokens. 9 | */ 10 | 11 | import {CssToken, CssTokenKind} from './tokens.js'; 12 | 13 | function escapeCodePoint(c: string): string { 14 | return `\\${c.codePointAt(0)!.toString(16)} `; 15 | } 16 | 17 | function escapeString(str: string): string { 18 | // We don't escape some characters to increase readability. 19 | return ( 20 | '"' + 21 | str.replace(/[^A-Za-z0-9_/. :,?=%;-]/g, (c) => escapeCodePoint(c)) + 22 | '"' 23 | ); 24 | } 25 | 26 | /** 27 | * Escapes a CSS identifier. 28 | * 29 | * @param ident The identifier to escape. 30 | * @return The escaped identifier. 31 | */ 32 | export function escapeIdent(ident: string): string { 33 | // We don't generally escape digits or "-" in identifiers, however we do need 34 | // to do this for the first character to avoid ambiguity. 35 | // 36 | // For example, the string "123" would create a valid number token, but if 37 | // we want to have an ident-token, it needs to be escaped as a "\31 23". 38 | const firstChar = /^[^A-Za-z_]/.test(ident) 39 | ? escapeCodePoint(ident[0]) 40 | : ident[0]; 41 | return ( 42 | firstChar + 43 | ident.slice(1).replace(/[^A-Za-z0-9_-]/g, (c) => escapeCodePoint(c)) 44 | ); 45 | } 46 | 47 | /** 48 | * Serializes a CSS token to a string. 49 | * 50 | * @param token The token to serialize. 51 | * @return The serialized token. 52 | */ 53 | export function serializeToken(token: CssToken): string { 54 | switch (token.tokenKind) { 55 | case CssTokenKind.AT_KEYWORD: 56 | return `@${escapeIdent(token.name)}`; 57 | case CssTokenKind.CDC: 58 | return '-->'; 59 | case CssTokenKind.CDO: 60 | return '