diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 3f47e2602..f6ce75eac 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -9,6 +9,7 @@ on: branches: - main - 3.x + - 2.x pull_request: jobs: @@ -17,7 +18,7 @@ jobs: strategy: matrix: - node-version: [16.x, 17.x, 18.x, 19.x] + node-version: [16.x, 17.x, 18.x, 19.x, 20.x, 21.x] steps: - name: Checkout @@ -38,7 +39,7 @@ jobs: with: run: npm run test:ci env: - TEST_BROWSERSTACK: ${{ startsWith(matrix.node-version, '19') }} - TEST_PROBE_ONLY: ${{ github.ref != 'refs/heads/main' }} + TEST_BROWSERSTACK: ${{ startsWith(matrix.node-version, '21') }} + TEST_PROBE_ONLY: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/2.x' }} BS_USERNAME: ${{ secrets.BS_USERNAME }} BS_ACCESSKEY: ${{ secrets.BS_ACCESSKEY }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 8931faebb..0d7d28d93 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -38,14 +38,14 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -59,4 +59,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/LICENSE b/LICENSE index a5423bd8d..aed61cbb2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ DOMPurify -Copyright 2023 Dr.-Ing. Mario Heiderich, Cure53 +Copyright 2024 Dr.-Ing. Mario Heiderich, Cure53 DOMPurify is free software; you can redistribute it and/or modify it under the terms of either: diff --git a/README.md b/README.md index 7f5234d8f..e8094265a 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,13 @@ DOMPurify is a DOM-only, super-fast, uber-tolerant XSS sanitizer for HTML, MathML and SVG. -It's also very simple to use and get started with. DOMPurify was [started in February 2014](https://github.com/cure53/DOMPurify/commit/a630922616927373485e0e787ab19e73e3691b2b) and, meanwhile, has reached version **v3.0.6**. +It's also very simple to use and get started with. DOMPurify was [started in February 2014](https://github.com/cure53/DOMPurify/commit/a630922616927373485e0e787ab19e73e3691b2b) and, meanwhile, has reached version **v3.2.0**. DOMPurify is written in JavaScript and works in all modern browsers (Safari (10+), Opera (15+), Edge, Firefox and Chrome - as well as almost anything else using Blink, Gecko or WebKit). It doesn't break on MSIE or other legacy browsers. It simply does nothing. -**Note that [DOMPurify v2.4.7](https://github.com/cure53/DOMPurify/releases/tag/2.4.6) is the latest version supporting MSIE. For important security updates compatible with MSIE, please use the [2.x branch](https://github.com/cure53/DOMPurify/tree/2.x).** +**Note that [DOMPurify v2.5.7](https://github.com/cure53/DOMPurify/releases/tag/2.5.7) is the latest version supporting MSIE. For important security updates compatible with MSIE, please use the [2.x branch](https://github.com/cure53/DOMPurify/tree/2.x).** -Our automated tests cover [19 different browsers](https://github.com/cure53/DOMPurify/blob/main/test/karma.custom-launchers.config.js#L5) right now, more to come. We also cover Node.js v16.x, v17.x, v18.x and v19.x, running DOMPurify on [jsdom](https://github.com/jsdom/jsdom). Older Node versions are known to work as well, but hey... no guarantees. +Our automated tests cover [24 different browsers](https://github.com/cure53/DOMPurify/blob/main/test/karma.custom-launchers.config.js#L5) right now, more to come. We also cover Node.js v16.x, v17.x, v18.x and v19.x, running DOMPurify on [jsdom](https://github.com/jsdom/jsdom). Older Node versions are known to work as well, but hey... no guarantees. DOMPurify is written by security people who have vast background in web attacks and XSS. Fear not. For more details please also read about our [Security Goals & Threat Model](https://github.com/cure53/DOMPurify/wiki/Security-Goals-&-Threat-Model). Please, read it. Like, really. @@ -45,7 +45,7 @@ const clean = DOMPurify.sanitize(dirty); Or maybe this, if you love working with Angular or alike: ```js -import * as DOMPurify from 'dompurify'; +import DOMPurify from 'dompurify'; const clean = DOMPurify.sanitize('hello there'); ``` @@ -57,10 +57,6 @@ Note that by default, we permit HTML, SVG **and** MathML. If you only need HTML, const clean = DOMPurify.sanitize(dirty, { USE_PROFILES: { html: true } }); ``` -### Where are the TypeScript type definitions? - -They can be found here: [@types/dompurify](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/dompurify) - ### Is there any foot-gun potential? Well, please note, if you _first_ sanitize HTML and then modify it _afterwards_, you might easily **void the effects of sanitization**. If you feed the sanitized markup to another library _after_ sanitization, please be certain that the library doesn't mess around with the HTML on its own. @@ -77,6 +73,8 @@ Running DOMPurify on the server requires a DOM to be present, which is probably Why? Because older versions of _jsdom_ are known to be buggy in ways that result in XSS _even if_ DOMPurify does everything 100% correctly. There are **known attack vectors** in, e.g. _jsdom v19.0.0_ that are fixed in _jsdom v20.0.0_ - and we really recommend to keep _jsdom_ up to date because of that. +Please also be aware that tools like [happy-dom](https://github.com/capricorn86/happy-dom) exist but **are not considered safe** at this point. Combining DOMPurify with _happy-dom_ is currently not recommended and will likely lead to XSS. + Other than that, you are fine to use DOMPurify on the server. Probably. This really depends on _jsdom_ or whatever DOM you utilize server-side. If you can live with that, this is how you get it to work: ```bash @@ -156,6 +154,15 @@ In version 2.0.0, a config flag was added to control DOMPurify's behavior regard When `DOMPurify.sanitize` is used in an environment where the Trusted Types API is available and `RETURN_TRUSTED_TYPE` is set to `true`, it tries to return a `TrustedHTML` value instead of a string (the behavior for `RETURN_DOM` and `RETURN_DOM_FRAGMENT` config options does not change). +Note that in order to create a policy in `trustedTypes` using DOMPurify, `RETURN_TRUSTED_TYPE: false` is required, as `createHTML` expects a normal string, not `TrustedHTML`. The example below shows this. + +```js +window.trustedTypes!.createPolicy('default', { + createHTML: (to_escape) => + DOMPurify.sanitize(to_escape, { RETURN_TRUSTED_TYPE: false }), +}); +``` + ## Can I configure DOMPurify? Yes. The included default configuration values are pretty good already - but you can of course override them. Check out the [`/demos`](https://github.com/cure53/DOMPurify/tree/main/demos) folder to see a bunch of examples on how you can [customize DOMPurify](https://github.com/cure53/DOMPurify/tree/main/demos#what-is-this). @@ -167,6 +174,13 @@ Yes. The included default configuration values are pretty good already - but you // allowing template parsing in user-controlled HTML is not advised at all. // only use this mode if there is really no alternative. const clean = DOMPurify.sanitize(dirty, {SAFE_FOR_TEMPLATES: true}); + + +// change how e.g. comments containing risky HTML characters are treated. +// be very careful, this setting should only be set to `false` if you really only handle +// HTML and nothing else, no SVG, MathML or the like. +// Otherwise, changing from `true` to `false` will lead to XSS in this or some other way. +const clean = DOMPurify.sanitize(dirty, {SAFE_FOR_XML: false}); ``` ### Control our allow-lists and block-lists @@ -354,15 +368,21 @@ _Example_: ```js DOMPurify.addHook( - 'beforeSanitizeElements', + 'uponSanitizeAttribute', function (currentNode, hookEvent, config) { - // Do something with the current node and return it - // You can also mutate hookEvent (i.e. set hookEvent.forceKeepAttr = true) - return currentNode; + // Do something with the current node + // You can also mutate hookEvent for current node (i.e. set hookEvent.forceKeepAttr = true) + // For other than 'uponSanitizeAttribute' hook types hookEvent equals to null } ); ``` +## Removed Configuration + +| Option | Since | Note | +|-----------------|-------|--------------------------| +| SAFE_FOR_JQUERY | 2.1.0 | No replacement required. | + ## Continuous Integration We are currently using Github Actions in combination with BrowserStack. This gives us the possibility to confirm for each and every commit that all is going according to plan in all supported browsers. Check out the build logs here: https://github.com/cure53/DOMPurify/actions @@ -409,10 +429,10 @@ Feature releases will not be announced to this list. Many people helped and help DOMPurify become what it is and need to be acknowledged here! -[dcramer 💸](https://github.com/dcramer), [JGraph 💸](https://github.com/jgraph), [baekilda 💸](https://github.com/baekilda), [Healthchecks 💸](https://github.com/healthchecks), [Sentry 💸](https://github.com/getsentry), [jarrodldavis 💸](https://github.com/jarrodldavis), [CynegeticIO](https://github.com/CynegeticIO), [ssi02014 ❤ī¸](https://github.com/ssi02014), [kevin_mizu](https://twitter.com/kevin_mizu), [GrantGryczan](https://github.com/GrantGryczan), [Lowdefy](https://twitter.com/lowdefy), [granlem](https://twitter.com/MaximeVeit), [oreoshake](https://github.com/oreoshake), [tdeekens ❤ī¸](https://github.com/tdeekens), [peernohell ❤ī¸](https://github.com/peernohell), [is2ei](https://github.com/is2ei), [SoheilKhodayari](https://github.com/SoheilKhodayari), [franktopel](https://github.com/franktopel), [NateScarlet](https://github.com/NateScarlet), [neilj](https://github.com/neilj), [fhemberger](https://github.com/fhemberger), [Joris-van-der-Wel](https://github.com/Joris-van-der-Wel), [ydaniv](https://github.com/ydaniv), [terjanq](https://twitter.com/terjanq), [filedescriptor](https://github.com/filedescriptor), [ConradIrwin](https://github.com/ConradIrwin), [gibson042](https://github.com/gibson042), [choumx](https://github.com/choumx), [0xSobky](https://github.com/0xSobky), [styfle](https://github.com/styfle), [koto](https://github.com/koto), [tlau88](https://github.com/tlau88), [strugee](https://github.com/strugee), [oparoz](https://github.com/oparoz), [mathiasbynens](https://github.com/mathiasbynens), [edg2s](https://github.com/edg2s), [dnkolegov](https://github.com/dnkolegov), [dhardtke](https://github.com/dhardtke), [wirehead](https://github.com/wirehead), [thorn0](https://github.com/thorn0), [styu](https://github.com/styu), [mozfreddyb](https://github.com/mozfreddyb), [mikesamuel](https://github.com/mikesamuel), [jorangreef](https://github.com/jorangreef), [jimmyhchan](https://github.com/jimmyhchan), [jameydeorio](https://github.com/jameydeorio), [jameskraus](https://github.com/jameskraus), [hyderali](https://github.com/hyderali), [hansottowirtz](https://github.com/hansottowirtz), [hackvertor](https://github.com/hackvertor), [freddyb](https://github.com/freddyb), [flavorjones](https://github.com/flavorjones), [djfarrelly](https://github.com/djfarrelly), [devd](https://github.com/devd), [camerondunford](https://github.com/camerondunford), [buu700](https://github.com/buu700), [buildog](https://github.com/buildog), [alabiaga](https://github.com/alabiaga), [Vector919](https://github.com/Vector919), [Robbert](https://github.com/Robbert), [GreLI](https://github.com/GreLI), [FuzzySockets](https://github.com/FuzzySockets), [ArtemBernatskyy](https://github.com/ArtemBernatskyy), [@garethheyes](https://twitter.com/garethheyes), [@shafigullin](https://twitter.com/shafigullin), [@mmrupp](https://twitter.com/mmrupp), [@irsdl](https://twitter.com/irsdl),[ShikariSenpai](https://github.com/ShikariSenpai), [ansjdnakjdnajkd](https://github.com/ansjdnakjdnajkd), [@asutherland](https://twitter.com/asutherland), [@mathias](https://twitter.com/mathias), [@cgvwzq](https://twitter.com/cgvwzq), [@robbertatwork](https://twitter.com/robbertatwork), [@giutro](https://twitter.com/giutro), [@CmdEngineer\_](https://twitter.com/CmdEngineer_), [@avr4mit](https://twitter.com/avr4mit) and especially [@securitymb ❤ī¸](https://twitter.com/securitymb) & [@masatokinugawa ❤ī¸](https://twitter.com/masatokinugawa) +[hash_kitten ❤ī¸](https://twitter.com/hash_kitten), [kevin_mizu ❤ī¸](https://twitter.com/kevin_mizu), [icesfont ❤ī¸](https://github.com/icesfont) [dcramer 💸](https://github.com/dcramer), [JGraph 💸](https://github.com/jgraph), [baekilda 💸](https://github.com/baekilda), [Healthchecks 💸](https://github.com/healthchecks), [Sentry 💸](https://github.com/getsentry), [jarrodldavis 💸](https://github.com/jarrodldavis), [CynegeticIO](https://github.com/CynegeticIO), [ssi02014 ❤ī¸](https://github.com/ssi02014), [GrantGryczan](https://github.com/GrantGryczan), [Lowdefy](https://twitter.com/lowdefy), [granlem](https://twitter.com/MaximeVeit), [oreoshake](https://github.com/oreoshake), [tdeekens ❤ī¸](https://github.com/tdeekens), [peernohell ❤ī¸](https://github.com/peernohell), [is2ei](https://github.com/is2ei), [SoheilKhodayari](https://github.com/SoheilKhodayari), [franktopel](https://github.com/franktopel), [NateScarlet](https://github.com/NateScarlet), [neilj](https://github.com/neilj), [fhemberger](https://github.com/fhemberger), [Joris-van-der-Wel](https://github.com/Joris-van-der-Wel), [ydaniv](https://github.com/ydaniv), [terjanq](https://twitter.com/terjanq), [filedescriptor](https://github.com/filedescriptor), [ConradIrwin](https://github.com/ConradIrwin), [gibson042](https://github.com/gibson042), [choumx](https://github.com/choumx), [0xSobky](https://github.com/0xSobky), [styfle](https://github.com/styfle), [koto](https://github.com/koto), [tlau88](https://github.com/tlau88), [strugee](https://github.com/strugee), [oparoz](https://github.com/oparoz), [mathiasbynens](https://github.com/mathiasbynens), [edg2s](https://github.com/edg2s), [dnkolegov](https://github.com/dnkolegov), [dhardtke](https://github.com/dhardtke), [wirehead](https://github.com/wirehead), [thorn0](https://github.com/thorn0), [styu](https://github.com/styu), [mozfreddyb](https://github.com/mozfreddyb), [mikesamuel](https://github.com/mikesamuel), [jorangreef](https://github.com/jorangreef), [jimmyhchan](https://github.com/jimmyhchan), [jameydeorio](https://github.com/jameydeorio), [jameskraus](https://github.com/jameskraus), [hyderali](https://github.com/hyderali), [hansottowirtz](https://github.com/hansottowirtz), [hackvertor](https://github.com/hackvertor), [freddyb](https://github.com/freddyb), [flavorjones](https://github.com/flavorjones), [djfarrelly](https://github.com/djfarrelly), [devd](https://github.com/devd), [camerondunford](https://github.com/camerondunford), [buu700](https://github.com/buu700), [buildog](https://github.com/buildog), [alabiaga](https://github.com/alabiaga), [Vector919](https://github.com/Vector919), [Robbert](https://github.com/Robbert), [GreLI](https://github.com/GreLI), [FuzzySockets](https://github.com/FuzzySockets), [ArtemBernatskyy](https://github.com/ArtemBernatskyy), [@garethheyes](https://twitter.com/garethheyes), [@shafigullin](https://twitter.com/shafigullin), [@mmrupp](https://twitter.com/mmrupp), [@irsdl](https://twitter.com/irsdl),[ShikariSenpai](https://github.com/ShikariSenpai), [ansjdnakjdnajkd](https://github.com/ansjdnakjdnajkd), [@asutherland](https://twitter.com/asutherland), [@mathias](https://twitter.com/mathias), [@cgvwzq](https://twitter.com/cgvwzq), [@robbertatwork](https://twitter.com/robbertatwork), [@giutro](https://twitter.com/giutro), [@CmdEngineer\_](https://twitter.com/CmdEngineer_), [@avr4mit](https://twitter.com/avr4mit) and especially [@securitymb ❤ī¸](https://twitter.com/securitymb) & [@masatokinugawa ❤ī¸](https://twitter.com/masatokinugawa) ## Testing powered by -
+
And last but not least, thanks to [BrowserStack Open-Source Program](https://www.browserstack.com/open-source) for supporting this project with their services for free and delivering excellent, dedicated and very professional support on top of that. diff --git a/bower.json b/bower.json index dd812ac65..bbfbadedf 100644 --- a/bower.json +++ b/bower.json @@ -1,10 +1,10 @@ { - "name": "DOMPurify", - "version": "3.0.6", + "name": "dompurify", + "version": "3.2.0", "homepage": "https://github.com/cure53/DOMPurify", "author": "Cure53 ", "description": "A DOM-only, super-fast, uber-tolerant XSS sanitizer for HTML, MathML and SVG", - "main": "src/purify.js", + "main": "dist/purify.min.js", "keywords": [ "dom", "xss", diff --git a/demos/README.md b/demos/README.md index e1cd2a20e..5528d43ba 100644 --- a/demos/README.md +++ b/demos/README.md @@ -12,7 +12,7 @@ This is the relevant code: ```javascript // Clean HTML string and write into our DIV -var clean = DOMPurify.sanitize(dirty); +const clean = DOMPurify.sanitize(dirty); ``` ### Config Demo [Link](config-demo.html) @@ -24,10 +24,10 @@ This is the relevant code: ```javascript // Specify a configuration directive, only

elements allowed // Note: We want to also keep

's text content, so we add #text too -var config = { ALLOWED_TAGS: ['p', '#text'], KEEP_CONTENT: false }; +const config = { ALLOWED_TAGS: ['p', '#text'], KEEP_CONTENT: false }; // Clean HTML string and write into our DIV -var clean = DOMPurify.sanitize(dirty, config); +const clean = DOMPurify.sanitize(dirty, config); ``` ### Advanced Config Demo [Link](advanced-config-demo.html) @@ -38,7 +38,7 @@ This is the relevant code: ```javascript // Specify a configuration directive -var config = { +const config = { ALLOWED_TAGS: ['p', '#text'], // only

and text nodes KEEP_CONTENT: false, // remove content from non-allow-listed nodes too ADD_ATTR: ['kitty-litter'], // permit kitty-litter attributes @@ -47,7 +47,7 @@ var config = { }; // Clean HTML string and write into our DIV -var clean = DOMPurify.sanitize(dirty, config); +const clean = DOMPurify.sanitize(dirty, config); ``` ### Hooks Demo [Link](hooks-demo.html) @@ -66,7 +66,7 @@ DOMPurify.addHook('beforeSanitizeAttributes', function (node) { }); // Clean HTML string and write into our DIV -var clean = DOMPurify.sanitize(dirty); +const clean = DOMPurify.sanitize(dirty); ``` ### Add hooks and remove hooks [Link](hooks-removal-demo.html) @@ -85,13 +85,13 @@ DOMPurify.addHook('beforeSanitizeAttributes', function (node) { }); // Clean HTML string and write into our DIV -var clean = DOMPurify.sanitize(dirty); +let clean = DOMPurify.sanitize(dirty); // now let's remove the hook again console.log(DOMPurify.removeHook('beforeSanitizeAttributes')); // Clean HTML string and write into our DIV -var clean = DOMPurify.sanitize(dirty); +let clean = DOMPurify.sanitize(dirty); ``` ### Hook to open all links in a new window [Link](hooks-target-blank-demo.html) @@ -117,7 +117,7 @@ DOMPurify.addHook('afterSanitizeAttributes', function (node) { }); // Clean HTML string and write into our DIV -var clean = DOMPurify.sanitize(dirty); +const clean = DOMPurify.sanitize(dirty); ``` ### Hook to white-list safe URI Schemes [Link](hooks-scheme-allowlist.html) @@ -130,15 +130,15 @@ This is the relevant code: ```javascript // allowed URI schemes -var allowlist = ['http', 'https', 'ftp']; +const allowlist = ['http', 'https', 'ftp']; // build fitting regex -var regex = RegExp('^(' + allowlist.join('|') + '):', 'gim'); +const regex = RegExp('^(' + allowlist.join('|') + '):', 'gim'); // Add a hook to enforce URI scheme allow-list DOMPurify.addHook('afterSanitizeAttributes', function (node) { // build an anchor to map URLs to - var anchor = document.createElement('a'); + const anchor = document.createElement('a'); // check all href attributes for validity if (node.hasAttribute('href')) { @@ -164,7 +164,7 @@ DOMPurify.addHook('afterSanitizeAttributes', function (node) { }); // Clean HTML string and write into our DIV -var clean = DOMPurify.sanitize(dirty); +const clean = DOMPurify.sanitize(dirty); ``` ### Hook to allow and sand-box all JavaScript [Link](hooks-mentaljs-demo.html) @@ -177,7 +177,7 @@ This is the relevant code: ```javascript // allow script elements -var config = { +const config = { ADD_TAGS: ['script'], ADD_ATTR: ['onclick', 'onmouseover', 'onload', 'onunload'], }; @@ -185,7 +185,7 @@ var config = { // Add a hook to sanitize all script content with MentalJS DOMPurify.addHook('uponSanitizeElement', function (node, data) { if (data.tagName === 'script') { - var script = node.textContent; + let script = node.textContent; if ( !script || 'src' in node.attributes || @@ -195,7 +195,7 @@ DOMPurify.addHook('uponSanitizeElement', function (node, data) { return node.parentNode.removeChild(node); } try { - var mental = MentalJS().parse({ + let mental = MentalJS().parse({ options: { eval: false, dom: true, @@ -212,7 +212,7 @@ DOMPurify.addHook('uponSanitizeElement', function (node, data) { // Add a hook to sanitize all white-listed events with MentalJS DOMPurify.addHook('uponSanitizeAttribute', function (node, data) { if (data.attrName.match(/^on\w+/)) { - var script = data.attrValue; + let script = data.attrValue; try { return (data.attrValue = MentalJS().parse({ options: { @@ -228,7 +228,7 @@ DOMPurify.addHook('uponSanitizeAttribute', function (node, data) { }); // Clean HTML string and write into our DIV -var clean = DOMPurify.sanitize(dirty, config); +const clean = DOMPurify.sanitize(dirty, config); ``` ### Hook to proxy all links [Link](hooks-link-proxy-demo.html) @@ -264,7 +264,7 @@ DOMPurify.addHook('afterSanitizeAttributes', function (node) { }); // Clean HTML string and write into our DIV -var clean = DOMPurify.sanitize(dirty); +const clean = DOMPurify.sanitize(dirty); ``` ### Hook to proxy all HTTP leaks including CSS [Link](hooks-proxy-demo.html) @@ -277,28 +277,28 @@ This is the relevant code: ```javascript // Specify proxy URL -var proxy = 'https://my.proxy/?url='; +const proxy = 'https://my.proxy/?url='; // What do we allow? Not much for now. But it's tight. -var config = { +const config = { FORBID_TAGS: ['svg'], WHOLE_DOCUMENT: true, }; // Specify attributes to proxy -var attributes = ['action', 'background', 'href', 'poster', 'src']; +const attributes = ['action', 'background', 'href', 'poster', 'src', 'srcset'] // specify the regex to detect external content -var regex = /(url\("?)(?!data:)/gim; +const regex = /(url\("?)(?!data:)/gim; /** * Take CSS property-value pairs and proxy URLs in values, * then add the styles to an array of property-value pairs */ function addStyles(output, styles) { - for (var prop = styles.length - 1; prop >= 0; prop--) { + for (let prop = styles.length - 1; prop >= 0; prop--) { if (styles[styles[prop]]) { - var url = styles[styles[prop]].replace(regex, '$1' + proxy); + let url = styles[styles[prop]].replace(regex, '$1' + proxy); styles[styles[prop]] = url; } if (styles[styles[prop]]) { @@ -312,8 +312,8 @@ function addStyles(output, styles) { * then create matching CSS text for later application to the DOM */ function addCSSRules(output, cssRules) { - for (var index = cssRules.length - 1; index >= 0; index--) { - var rule = cssRules[index]; + for (let index = cssRules.length - 1; index >= 0; index--) { + let rule = cssRules[index]; // check for rules with selector if (rule.type == 1 && rule.selectorText) { output.push(rule.selectorText + '{'); @@ -336,8 +336,8 @@ function addCSSRules(output, cssRules) { // check for @keyframes rules } else if (rule.type === rule.KEYFRAMES_RULE) { output.push('@keyframes ' + rule.name + '{'); - for (var i = rule.cssRules.length - 1; i >= 0; i--) { - var frame = rule.cssRules[i]; + for (let i = rule.cssRules.length - 1; i >= 0; i--) { + let frame = rule.cssRules[i]; if (frame.type === 8 && frame.keyText) { output.push(frame.keyText + '{'); if (frame.style) { @@ -365,7 +365,7 @@ function proxyAttribute(url) { // Add a hook to enforce proxy for leaky CSS rules DOMPurify.addHook('uponSanitizeElement', function (node, data) { if (data.tagName === 'style') { - var output = []; + let output = []; addCSSRules(output, node.sheet.cssRules); node.textContent = output.join('\n'); } @@ -374,7 +374,7 @@ DOMPurify.addHook('uponSanitizeElement', function (node, data) { // Add a hook to enforce proxy for all HTTP leaks incl. inline CSS DOMPurify.addHook('afterSanitizeAttributes', function (node) { // Check all src attributes and proxy them - for (var i = 0; i <= attributes.length - 1; i++) { + for (let i = 0; i <= attributes.length - 1; i++) { if (node.hasAttribute(attributes[i])) { node.setAttribute( attributes[i], @@ -385,12 +385,12 @@ DOMPurify.addHook('afterSanitizeAttributes', function (node) { // Check all style attribute values and proxy them if (node.hasAttribute('style')) { - var styles = node.style; - var output = []; - for (var prop = styles.length - 1; prop >= 0; prop--) { + let styles = node.style; + let output = []; + for (let prop = styles.length - 1; prop >= 0; prop--) { // we re-write each property-value pair to remove invalid CSS if (node.style[styles[prop]] && regex.test(node.style[styles[prop]])) { - var url = node.style[styles[prop]].replace(regex, '$1' + proxy); + let url = node.style[styles[prop]].replace(regex, '$1' + proxy); node.style[styles[prop]] = url; } output.push(styles[prop] + ':' + node.style[styles[prop]] + ';'); @@ -405,7 +405,7 @@ DOMPurify.addHook('afterSanitizeAttributes', function (node) { }); // Clean HTML string and write into our DIV -var clean = DOMPurify.sanitize(dirty, config); +const clean = DOMPurify.sanitize(dirty, config); ``` ### Hook to sanitize SVGs shown via an `` tag. [Link](hooks-svg-demo.html) @@ -423,14 +423,14 @@ DOMPurify.addHook('afterSanitizeAttributes', function (node) { }); // Clean SVG string and allow the "filter" tag -var clean = DOMPurify.sanitize(dirty, { ADD_TAGS: ['filter'] }); +const clean = DOMPurify.sanitize(dirty, { ADD_TAGS: ['filter'] }); // Remove partial XML comment left in the HTML -var badTag = clean.indexOf(']>'); -var pureSvg = clean.substring(badTag < 0 ? 0 : 5, clean.length); +let badTag = clean.indexOf(']>'); +let pureSvg = clean.substring(badTag < 0 ? 0 : 5, clean.length); // Show sanitized content in element -var img = new Image(); +let img = new Image(); img.src = 'data:image/svg+xml;base64,' + window.btoa(pureSvg); document.getElementById('sanitized').appendChild(img); ``` diff --git a/demos/advanced-config-demo.html b/demos/advanced-config-demo.html index 92b29d55b..9009f244e 100644 --- a/demos/advanced-config-demo.html +++ b/demos/advanced-config-demo.html @@ -9,28 +9,32 @@ diff --git a/demos/basic-demo.html b/demos/basic-demo.html index b7577b14f..e5b02cadf 100644 --- a/demos/basic-demo.html +++ b/demos/basic-demo.html @@ -9,15 +9,16 @@ diff --git a/demos/config-demo.html b/demos/config-demo.html index f84f901d8..c56e0caee 100644 --- a/demos/config-demo.html +++ b/demos/config-demo.html @@ -9,20 +9,23 @@ diff --git a/demos/hooks-demo.html b/demos/hooks-demo.html index bd6c099c3..15ab3cc20 100644 --- a/demos/hooks-demo.html +++ b/demos/hooks-demo.html @@ -9,23 +9,24 @@ diff --git a/demos/hooks-link-proxy-demo.html b/demos/hooks-link-proxy-demo.html index 02d0ed188..7a7019e5b 100644 --- a/demos/hooks-link-proxy-demo.html +++ b/demos/hooks-link-proxy-demo.html @@ -9,44 +9,42 @@ diff --git a/demos/hooks-mentaljs-demo.html b/demos/hooks-mentaljs-demo.html index e951316f4..5f4ddd730 100644 --- a/demos/hooks-mentaljs-demo.html +++ b/demos/hooks-mentaljs-demo.html @@ -11,84 +11,73 @@

diff --git a/demos/hooks-node-removal-demo.html b/demos/hooks-node-removal-demo.html index 012312b90..c3cef45b9 100644 --- a/demos/hooks-node-removal-demo.html +++ b/demos/hooks-node-removal-demo.html @@ -9,23 +9,21 @@ diff --git a/demos/hooks-node-removal2-demo.html b/demos/hooks-node-removal2-demo.html index f222c6ce7..1350f910e 100644 --- a/demos/hooks-node-removal2-demo.html +++ b/demos/hooks-node-removal2-demo.html @@ -9,53 +9,49 @@ diff --git a/demos/hooks-proxy-demo.html b/demos/hooks-proxy-demo.html index 17076ce1b..d6f976863 100644 --- a/demos/hooks-proxy-demo.html +++ b/demos/hooks-proxy-demo.html @@ -9,147 +9,100 @@ diff --git a/demos/hooks-removal-demo.html b/demos/hooks-removal-demo.html index 1134c7303..da9675a2b 100644 --- a/demos/hooks-removal-demo.html +++ b/demos/hooks-removal-demo.html @@ -9,90 +9,53 @@ diff --git a/demos/hooks-sanitize-css-demo.html b/demos/hooks-sanitize-css-demo.html index 5ebd354c5..a2b91fdf8 100644 --- a/demos/hooks-sanitize-css-demo.html +++ b/demos/hooks-sanitize-css-demo.html @@ -13,21 +13,21 @@ /* global DOMPurify */ 'use strict'; window.onload = function(){ - + // Specify dirty HTML - var dirty = document.getElementById('payload').value; + let dirty = document.getElementById('payload').value; // We can allow all (default elements) but SVG - var config = { + const config = { FORBID_TAGS: ['svg'] // SVG is not yet supported. Too messy. }; - // Specify CSS property allow-list - var allowed_properties = [ - 'color', + // Specify CSS property whitelist + const allowed_properties = [ + 'color', 'background', - 'border', - 'padding', + 'border', + 'padding', 'margin', 'font-family', 'content', @@ -35,48 +35,46 @@ ]; // Specify if CSS functions are permitted - var allow_css_functions = true; + const allow_css_functions = true; /** - * Take CSS property-value pairs and validate against allow-list, + * Take CSS property-value pairs and validate against white-list, * then add the styles to an array of property-value pairs */ function validateStyles(output, styles) { - // Validate regular CSS properties - for (var prop in styles) { - if (typeof styles[prop] === 'string') { - if (styles[prop] && allowed_properties.indexOf(prop) > -1) { - if (allow_css_functions || !/\w+\(/.test(styles[prop])) { - output.push(prop + ':' + styles[prop] +';'); - } - } - } + Object.keys(styles).forEach(function(index) { + if (styles.hasOwnProperty(index)) { + let normalizedKey = styles[index].replace(/([A-Z])/g, '-$1').toLowerCase(); + if (allowed_properties.includes(normalizedKey)) { + let value = styles[normalizedKey]; + output.push(`${normalizedKey}:${value};`); + } } + }); } /** * Take CSS rules and analyze them, create string wrapper to - * apply them to the DOM later on. Note that only selector rules + * apply them to the DOM later on. Note that only selector rules * are supported right now */ function addCSSRules(output, cssRules) { - for (var index = cssRules.length-1; index >= 0; index--) { - var rule = cssRules[index]; + Array.from(cssRules).reverse().forEach(rule => { // check for rules with selector - if (rule.type == 1 && rule.selectorText) { - output.push(rule.selectorText + '{') + if (rule.type === 1 && rule.selectorText) { + output.push(`${rule.selectorText}{`); if (rule.style) { - validateStyles(output, rule.style) + validateStyles(output, rule.style); } output.push('}'); } - } + }); } // Add a hook to enforce CSS element sanitization DOMPurify.addHook('uponSanitizeElement', function(node, data) { if (data.tagName === 'style') { - var output = []; + let output = []; addCSSRules(output, node.sheet.cssRules); node.textContent = output.join("\n"); } @@ -86,13 +84,13 @@ DOMPurify.addHook('afterSanitizeAttributes', function(node) { // Nasty hack to fix baseURI + CSS problems in Chrome if (!node.ownerDocument.baseURI) { - var base = document.createElement('base'); + let base = document.createElement('base'); base.href = document.baseURI; node.ownerDocument.head.appendChild(base); } // Check all style attribute values and validate them if (node.hasAttribute('style')) { - var output = []; + let output = []; validateStyles(output, node.style); // re-add styles in case any are left if (output.length) { @@ -104,7 +102,7 @@ }); // Clean HTML string and write into our DIV - var clean = DOMPurify.sanitize(dirty, config); + let clean = DOMPurify.sanitize(dirty, config); document.getElementById('sanitized').innerHTML = clean; } diff --git a/demos/hooks-scheme-allowlist.html b/demos/hooks-scheme-allowlist.html index 9f78fa1f3..8eb7411fc 100644 --- a/demos/hooks-scheme-allowlist.html +++ b/demos/hooks-scheme-allowlist.html @@ -9,68 +9,69 @@ diff --git a/demos/hooks-svg-demo.html b/demos/hooks-svg-demo.html deleted file mode 100644 index efb7dfd13..000000000 --- a/demos/hooks-svg-demo.html +++ /dev/null @@ -1,120 +0,0 @@ - - - - - - - -
- - - - - diff --git a/demos/hooks-target-blank-demo.html b/demos/hooks-target-blank-demo.html index b87fd38a9..cfa64efcf 100644 --- a/demos/hooks-target-blank-demo.html +++ b/demos/hooks-target-blank-demo.html @@ -9,42 +9,35 @@ diff --git a/demos/trusted-types-demo.html b/demos/trusted-types-demo.html index 20b566d78..f90004957 100644 --- a/demos/trusted-types-demo.html +++ b/demos/trusted-types-demo.html @@ -2,7 +2,6 @@ - \">", "expected": "

" }, { - "title": "Tests against additonal problems regarding HTML inside MathML 2/2", + "title": "Tests against additional problems regarding HTML inside MathML 2/2", "payload": "", "expected": "" }, { diff --git a/test/karma.conf.js b/test/karma.conf.js index a6e91b77c..e5b3123a8 100644 --- a/test/karma.conf.js +++ b/test/karma.conf.js @@ -1,5 +1,5 @@ const includePaths = require('rollup-plugin-includepaths'); -const rollupConfig = require('../rollup.config.js'); +const rollupConfig = require('../rollup.config.js')[0]; const customLaunchers = require('./karma.custom-launchers.config.js').customLaunchers; const browsers = require('./karma.custom-launchers.config.js').browsers; @@ -27,7 +27,7 @@ module.exports = function (config) { ], preprocessors: { - 'src/*.js': ['rollup'], + 'src/*.ts': ['rollup'], 'test/**/*.spec.js': ['rollup'], }, diff --git a/test/karma.custom-launchers.config.js b/test/karma.custom-launchers.config.js index e3953fb33..fd367dec7 100644 --- a/test/karma.custom-launchers.config.js +++ b/test/karma.custom-launchers.config.js @@ -42,20 +42,36 @@ const customLaunchers = { browser: 'safari', os_version: 'Big Sur', }, - bs_win10_edge_84: { + bs_monterey_safari_15: { base: 'BrowserStack', device: null, - os: 'Windows', - browser_version: '84.0', - browser: 'edge', - os_version: '10', + os: 'OS X', + browser_version: '15.6', + browser: 'safari', + os_version: 'Monterey', }, - bs_win10_firefox_60: { + bs_ventura_safari_16: { + base: 'BrowserStack', + device: null, + os: 'OS X', + browser_version: '16.5', + browser: 'safari', + os_version: 'Ventura', + }, + bs_sonoma_safari_17: { + base: 'BrowserStack', + device: null, + os: 'OS X', + browser_version: '17.0', + browser: 'safari', + os_version: 'Sonoma', + }, + bs_win10_edge_84: { base: 'BrowserStack', device: null, os: 'Windows', - browser_version: '60.0', - browser: 'firefox', + browser_version: '84.0', + browser: 'edge', os_version: '10', }, bs_win10_firefox_70: { @@ -98,6 +114,22 @@ const customLaunchers = { browser: 'firefox', os_version: '10', }, + bs_win10_firefox_120: { + base: 'BrowserStack', + device: null, + os: 'Windows', + browser_version: '120.0', + browser: 'firefox', + os_version: '11', + }, + bs_win10_firefox_125: { + base: 'BrowserStack', + device: null, + os: 'Windows', + browser_version: '125.0', + browser: 'firefox', + os_version: '11', + }, bs_win10_chrome_60: { base: 'BrowserStack', device: null, @@ -146,6 +178,22 @@ const customLaunchers = { browser: 'chrome', os_version: '10', }, + bs_win10_chrome_120: { + base: 'BrowserStack', + device: null, + os: 'Windows', + browser_version: '120.0', + browser: 'chrome', + os_version: '11', + }, + bs_win10_chrome_124: { + base: 'BrowserStack', + device: null, + os: 'Windows', + browser_version: '124.0', + browser: 'chrome', + os_version: '11', + }, }; const getAllBrowsers = () => Object.keys(customLaunchers); @@ -153,12 +201,12 @@ const getRandomBrowser = () => sample(getAllBrowsers()); /** * Environment variables are passed into the script and the depth of testing - * is affected accordginly. + * is affected accordingly. * * - Whenever on a PR we only want to probe test with Firefox * - Whenever we are on the most recent node version on GitHub Actions we test via BrowserStack * - If none of the prior mentioned holds we assume to be running local and respect the passed - * in borwsers argv + * in browsers argv */ const shouldProbeOnly = argv.shouldProbeOnly === 'true'; const shouldTestOnBrowserStack = argv.shouldTestOnBrowserStack === 'true'; diff --git a/test/test-suite.js b/test/test-suite.js index 23e0e1271..a8e44e6fe 100644 --- a/test/test-suite.js +++ b/test/test-suite.js @@ -804,6 +804,21 @@ ); } ); + QUnit.test( + 'CUSTOM_ELEMENT_HANDLING config values of null do not throw a TypeError.', + function (assert) { + DOMPurify.sanitize('', { + CUSTOM_ELEMENT_HANDLING: { + tagNameCheck: null, + attributeNameCheck: null, + allowCustomizedBuiltInElements: null, + }, + }); + + // Don't see a great way to assert NOT throws... + assert.ok(true); + } + ); QUnit.test('Test dirty being an array', function (assert) { assert.equal( DOMPurify.sanitize(['123456']), @@ -1109,9 +1124,9 @@ assert.equal(DOMPurify.removed.length, 0); } ); - // Tests to make sure that the node scanning feature delivers acurate results on all browsers + // Tests to make sure that the node scanning feature delivers accurate results on all browsers QUnit.test( - 'DOMPurify should deliver acurate results when sanitizing nodes 1', + 'DOMPurify should deliver accurate results when sanitizing nodes 1', function (assert) { var clean = DOMPurify.sanitize(document.createElement('td')); assert.equal(clean, ''); @@ -1428,7 +1443,7 @@ ALLOWED_URI_REGEXP: /test\.com/i }), ''); - // ensure that the previous regexp does not affect future santize calls + // ensure that the previous regexp does not affect future sanitize calls assert.equal(DOMPurify.sanitize(dirty), expected); }); QUnit.test( @@ -1573,7 +1588,7 @@ } ); QUnit.test( - 'Test for less agressive mXSS handling, See #369', + 'Test for less aggressive mXSS handling, See #369', function (assert) { var config = { FORBID_TAGS: ['svg', 'math'], @@ -1694,8 +1709,8 @@ test: '', expected: [ - '', - '', + '', + '', '', ], }, @@ -1720,9 +1735,9 @@ { test: '', expected: [ - '', + '', '', - '', + '', ], }, { @@ -2078,5 +2093,16 @@ // cleanup hook DOMPurify.removeHook(entryPoint); }); + + QUnit.test('Test proper removal of annotation-xml w. custom elements', function (assert) { + const dirty = '

'; + const config = { + CUSTOM_ELEMENT_HANDLING: { tagNameCheck: /.*/ }, + FORBID_CONTENTS: [""] + }; + const expected = ''; + let clean = DOMPurify.sanitize(dirty, config); + assert.contains(clean, expected); + }); }; -}); +}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..c5e111f20 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "declaration": false, + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", + "skipLibCheck": true + }, + "include": ["src/**/*.ts"] +} diff --git a/website/index.html b/website/index.html index 9f55edc2a..977ae55fe 100644 --- a/website/index.html +++ b/website/index.html @@ -1,8 +1,8 @@ - + - DOMPurify 3.0.6 "Factory Reset" + DOMPurify 3.2.0 "Typewriter" @@ -23,7 +23,7 @@ -

DOMPurify 3.0.6 "Factory Reset"

+

DOMPurify 3.2.0 "Typewriter"

npm version Build and Test