Skip to content

Commit

Permalink
AG-36615 Improve 'trusted-replace-node-text' — add 'trustedTypes.crea…
Browse files Browse the repository at this point in the history
…tePolicy'. #457

Squashed commit of the following:

commit 9e1f76e
Author: Adam Wróblewski <[email protected]>
Date:   Tue Oct 22 11:36:26 2024 +0200

    Add test

commit c4dd4d8
Author: Adam Wróblewski <[email protected]>
Date:   Tue Oct 22 10:16:20 2024 +0200

    Add trusted-types policy to trusted-replace-node-text scriptlet
  • Loading branch information
AdamWr committed Oct 22, 2024
1 parent cbe3a8b commit 677ecb2
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 4 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ The format is based on [Keep a Changelog], and this project adheres to [Semantic

### Added

- `trusted-types` policy to `trusted-replace-node-text` scriptlet [#457]
- `prevent-canvas` scriptlet [#451]
- `parentSelector` option to search for nodes for `remove-node-text` scriptlet [#397]
- `transform` option with `base64decode` value for `href-sanitizer` scriptlet [#455]
Expand All @@ -33,6 +34,7 @@ The format is based on [Keep a Changelog], and this project adheres to [Semantic
[#441]: https://github.com/AdguardTeam/Scriptlets/issues/441
[#397]: https://github.com/AdguardTeam/Scriptlets/issues/397
[#458]: https://github.com/AdguardTeam/Scriptlets/issues/458
[#457]: https://github.com/AdguardTeam/Scriptlets/issues/457

## [v1.12.1] - 2024-09-20

Expand Down
34 changes: 33 additions & 1 deletion src/helpers/node-text-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@ import { nodeListToArray } from './array-utils';
import { getAddedNodes } from './observer';
import { toRegExp } from './string-utils';

declare global {
interface Window {
trustedTypes?: {
createPolicy: (
name: string,
rules: { createScript: (input: string) => string }
) => { createScript: (input: string) => string };
};
}
}

type NodeHandler = (nodes: Node[]) => void;

/**
Expand Down Expand Up @@ -109,7 +120,28 @@ export const replaceNodeText = (
): void => {
const { textContent } = node;
if (textContent) {
node.textContent = textContent.replace(pattern, replacement);
// For websites that use Trusted Types
// https://w3c.github.io/webappsec-trusted-types/dist/spec/
if (
node.nodeName === 'SCRIPT'
&& window.trustedTypes
&& window.trustedTypes.createPolicy
) {
// The name for the trusted-types policy should only be 'AGPolicy',because corelibs can
// allow our policy if the server has restricted the creation of a trusted-types policy with
// the directive 'Content-Security-Policy: trusted-types <policyName>;`.
// If such a header is presented in the server response, corelibs adds permission to create
// the 'AGPolicy' policy with the 'allow-duplicates' option to prevent errors.
// See AG-18204 for details.
const policy = window.trustedTypes.createPolicy('AGPolicy', {
createScript: (string) => string,
});
const modifiedText = textContent.replace(pattern, replacement);
const trustedReplacement = policy.createScript(modifiedText);
node.textContent = trustedReplacement;
} else {
node.textContent = textContent.replace(pattern, replacement);
}
hit(source);
}
};
Expand Down
13 changes: 12 additions & 1 deletion tests/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,18 @@ export const runScriptlet = (name, args, verbose = true) => {
verbose,
};
const resultString = window.scriptlets.invoke(params);
evalWrapper(resultString);

// Create a trustedTypes policy for eval,
// it's required for a test with CSP "require-trusted-types-for" for "trusted-replace-node-text" scriptlet
if (window.trustedTypes) {
const policy = window.trustedTypes.createPolicy('myEscapePolicy', {
createScript: (string) => string,
});
const sanitizedString = policy.createScript(resultString);
evalWrapper(sanitizedString);
} else {
evalWrapper(resultString);
}
};

/**
Expand Down
2 changes: 1 addition & 1 deletion tests/scriptlets/abort-on-stack-trace.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ test('abort String.fromCodePoint, inline script line number regexp', (assert) =>

test('abort JSON.parse, inline script line number regexp, two scripts abort only second', (assert) => {
const property = 'JSON.parse';
const stackMatch = '/inlineScript:33/';
const stackMatch = '/inlineScript:3(2|3)9/';
const scriptletArgs = [property, stackMatch];
runScriptlet(name, scriptletArgs);

Expand Down
52 changes: 51 additions & 1 deletion tests/scriptlets/trusted-replace-node-text.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,21 @@ const afterEach = () => {

module(name, { beforeEach, afterEach });

let policy;
if (window.trustedTypes) {
policy = window.trustedTypes.createPolicy('myEscapePolicy', {
createHTML: (string) => string,
createScript: (string) => string,
});
}

const addNode = (nodeName, textContent) => {
const node = document.createElement(nodeName);
node.textContent = textContent;
if (nodeName === 'script' && policy) {
node.textContent = policy.createScript(textContent);
} else {
node.textContent = textContent;
}
elements.push(node);
document.body.appendChild(node);
return node;
Expand Down Expand Up @@ -90,6 +102,44 @@ test('using matchers as regexes', (assert) => {
}, 1);
});

test('CSP require-trusted-types-for "script" - replace script content', (assert) => {
const done = assert.async();

// QUnit uses innerHTML to render tests, so we need to override it and add trusted types policy
const nativeInnerHTMLSet = Object.getOwnPropertyDescriptor(Element.prototype, 'innerHTML').set;
Object.defineProperty(Element.prototype, 'innerHTML', {
set(html) {
if (policy) {
const sanitizedHTML = policy.createHTML(html);
return nativeInnerHTMLSet.call(this, sanitizedHTML);
}
return nativeInnerHTMLSet.call(this, html);
},
});

const meta = document.createElement('meta');
document.head.appendChild(meta);
meta.setAttribute('http-equiv', 'Content-Security-Policy');
meta.setAttribute('content', "require-trusted-types-for 'script'");

const nodeName = 'script';
const textMatch = 'window.showAds';
const pattern = 'window.showAds = true';
const replacement = 'window.showAds = false';

const expectedText = 'window.showAds = false;';

runScriptlet(name, [nodeName, textMatch, pattern, replacement]);

const nodeAfter = addNode('script', 'window.showAds = true;');
setTimeout(() => {
assert.strictEqual(nodeAfter.textContent, expectedText, 'text content should be modified');
assert.strictEqual(window.hit, 'FIRED', 'hit function should fire');
clearGlobalProps('showAds');
done();
}, 1);
});

test('Log content', (assert) => {
// There are 7 "asserts" in test but node is modified two times
// so it's logged twice, that's why 9 is expected
Expand Down

0 comments on commit 677ecb2

Please sign in to comment.