Skip to content

Commit

Permalink
Add option to continue on trusted-types policy-creation failure
Browse files Browse the repository at this point in the history
Webpack already allows for specifying a trusted-types policy name. However, its current implementation is such that if a call to trustedTypes.createPolicy fails, the code will immediately stop executing. This isn't necessarily desirable, as an application could be in the early phases of rolling out trusted types, and thus have the CSP rule for trusted-types LibraryA LibraryB etc, BUT have require-trusted-types-for 'script' be in "report only" mode (Content-Security-Policy-Report-Only). In such a configuration, and when the webpacked code is dynamically-loaded into an application, adding the policy name to the webpack config will break old versions.

This PR keeps the original behavior, but introduces a new option for onPolicyCreationFailure: "continue" | "stop" (with "stop" remaining the default). If a developer chooses the "continue" option, the policy-creation will be wrapped in a try/catch. There is no security risk to this, since for host applications that DO have strict enforcement of trusted-types, the code will simply fail when the dangerous sink is used (e.g., when doing parseFromString). And likewise, wrapping in try/catch and doing nothing on catch is OK, because the code already deals with the possibility of the trustedTypes API not being available on the browser.
  • Loading branch information
Zlatkovsky committed May 2, 2023
1 parent 59abec1 commit 2203e24
Show file tree
Hide file tree
Showing 13 changed files with 159 additions and 4 deletions.
4 changes: 4 additions & 0 deletions declarations/WebpackOptions.d.ts
Expand Up @@ -2251,6 +2251,10 @@ export interface Environment {
* Use a Trusted Types policy to create urls for chunks.
*/
export interface TrustedTypes {
/**
* If the call to `trustedTypes.createPolicy(...)` fails -- e.g., due to the policy name missing from the CSP `trusted-types` list, or it being a duplicate name, etc. -- controls whether to continue with loading in the hope that `require-trusted-types-for 'script'` isn't enforced yet, versus fail immediately. Default behavior is 'stop'.
*/
onPolicyCreationFailure?: "continue" | "stop";
/**
* The name of the Trusted Types policy created by webpack to serve bundle chunks.
*/
Expand Down
1 change: 1 addition & 0 deletions lib/config/defaults.js
Expand Up @@ -961,6 +961,7 @@ const applyOutputDefaults = (
() =>
output.uniqueName.replace(/[^a-zA-Z0-9\-#=_/@.%]+/g, "_") || "webpack"
);
D(trustedTypes, "onPolicyCreationFailure", "stop");
}

/**
Expand Down
25 changes: 22 additions & 3 deletions lib/runtime/GetTrustedTypesPolicyRuntimeModule.js
Expand Up @@ -25,6 +25,9 @@ class GetTrustedTypesPolicyRuntimeModule extends HelperRuntimeModule {
const { runtimeTemplate, outputOptions } = compilation;
const { trustedTypes } = outputOptions;
const fn = RuntimeGlobals.getTrustedTypesPolicy;
const wrapPolicyCreationInTryCatch = trustedTypes
? trustedTypes.onPolicyCreationFailure === "continue"
: false;

return Template.asString([
"var policy;",
Expand Down Expand Up @@ -58,9 +61,25 @@ class GetTrustedTypesPolicyRuntimeModule extends HelperRuntimeModule {
? [
'if (typeof trustedTypes !== "undefined" && trustedTypes.createPolicy) {',
Template.indent([
`policy = trustedTypes.createPolicy(${JSON.stringify(
trustedTypes.policyName
)}, policy);`
...(wrapPolicyCreationInTryCatch ? ["try {"] : []),
...[
`policy = trustedTypes.createPolicy(${JSON.stringify(
trustedTypes.policyName
)}, policy);`
].map(line =>
wrapPolicyCreationInTryCatch ? Template.indent(line) : line
),
...(wrapPolicyCreationInTryCatch
? [
"} catch (e) {",
Template.indent([
`console.warn('Could not create trusted-types policy ${JSON.stringify(
trustedTypes.policyName
)}');`
]),
"}"
]
: [])
]),
"}"
]
Expand Down
2 changes: 1 addition & 1 deletion schemas/WebpackOptions.check.js

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions schemas/WebpackOptions.json
Expand Up @@ -5021,6 +5021,10 @@
"type": "object",
"additionalProperties": false,
"properties": {
"onPolicyCreationFailure": {
"description": "If the call to `trustedTypes.createPolicy(...)` fails -- e.g., due to the policy name missing from the CSP `trusted-types` list, or it being a duplicate name, etc. -- controls whether to continue with loading in the hope that `require-trusted-types-for 'script'` isn't enforced yet, versus fail immediately. Default behavior is 'stop'.",
"enum": ["continue", "stop"]
},
"policyName": {
"description": "The name of the Trusted Types policy created by webpack to serve bundle chunks.",
"type": "string",
Expand Down
17 changes: 17 additions & 0 deletions test/__snapshots__/Cli.basictest.js.snap
Expand Up @@ -6602,6 +6602,23 @@ Object {
"multiple": false,
"simpleType": "string",
},
"output-trusted-types-on-policy-creation-failure": Object {
"configs": Array [
Object {
"description": "If the call to \`trustedTypes.createPolicy(...)\` fails -- e.g., due to the policy name missing from the CSP \`trusted-types\` list, or it being a duplicate name, etc. -- controls whether to continue with loading in the hope that \`require-trusted-types-for 'script'\` isn't enforced yet, versus fail immediately. Default behavior is 'stop'.",
"multiple": false,
"path": "output.trustedTypes.onPolicyCreationFailure",
"type": "enum",
"values": Array [
"continue",
"stop",
],
},
],
"description": "If the call to \`trustedTypes.createPolicy(...)\` fails -- e.g., due to the policy name missing from the CSP \`trusted-types\` list, or it being a duplicate name, etc. -- controls whether to continue with loading in the hope that \`require-trusted-types-for 'script'\` isn't enforced yet, versus fail immediately. Default behavior is 'stop'.",
"multiple": false,
"simpleType": "string",
},
"output-trusted-types-policy-name": Object {
"configs": Array [
Object {
Expand Down
Empty file.
@@ -0,0 +1,36 @@
it("can continue on policy creation failure", function () {
// emulate trusted types in a window object
window.trustedTypes = {
createPolicy: () => {
throw new Error("Rejecting createPolicy call");
}
};

const createPolicySpy = jest.spyOn(window.trustedTypes, "createPolicy");
const consoleWarn = jest.spyOn(console, "warn").mockImplementation(() => {});

const promise = import(
"./empty?b" /* webpackChunkName: "continue-on-policy-creation-failure" */
);
var script = document.head._children.pop();
expect(script.src).toBe(
"https://test.cases/path/continue-on-policy-creation-failure.web.js"
);
__non_webpack_require__("./continue-on-policy-creation-failure.web.js");

expect(createPolicySpy).toHaveBeenCalledWith(
"CustomPolicyName",
expect.objectContaining({
createScriptURL: expect.anything()
})
);
expect(createPolicySpy).toThrow();
expect(consoleWarn).toHaveBeenCalledWith(
`Could not create trusted-types policy "CustomPolicyName"`
);

createPolicySpy.mockReset();
consoleWarn.mockReset();

return promise;
});
@@ -0,0 +1,17 @@
module.exports = {
target: "web",
output: {
chunkFilename: "[name].web.js",
crossOriginLoading: "anonymous",
trustedTypes: {
policyName: "CustomPolicyName",
onPolicyCreationFailure: "continue"
}
},
performance: {
hints: false
},
optimization: {
minimize: false
}
};
Empty file.
@@ -0,0 +1,38 @@
it("should stop if policy fails to be created", function () {
// emulate trusted types in a window object
window.trustedTypes = {
createPolicy: () => {
throw new Error("Rejecting createPolicy call");
}
};

const createPolicySpy = jest.spyOn(window.trustedTypes, "createPolicy");
const consoleWarn = jest.spyOn(console, "warn").mockImplementation(() => {});

let promise;
try {
promise = import(
"./empty?test=stop-on-policy-creation-failure" /* webpackChunkName: "stop-on-policy-creation-failure" */
);
} catch (e) {
expect(e.message).toBe("Rejecting createPolicy call");
}

// Unlike in the other test cases, we expect the failure above to prevent any scripts from being added to the document head
expect(document.head._children.length).toBe(0);
expect(createPolicySpy).toHaveBeenCalledWith(
"webpack",
expect.objectContaining({
createScriptURL: expect.anything()
})
);

// Unlike in the "continue-on-policy-creation-failure" case, we expect an outright thrown error,
// rather than a console warning. The console should not have been called:
expect(consoleWarn).toHaveBeenCalledTimes(0);

createPolicySpy.mockReset();
consoleWarn.mockReset();

return promise;
});
@@ -0,0 +1,14 @@
module.exports = {
target: "web",
output: {
chunkFilename: "[name].web.js",
crossOriginLoading: "anonymous",
trustedTypes: true
},
performance: {
hints: false
},
optimization: {
minimize: false
}
};
5 changes: 5 additions & 0 deletions types.d.ts
Expand Up @@ -12111,6 +12111,11 @@ declare class TopLevelSymbol {
* Use a Trusted Types policy to create urls for chunks.
*/
declare interface TrustedTypes {
/**
* If the call to `trustedTypes.createPolicy(...)` fails -- e.g., due to the policy name missing from the CSP `trusted-types` list, or it being a duplicate name, etc. -- controls whether to continue with loading in the hope that `require-trusted-types-for 'script'` isn't enforced yet, versus fail immediately. Default behavior is 'stop'.
*/
onPolicyCreationFailure?: "continue" | "stop";

/**
* The name of the Trusted Types policy created by webpack to serve bundle chunks.
*/
Expand Down

0 comments on commit 2203e24

Please sign in to comment.