Skip to content

Commit

Permalink
Support running with TrustedTypes enforced.
Browse files Browse the repository at this point in the history
Trusted Types is a new Content Security Policy specification,
currently implemented in browsers based on Chromium 83 or higher, which
requires that data passed to APIs which may result in arbitrary code
execution must go through an explicit policy. This helps to catch
unintended use of dangerous APIs, and reduces the surface area for
some security reviews.

I'm not sure if test infrastructure like mocha is a likely target
for attack – seems like in most cases an attacker could only access test
data, and it is rare for tests to handle untrusted data. However,
there's value for infrastructure to be compatible with running with
Trusted Types enabled, as it will allow users to write tests to ensure
that the code under test can run with Trusted Types.

This change creates and applies policies for the two places in mocha
that call innerHTML, and adds a temporary patch to the rollup build.
With those changes in place, we can run mocha's karma tests with
Trusted Types enabled (save for the one test that runs with requirejs,
which relies on eval).

More info:

* Spec: https://w3c.github.io/webappsec-trusted-types/dist/spec/#introduction
* Related PR adding support to karma: karma-runner/karma#3360
  • Loading branch information
rictic committed Sep 15, 2020
1 parent 738a575 commit 614e23d
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 11 deletions.
13 changes: 13 additions & 0 deletions karma.conf.js
Expand Up @@ -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,
Expand Down
18 changes: 17 additions & 1 deletion lib/browser/highlight-tags.js
Expand Up @@ -25,6 +25,22 @@ function highlight(js) {
);
}

var 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`.
*
Expand All @@ -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);
}
};
41 changes: 32 additions & 9 deletions lib/reporters/html.js
Expand Up @@ -313,6 +313,27 @@ function error(msg) {
document.body.appendChild(fragment('<div id="mocha-error">%s</div>', msg));
}

var 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' &&
typeof window.trustedTypes !== 'undefined'
) {
policy = window.trustedTypes.createPolicy('mocha-html-reporter', policy);
}

/**
* Return a DOM fragment from `html`.
*
Expand All @@ -323,15 +344,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;
}
Expand Down
34 changes: 33 additions & 1 deletion rollup.config.js
Expand Up @@ -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';

Expand All @@ -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: {
Expand Down Expand Up @@ -47,7 +78,8 @@ const config = {
]
],
babelHelpers: 'bundled'
})
}),
applyTemporaryCspPatchPlugin
],
onwarn: (warning, warn) => {
if (warning.code === 'CIRCULAR_DEPENDENCY') return;
Expand Down

0 comments on commit 614e23d

Please sign in to comment.