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;