Skip to content

Commit 677ecb2

Browse files
committed
AG-36615 Improve 'trusted-replace-node-text' — add 'trustedTypes.createPolicy'. #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
1 parent cbe3a8b commit 677ecb2

File tree

5 files changed

+99
-4
lines changed

5 files changed

+99
-4
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ The format is based on [Keep a Changelog], and this project adheres to [Semantic
1414

1515
### Added
1616

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

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

src/helpers/node-text-utils.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,17 @@ import { nodeListToArray } from './array-utils';
33
import { getAddedNodes } from './observer';
44
import { toRegExp } from './string-utils';
55

6+
declare global {
7+
interface Window {
8+
trustedTypes?: {
9+
createPolicy: (
10+
name: string,
11+
rules: { createScript: (input: string) => string }
12+
) => { createScript: (input: string) => string };
13+
};
14+
}
15+
}
16+
617
type NodeHandler = (nodes: Node[]) => void;
718

819
/**
@@ -109,7 +120,28 @@ export const replaceNodeText = (
109120
): void => {
110121
const { textContent } = node;
111122
if (textContent) {
112-
node.textContent = textContent.replace(pattern, replacement);
123+
// For websites that use Trusted Types
124+
// https://w3c.github.io/webappsec-trusted-types/dist/spec/
125+
if (
126+
node.nodeName === 'SCRIPT'
127+
&& window.trustedTypes
128+
&& window.trustedTypes.createPolicy
129+
) {
130+
// The name for the trusted-types policy should only be 'AGPolicy',because corelibs can
131+
// allow our policy if the server has restricted the creation of a trusted-types policy with
132+
// the directive 'Content-Security-Policy: trusted-types <policyName>;`.
133+
// If such a header is presented in the server response, corelibs adds permission to create
134+
// the 'AGPolicy' policy with the 'allow-duplicates' option to prevent errors.
135+
// See AG-18204 for details.
136+
const policy = window.trustedTypes.createPolicy('AGPolicy', {
137+
createScript: (string) => string,
138+
});
139+
const modifiedText = textContent.replace(pattern, replacement);
140+
const trustedReplacement = policy.createScript(modifiedText);
141+
node.textContent = trustedReplacement;
142+
} else {
143+
node.textContent = textContent.replace(pattern, replacement);
144+
}
113145
hit(source);
114146
}
115147
};

tests/helpers.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,18 @@ export const runScriptlet = (name, args, verbose = true) => {
4444
verbose,
4545
};
4646
const resultString = window.scriptlets.invoke(params);
47-
evalWrapper(resultString);
47+
48+
// Create a trustedTypes policy for eval,
49+
// it's required for a test with CSP "require-trusted-types-for" for "trusted-replace-node-text" scriptlet
50+
if (window.trustedTypes) {
51+
const policy = window.trustedTypes.createPolicy('myEscapePolicy', {
52+
createScript: (string) => string,
53+
});
54+
const sanitizedString = policy.createScript(resultString);
55+
evalWrapper(sanitizedString);
56+
} else {
57+
evalWrapper(resultString);
58+
}
4859
};
4960

5061
/**

tests/scriptlets/abort-on-stack-trace.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,7 @@ test('abort String.fromCodePoint, inline script line number regexp', (assert) =>
410410

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

tests/scriptlets/trusted-replace-node-text.test.js

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,21 @@ const afterEach = () => {
2222

2323
module(name, { beforeEach, afterEach });
2424

25+
let policy;
26+
if (window.trustedTypes) {
27+
policy = window.trustedTypes.createPolicy('myEscapePolicy', {
28+
createHTML: (string) => string,
29+
createScript: (string) => string,
30+
});
31+
}
32+
2533
const addNode = (nodeName, textContent) => {
2634
const node = document.createElement(nodeName);
27-
node.textContent = textContent;
35+
if (nodeName === 'script' && policy) {
36+
node.textContent = policy.createScript(textContent);
37+
} else {
38+
node.textContent = textContent;
39+
}
2840
elements.push(node);
2941
document.body.appendChild(node);
3042
return node;
@@ -90,6 +102,44 @@ test('using matchers as regexes', (assert) => {
90102
}, 1);
91103
});
92104

105+
test('CSP require-trusted-types-for "script" - replace script content', (assert) => {
106+
const done = assert.async();
107+
108+
// QUnit uses innerHTML to render tests, so we need to override it and add trusted types policy
109+
const nativeInnerHTMLSet = Object.getOwnPropertyDescriptor(Element.prototype, 'innerHTML').set;
110+
Object.defineProperty(Element.prototype, 'innerHTML', {
111+
set(html) {
112+
if (policy) {
113+
const sanitizedHTML = policy.createHTML(html);
114+
return nativeInnerHTMLSet.call(this, sanitizedHTML);
115+
}
116+
return nativeInnerHTMLSet.call(this, html);
117+
},
118+
});
119+
120+
const meta = document.createElement('meta');
121+
document.head.appendChild(meta);
122+
meta.setAttribute('http-equiv', 'Content-Security-Policy');
123+
meta.setAttribute('content', "require-trusted-types-for 'script'");
124+
125+
const nodeName = 'script';
126+
const textMatch = 'window.showAds';
127+
const pattern = 'window.showAds = true';
128+
const replacement = 'window.showAds = false';
129+
130+
const expectedText = 'window.showAds = false;';
131+
132+
runScriptlet(name, [nodeName, textMatch, pattern, replacement]);
133+
134+
const nodeAfter = addNode('script', 'window.showAds = true;');
135+
setTimeout(() => {
136+
assert.strictEqual(nodeAfter.textContent, expectedText, 'text content should be modified');
137+
assert.strictEqual(window.hit, 'FIRED', 'hit function should fire');
138+
clearGlobalProps('showAds');
139+
done();
140+
}, 1);
141+
});
142+
93143
test('Log content', (assert) => {
94144
// There are 7 "asserts" in test but node is modified two times
95145
// so it's logged twice, that's why 9 is expected

0 commit comments

Comments
 (0)