From 57f2998c93981ab1176a38cdc8ce51fc096a56bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Wr=C3=B3blewski?= Date: Mon, 28 Jul 2025 15:47:55 +0200 Subject: [PATCH 1/4] =?UTF-8?q?Fix=20'trusted-replace-node-text'=20?= =?UTF-8?q?=E2=80=94=20some=20quotes=20are=20incorrectly=20escaped?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 ++ src/helpers/injector.ts | 4 ++- src/helpers/node-text-utils.ts | 6 +--- src/scriptlets/trusted-replace-node-text.js | 14 +++++++-- .../trusted-replace-node-text.test.js | 31 ++++++++++++++++++- 5 files changed, 48 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c0e9b71..64093d92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,10 +14,12 @@ The format is based on [Keep a Changelog], and this project adheres to [Semantic ### Fixed +- issue with incorrectly escaped quotes in `trusted-replace-node-text` scriptlet [#517]. - issue with `TrustedScriptURL` in `prevent-element-src-loading` scriptlet [#514]. [Unreleased]: https://github.com/AdguardTeam/Scriptlets/compare/v2.2.8...HEAD [#514]: https://github.com/AdguardTeam/Scriptlets/issues/514 +[#517]: https://github.com/AdguardTeam/Scriptlets/issues/517 ## [v2.2.8] - 2025-07-08 diff --git a/src/helpers/injector.ts b/src/helpers/injector.ts index 46bd8ab6..eba85354 100644 --- a/src/helpers/injector.ts +++ b/src/helpers/injector.ts @@ -19,7 +19,9 @@ export async function attachDependencies(scriptlet: Scriptlet | Redirect): Promi try { const depStr = dep.toString(); const result = await minify(depStr, { - compress: true, + compress: { + drop_debugger: false, + }, mangle: { // injection functions should be accessible by the same name // so we preserve their names diff --git a/src/helpers/node-text-utils.ts b/src/helpers/node-text-utils.ts index 56e90bf1..d56c276d 100644 --- a/src/helpers/node-text-utils.ts +++ b/src/helpers/node-text-utils.ts @@ -111,11 +111,7 @@ export const replaceNodeText = ( ): void => { const { textContent } = node; if (textContent) { - // Remove quotes' escapes for cases where scriptlet rule argument has own escaped quotes - // https://github.com/AdguardTeam/Scriptlets/issues/440 - let modifiedText = textContent.replace(pattern, replacement) - .replace(/\\'/g, "'") - .replace(/\\"/g, '"'); + let modifiedText = textContent.replace(pattern, replacement); // For websites that use Trusted Types // https://w3c.github.io/webappsec-trusted-types/dist/spec/ diff --git a/src/scriptlets/trusted-replace-node-text.js b/src/scriptlets/trusted-replace-node-text.js index 29625bea..8a023447 100644 --- a/src/scriptlets/trusted-replace-node-text.js +++ b/src/scriptlets/trusted-replace-node-text.js @@ -99,12 +99,22 @@ import { */ /* eslint-enable max-len */ export function trustedReplaceNodeText(source, nodeName, textMatch, pattern, replacement, ...extraArgs) { + // Remove quotes' escapes for cases where scriptlet rule argument has own escaped quotes + // https://github.com/AdguardTeam/Scriptlets/issues/440 + const fixedPattern = pattern + .replace(/\\'/g, "'") + .replace(/\\"/g, '"'); + + const fixedReplacement = replacement + .replace(/\\'/g, "'") + .replace(/\\"/g, '"'); + const { selector, nodeNameMatch, textContentMatch, patternMatch, - } = parseNodeTextParams(nodeName, textMatch, pattern); + } = parseNodeTextParams(nodeName, textMatch, fixedPattern); const shouldLog = extraArgs.includes('verbose'); @@ -130,7 +140,7 @@ export function trustedReplaceNodeText(source, nodeName, textMatch, pattern, rep logMessage(source, `Original text content: ${originalText}`); } } - replaceNodeText(source, node, patternMatch, replacement); + replaceNodeText(source, node, patternMatch, fixedReplacement); if (shouldLog) { const modifiedText = node.textContent; if (modifiedText) { diff --git a/tests/scriptlets/trusted-replace-node-text.test.js b/tests/scriptlets/trusted-replace-node-text.test.js index 3d115b70..e43071ad 100644 --- a/tests/scriptlets/trusted-replace-node-text.test.js +++ b/tests/scriptlets/trusted-replace-node-text.test.js @@ -72,7 +72,7 @@ test('simple case', (assert) => { }, 1); }); -test('simple case - check if quotes are correctly escaped', (assert) => { +test('simple case - check if quotes are correctly escaped in pattern and replacement', (assert) => { const done = assert.async(); const nodeName = 'div'; @@ -101,6 +101,35 @@ test('simple case - check if quotes are correctly escaped', (assert) => { }, 1); }); +test('simple case - check if quotes are correctly escaped in result', (assert) => { + const done = assert.async(); + + const nodeName = 'div'; + const textMatch = 'alert'; + const pattern = 'foo'; + const replacement = 'bar'; + const text = 'alert("\\"foo\\"")'; + const expectedText = 'alert("\\"bar\\"")'; + + const nodeBefore = addNode('div', text); + const safeNodeBefore = addNode('a', text); + + runScriptlet(name, [nodeName, textMatch, pattern, replacement]); + + const nodeAfter = addNode('div', text); + const safeNodeAfter = addNode('span', text); + setTimeout(() => { + assert.strictEqual(nodeAfter.textContent, expectedText, 'text content should be modified'); + assert.strictEqual(nodeBefore.textContent, expectedText, 'text content should be modified'); + + assert.strictEqual(safeNodeAfter.textContent, text, 'non-matched node should not be affected'); + assert.strictEqual(safeNodeBefore.textContent, text, 'non-matched node should not be affected'); + + assert.strictEqual(window.hit, 'FIRED', 'hit function should fire'); + done(); + }, 1); +}); + test('using matchers as regexes', (assert) => { const done = assert.async(); From 99e6933d6e18f62158224631d9807086e376c07e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Wr=C3=B3blewski?= Date: Mon, 28 Jul 2025 16:31:06 +0200 Subject: [PATCH 2/4] Add fixQuotes function --- src/scriptlets/trusted-replace-node-text.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/scriptlets/trusted-replace-node-text.js b/src/scriptlets/trusted-replace-node-text.js index 8a023447..2029c94c 100644 --- a/src/scriptlets/trusted-replace-node-text.js +++ b/src/scriptlets/trusted-replace-node-text.js @@ -101,13 +101,14 @@ import { export function trustedReplaceNodeText(source, nodeName, textMatch, pattern, replacement, ...extraArgs) { // Remove quotes' escapes for cases where scriptlet rule argument has own escaped quotes // https://github.com/AdguardTeam/Scriptlets/issues/440 - const fixedPattern = pattern - .replace(/\\'/g, "'") - .replace(/\\"/g, '"'); + const fixQuotes = (str) => { + return str + .replace(/\\'/g, "'") + .replace(/\\"/g, '"'); + }; - const fixedReplacement = replacement - .replace(/\\'/g, "'") - .replace(/\\"/g, '"'); + const fixedPattern = fixQuotes(pattern); + const fixedReplacement = fixQuotes(replacement); const { selector, From eba43cc884b1a02bc2e93a6b40fa3a02ba717ac3 Mon Sep 17 00:00:00 2001 From: Slava Leleka Date: Mon, 28 Jul 2025 17:35:01 +0300 Subject: [PATCH 3/4] Update changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64093d92..f4ec9ae8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,8 +14,8 @@ The format is based on [Keep a Changelog], and this project adheres to [Semantic ### Fixed -- issue with incorrectly escaped quotes in `trusted-replace-node-text` scriptlet [#517]. -- issue with `TrustedScriptURL` in `prevent-element-src-loading` scriptlet [#514]. +- Incorrectly escaped quotes in `trusted-replace-node-text` scriptlet [#517]. +- `TrustedScriptURL` in `prevent-element-src-loading` scriptlet [#514]. [Unreleased]: https://github.com/AdguardTeam/Scriptlets/compare/v2.2.8...HEAD [#514]: https://github.com/AdguardTeam/Scriptlets/issues/514 From 4530dda1d944a61d7e1a56b52dfaa1e763d2538d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Wr=C3=B3blewski?= Date: Mon, 28 Jul 2025 21:36:59 +0200 Subject: [PATCH 4/4] Fix fixQuotes function - handle case where pattern and replacement are not provided --- src/scriptlets/trusted-replace-node-text.js | 3 ++ .../trusted-replace-node-text.test.js | 28 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/scriptlets/trusted-replace-node-text.js b/src/scriptlets/trusted-replace-node-text.js index 2029c94c..f047a1a9 100644 --- a/src/scriptlets/trusted-replace-node-text.js +++ b/src/scriptlets/trusted-replace-node-text.js @@ -102,6 +102,9 @@ export function trustedReplaceNodeText(source, nodeName, textMatch, pattern, rep // Remove quotes' escapes for cases where scriptlet rule argument has own escaped quotes // https://github.com/AdguardTeam/Scriptlets/issues/440 const fixQuotes = (str) => { + if (typeof str !== 'string') { + return str; + } return str .replace(/\\'/g, "'") .replace(/\\"/g, '"'); diff --git a/tests/scriptlets/trusted-replace-node-text.test.js b/tests/scriptlets/trusted-replace-node-text.test.js index e43071ad..43122004 100644 --- a/tests/scriptlets/trusted-replace-node-text.test.js +++ b/tests/scriptlets/trusted-replace-node-text.test.js @@ -72,6 +72,34 @@ test('simple case', (assert) => { }, 1); }); +test('simple case - should not throw error when pattern and replacement are not provided', (assert) => { + const done = assert.async(); + + const nodeName = 'div'; + const textMatch = 'qwerty!'; + + const text = 'qwerty!1'; + const expectedText = 'qwerty!1'; + + const nodeBefore = addNode('div', text); + const safeNodeBefore = addNode('a', text); + + runScriptlet(name, [nodeName, textMatch]); + + const nodeAfter = addNode('div', text); + const safeNodeAfter = addNode('span', text); + setTimeout(() => { + assert.strictEqual(nodeAfter.textContent, expectedText, 'text content should not be modified'); + assert.strictEqual(nodeBefore.textContent, expectedText, 'text content should not be modified'); + + assert.strictEqual(safeNodeAfter.textContent, text, 'non-matched node should not be affected'); + assert.strictEqual(safeNodeBefore.textContent, text, 'non-matched node should not be affected'); + + assert.strictEqual(window.hit, 'FIRED', 'hit function should fire'); + done(); + }, 1); +}); + test('simple case - check if quotes are correctly escaped in pattern and replacement', (assert) => { const done = assert.async();