diff --git a/karma.conf.js b/karma.conf.js index 785172061b..e9cad12e4c 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -123,6 +123,19 @@ module.exports = config => { cfg = addSauceTests(cfg, sauceConfig); cfg = chooseTestSuite(cfg, env.MOCHA_TEST); + // It would be very difficult to meaningfully apply trusted types to + // a requirejs environment, and would require changes to requirejs if so. + if (env.MOCHA_TEST !== 'requirejs') { + cfg.customHeaders = cfg.customHeaders || []; + // Test with native trusted types (in browsers that support them). + // https://w3c.github.io/webappsec-trusted-types/dist/spec/#introduction + cfg.customHeaders.push({ + match: '.*', + name: 'Content-Security-Policy', + value: "require-trusted-types-for 'script';" + }); + } + // include sourcemap cfg = { ...cfg, diff --git a/lib/browser/highlight-tags.js b/lib/browser/highlight-tags.js index d98896e789..edabef8429 100644 --- a/lib/browser/highlight-tags.js +++ b/lib/browser/highlight-tags.js @@ -25,6 +25,22 @@ function highlight(js) { ); } +let highlightPolicy = { + createHTML: function(html) { + // The highlight function escapes its input. + return highlight(html); + } +}; +if ( + typeof window !== 'undefined' && + typeof window.trustedTypes !== 'undefined' +) { + highlightPolicy = window.trustedTypes.createPolicy( + 'mocha-highlight-tags', + highlightPolicy + ); +} + /** * Highlight the contents of tag `name`. * @@ -34,6 +50,6 @@ function highlight(js) { module.exports = function highlightTags(name) { var code = document.getElementById('mocha').getElementsByTagName(name); for (var i = 0, len = code.length; i < len; ++i) { - code[i].innerHTML = highlight(code[i].innerHTML); + code[i].innerHTML = highlightPolicy.createHTML(code[i].innerHTML); } }; diff --git a/lib/reporters/html.js b/lib/reporters/html.js index 70e8698407..610a968b79 100644 --- a/lib/reporters/html.js +++ b/lib/reporters/html.js @@ -313,6 +313,24 @@ function error(msg) { document.body.appendChild(fragment('
%s
', msg)); } +let policy = { + createHTML: function(html) { + /** + * Note that this policy lets html through unchanged. This is potentially + * a security vulnerability if untrusted data is set to innerHTML, as it + * allows arbitrary code execution. + * + * Ideally this code would be refactored to not use .innerHTML, and this + * policy deleted, or this policy could return the html after it has been + * processed by a secure sanitization system like dompurify + */ + return html; + } +}; +if (typeof window !== 'undefined' && window.trustedTypes != null) { + policy = window.trustedTypes.createPolicy('mocha-html-reporter', policy); +} + /** * Return a DOM fragment from `html`. * @@ -323,15 +341,17 @@ function fragment(html) { var div = document.createElement('div'); var i = 1; - div.innerHTML = html.replace(/%([se])/g, function(_, type) { - switch (type) { - case 's': - return String(args[i++]); - case 'e': - return escape(args[i++]); - // no default - } - }); + div.innerHTML = policy.createHTML( + html.replace(/%([se])/g, function(_, type) { + switch (type) { + case 's': + return String(args[i++]); + case 'e': + return escape(args[i++]); + // no default + } + }) + ); return div.firstChild; } diff --git a/rollup.config.js b/rollup.config.js index 4b4f3494e4..e11ff9fb17 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -3,6 +3,7 @@ import nodeResolve from '@rollup/plugin-node-resolve'; import json from '@rollup/plugin-json'; import builtins from 'rollup-plugin-node-builtins'; import globals from 'rollup-plugin-node-globals'; +import * as fs from 'fs'; import {babel} from '@rollup/plugin-babel'; @@ -11,6 +12,36 @@ import visualizer from 'rollup-plugin-visualizer'; import pickFromPackageJson from './scripts/pick-from-package-json'; +/** + * A temporary plugin workaround for a globalThis polyfill. + * + * Older versions of regenerator-runtime use Function("return this")() to get + * the global `this` value when running in strict mode. This is not compatible + * with some content security policies, including trusted-types, which we + * test with in browsers that support it. + * + * Fortunately, all browsers that support trusted-types also support the global + * variable named `globalThis` for accessing the global `this` value. So + * whenever we would run `Function("return this")()` we can instead first look + * whether `globalThis` is defined, and if so, just use that. + * + * The latest version of regenerator-runtime does rely on calling Function + * to get globalThis, so we only need this plugin until the updated version + * has percolated through our dependency tree. We can try to remove it on + * 2021-01-01. This behavior is tested, so we can just remove the plugin + * from our array and try `npm test`. If the tests pass, this can be removed. + */ +const applyTemporaryCspPatchPlugin = { + writeBundle(options) { + let contents = fs.readFileSync(options.file, {encoding: 'utf8'}); + contents = contents.replace( + /Function\("return this"\)\(\)/g, + `(typeof globalThis !== 'undefined' ? globalThis : Function("return this")())` + ); + fs.writeFileSync(options.file, contents, {encoding: 'utf8'}); + } +}; + const config = { input: './browser-entry.js', output: { @@ -47,7 +78,8 @@ const config = { ] ], babelHelpers: 'bundled' - }) + }), + applyTemporaryCspPatchPlugin ], onwarn: (warning, warn) => { if (warning.code === 'CIRCULAR_DEPENDENCY') return;