Skip to content

Commit f10b5b9

Browse files
committed
AG-44584 Fix 'trusted-replace-node-text' — some quotes are incorrectly escaped. #517
Squashed commit of the following: commit 4530dda Author: Adam Wróblewski <[email protected]> Date: Mon Jul 28 21:36:59 2025 +0200 Fix fixQuotes function - handle case where pattern and replacement are not provided commit eba43cc Author: Slava Leleka <[email protected]> Date: Mon Jul 28 17:35:01 2025 +0300 Update changelog commit 99e6933 Author: Adam Wróblewski <[email protected]> Date: Mon Jul 28 16:31:06 2025 +0200 Add fixQuotes function commit 57f2998 Author: Adam Wróblewski <[email protected]> Date: Mon Jul 28 15:47:55 2025 +0200 Fix 'trusted-replace-node-text' — some quotes are incorrectly escaped
1 parent 1c85645 commit f10b5b9

File tree

5 files changed

+81
-10
lines changed

5 files changed

+81
-10
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ The format is based on [Keep a Changelog], and this project adheres to [Semantic
1414

1515
### Fixed
1616

17-
- issue with `TrustedScriptURL` in `prevent-element-src-loading` scriptlet [#514].
17+
- Incorrectly escaped quotes in `trusted-replace-node-text` scriptlet [#517].
18+
- `TrustedScriptURL` in `prevent-element-src-loading` scriptlet [#514].
1819

1920
[Unreleased]: https://github.com/AdguardTeam/Scriptlets/compare/v2.2.8...HEAD
2021
[#514]: https://github.com/AdguardTeam/Scriptlets/issues/514
22+
[#517]: https://github.com/AdguardTeam/Scriptlets/issues/517
2123

2224
## [v2.2.8] - 2025-07-08
2325

src/helpers/injector.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ export async function attachDependencies(scriptlet: Scriptlet | Redirect): Promi
1919
try {
2020
const depStr = dep.toString();
2121
const result = await minify(depStr, {
22-
compress: true,
22+
compress: {
23+
drop_debugger: false,
24+
},
2325
mangle: {
2426
// injection functions should be accessible by the same name
2527
// so we preserve their names

src/helpers/node-text-utils.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -111,11 +111,7 @@ export const replaceNodeText = (
111111
): void => {
112112
const { textContent } = node;
113113
if (textContent) {
114-
// Remove quotes' escapes for cases where scriptlet rule argument has own escaped quotes
115-
// https://github.com/AdguardTeam/Scriptlets/issues/440
116-
let modifiedText = textContent.replace(pattern, replacement)
117-
.replace(/\\'/g, "'")
118-
.replace(/\\"/g, '"');
114+
let modifiedText = textContent.replace(pattern, replacement);
119115

120116
// For websites that use Trusted Types
121117
// https://w3c.github.io/webappsec-trusted-types/dist/spec/

src/scriptlets/trusted-replace-node-text.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,26 @@ import {
9999
*/
100100
/* eslint-enable max-len */
101101
export function trustedReplaceNodeText(source, nodeName, textMatch, pattern, replacement, ...extraArgs) {
102+
// Remove quotes' escapes for cases where scriptlet rule argument has own escaped quotes
103+
// https://github.com/AdguardTeam/Scriptlets/issues/440
104+
const fixQuotes = (str) => {
105+
if (typeof str !== 'string') {
106+
return str;
107+
}
108+
return str
109+
.replace(/\\'/g, "'")
110+
.replace(/\\"/g, '"');
111+
};
112+
113+
const fixedPattern = fixQuotes(pattern);
114+
const fixedReplacement = fixQuotes(replacement);
115+
102116
const {
103117
selector,
104118
nodeNameMatch,
105119
textContentMatch,
106120
patternMatch,
107-
} = parseNodeTextParams(nodeName, textMatch, pattern);
121+
} = parseNodeTextParams(nodeName, textMatch, fixedPattern);
108122

109123
const shouldLog = extraArgs.includes('verbose');
110124

@@ -130,7 +144,7 @@ export function trustedReplaceNodeText(source, nodeName, textMatch, pattern, rep
130144
logMessage(source, `Original text content: ${originalText}`);
131145
}
132146
}
133-
replaceNodeText(source, node, patternMatch, replacement);
147+
replaceNodeText(source, node, patternMatch, fixedReplacement);
134148
if (shouldLog) {
135149
const modifiedText = node.textContent;
136150
if (modifiedText) {

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

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,35 @@ test('simple case', (assert) => {
7272
}, 1);
7373
});
7474

75-
test('simple case - check if quotes are correctly escaped', (assert) => {
75+
test('simple case - should not throw error when pattern and replacement are not provided', (assert) => {
76+
const done = assert.async();
77+
78+
const nodeName = 'div';
79+
const textMatch = 'qwerty!';
80+
81+
const text = 'qwerty!1';
82+
const expectedText = 'qwerty!1';
83+
84+
const nodeBefore = addNode('div', text);
85+
const safeNodeBefore = addNode('a', text);
86+
87+
runScriptlet(name, [nodeName, textMatch]);
88+
89+
const nodeAfter = addNode('div', text);
90+
const safeNodeAfter = addNode('span', text);
91+
setTimeout(() => {
92+
assert.strictEqual(nodeAfter.textContent, expectedText, 'text content should not be modified');
93+
assert.strictEqual(nodeBefore.textContent, expectedText, 'text content should not be modified');
94+
95+
assert.strictEqual(safeNodeAfter.textContent, text, 'non-matched node should not be affected');
96+
assert.strictEqual(safeNodeBefore.textContent, text, 'non-matched node should not be affected');
97+
98+
assert.strictEqual(window.hit, 'FIRED', 'hit function should fire');
99+
done();
100+
}, 1);
101+
});
102+
103+
test('simple case - check if quotes are correctly escaped in pattern and replacement', (assert) => {
76104
const done = assert.async();
77105

78106
const nodeName = 'div';
@@ -101,6 +129,35 @@ test('simple case - check if quotes are correctly escaped', (assert) => {
101129
}, 1);
102130
});
103131

132+
test('simple case - check if quotes are correctly escaped in result', (assert) => {
133+
const done = assert.async();
134+
135+
const nodeName = 'div';
136+
const textMatch = 'alert';
137+
const pattern = 'foo';
138+
const replacement = 'bar';
139+
const text = 'alert("\\"foo\\"")';
140+
const expectedText = 'alert("\\"bar\\"")';
141+
142+
const nodeBefore = addNode('div', text);
143+
const safeNodeBefore = addNode('a', text);
144+
145+
runScriptlet(name, [nodeName, textMatch, pattern, replacement]);
146+
147+
const nodeAfter = addNode('div', text);
148+
const safeNodeAfter = addNode('span', text);
149+
setTimeout(() => {
150+
assert.strictEqual(nodeAfter.textContent, expectedText, 'text content should be modified');
151+
assert.strictEqual(nodeBefore.textContent, expectedText, 'text content should be modified');
152+
153+
assert.strictEqual(safeNodeAfter.textContent, text, 'non-matched node should not be affected');
154+
assert.strictEqual(safeNodeBefore.textContent, text, 'non-matched node should not be affected');
155+
156+
assert.strictEqual(window.hit, 'FIRED', 'hit function should fire');
157+
done();
158+
}, 1);
159+
});
160+
104161
test('using matchers as regexes', (assert) => {
105162
const done = assert.async();
106163

0 commit comments

Comments
 (0)