Skip to content

Commit 8b4b2cc

Browse files
committed
AG-28397 Fix issue with modifying RegExp.$1 value in log-on-stack-trace and abort-on-stack-trace scriptlets. #384
Squashed commit of the following: commit 0f6b357 Merge: 1ce19f2 aefd3da Author: Adam Wróblewski <adam@adguard.com> Date: Thu Aug 22 13:44:45 2024 +0200 Merge branch 'master' into fix/AG-28397 commit 1ce19f2 Author: Adam Wróblewski <adam@adguard.com> Date: Thu Aug 22 13:42:45 2024 +0200 Fix changelog Rename getRegexpValues to backupRegExpValues Rename setPreviousRegExpValues to restoreRegExpValues Log error Add test for backupRegExpValues commit 579b9d9 Author: Adam Wróblewski <adam@adguard.com> Date: Thu Aug 22 11:59:59 2024 +0200 Change null to void commit ecb91de Author: Adam Wróblewski <adam@adguard.com> Date: Thu Aug 22 10:55:31 2024 +0200 Fix issue with modifying `RegExp.$1` value in `log-on-stack-trace` and `abort-on-stack-trace` scriptlets
1 parent aefd3da commit 8b4b2cc

17 files changed

+232
-1
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,13 @@ The format is based on [Keep a Changelog], and this project adheres to [Semantic
1818
- new values to `set-local-storage-item` and `set-session-storage-item` scriptlets:
1919
`allowed`, `denied` [#445]
2020

21+
### Fixed
22+
23+
- issue with modyfing `RegExp.$1, …, RegExp.$9` values
24+
in `log-on-stack-trace` and `abort-on-stack-trace` scriptlets [#384]
25+
2126
[Unreleased]: https://github.com/AdguardTeam/Scriptlets/compare/v1.11.16...HEAD
27+
[#384]: https://github.com/AdguardTeam/Scriptlets/issues/384
2228
[#301]: https://github.com/AdguardTeam/Scriptlets/issues/301
2329
[#439]: https://github.com/AdguardTeam/Scriptlets/issues/439
2430
[#444]: https://github.com/AdguardTeam/Scriptlets/issues/444

src/helpers/match-stack.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { toRegExp } from './string-utils';
22
import { shouldAbortInlineOrInjectedScript } from './script-source-utils';
3-
import { getNativeRegexpTest } from './regexp-utils';
3+
import { getNativeRegexpTest, backupRegExpValues, restoreRegExpValues } from './regexp-utils';
44

55
/**
66
* Checks if the stackTrace contains stackRegexp
@@ -15,7 +15,12 @@ export const matchStackTrace = (stackMatch: string | undefined, stackTrace: stri
1515
return true;
1616
}
1717

18+
const regExpValues = backupRegExpValues();
19+
1820
if (shouldAbortInlineOrInjectedScript(stackMatch, stackTrace)) {
21+
if (regExpValues.length && regExpValues[0] !== RegExp.$1) {
22+
restoreRegExpValues(regExpValues);
23+
}
1924
return true;
2025
}
2126

@@ -26,5 +31,8 @@ export const matchStackTrace = (stackMatch: string | undefined, stackTrace: stri
2631
.map((line) => line.trim()) // trim the lines
2732
.join('\n');
2833

34+
if (regExpValues.length && regExpValues[0] !== RegExp.$1) {
35+
restoreRegExpValues(regExpValues);
36+
}
2937
return getNativeRegexpTest().call(stackRegexp, refinedStackTrace);
3038
};

src/helpers/regexp-utils.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,62 @@ export const getNativeRegexpTest = (): (string: string) => boolean => {
1212
}
1313
throw new Error('RegExp.prototype.test is not a function');
1414
};
15+
16+
/**
17+
* Retrieves the values of the global RegExp.$1, …, RegExp.$9 properties
18+
* The problem is that RegExp.$1 is modified by scriptlet and according
19+
* to https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/n#description
20+
* the values of $1, …, $9 update whenever a RegExp instance makes a successful match
21+
* so we need to save these values and then change them back.
22+
* Related issue - https://github.com/AdguardTeam/Scriptlets/issues/384
23+
*
24+
* @returns {Array} An array containing the values of the RegExp.$1, …, RegExp.$9 properties.
25+
*/
26+
export const backupRegExpValues = (): Array<any> => {
27+
try {
28+
const arrayOfRegexpValues = [];
29+
for (let index = 1; index < 10; index += 1) {
30+
const value = `$${index}`;
31+
if (!(RegExp as any)[value]) {
32+
break;
33+
}
34+
arrayOfRegexpValues.push((RegExp as any)[value]);
35+
}
36+
return arrayOfRegexpValues;
37+
} catch (error) {
38+
return [];
39+
}
40+
};
41+
42+
/**
43+
* Sets previous values of the RegExp.$1, …, RegExp.$9 properties.
44+
*
45+
* @param {Array} array
46+
* @returns {void}
47+
*/
48+
export const restoreRegExpValues = (array: Array<any>): void => {
49+
if (!array.length) {
50+
return;
51+
}
52+
try {
53+
let stringPattern = '';
54+
if (array.length === 1) {
55+
stringPattern = `(${array[0]})`;
56+
} else {
57+
// Create a string pattern with a capturing group from passed array,
58+
// e.g. ['foo', 'bar', 'baz'] will create '(foo),(bar),(baz)' string
59+
stringPattern = array.reduce((accumulator, currentValue, currentIndex) => {
60+
if (currentIndex === 1) {
61+
return `(${accumulator}),(${currentValue})`;
62+
}
63+
return `${accumulator},(${currentValue})`;
64+
});
65+
}
66+
const regExpGroup = new RegExp(stringPattern);
67+
array.toString().replace(regExpGroup, '');
68+
} catch (error) {
69+
const message = `Failed to restore RegExp values: ${error}`;
70+
// eslint-disable-next-line no-console
71+
console.log(message);
72+
}
73+
};

src/scriptlets/abort-on-stack-trace.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import {
1414
toRegExp,
1515
isEmptyObject,
1616
getNativeRegexpTest,
17+
backupRegExpValues,
18+
restoreRegExpValues,
1719
} from '../helpers/index';
1820

1921
/* eslint-disable max-len */
@@ -170,4 +172,6 @@ abortOnStackTrace.injections = [
170172
isEmptyObject,
171173
getNativeRegexpTest,
172174
shouldAbortInlineOrInjectedScript,
175+
backupRegExpValues,
176+
restoreRegExpValues,
173177
];

src/scriptlets/evaldata-prune.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {
1010
// following helpers are needed for helpers above
1111
getNativeRegexpTest,
1212
shouldAbortInlineOrInjectedScript,
13+
backupRegExpValues,
14+
restoreRegExpValues,
1315
} from '../helpers/index';
1416

1517
/* eslint-disable max-len */
@@ -139,4 +141,6 @@ evalDataPrune.injections = [
139141
// following helpers are needed for helpers above
140142
getNativeRegexpTest,
141143
shouldAbortInlineOrInjectedScript,
144+
backupRegExpValues,
145+
restoreRegExpValues,
142146
];

src/scriptlets/json-prune-fetch-response.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import {
2424
getWildcardPropertyInChain,
2525
shouldAbortInlineOrInjectedScript,
2626
getNativeRegexpTest,
27+
backupRegExpValues,
28+
restoreRegExpValues,
2729
} from '../helpers/index';
2830

2931
/**
@@ -224,4 +226,6 @@ jsonPruneFetchResponse.injections = [
224226
getWildcardPropertyInChain,
225227
shouldAbortInlineOrInjectedScript,
226228
getNativeRegexpTest,
229+
backupRegExpValues,
230+
restoreRegExpValues,
227231
];

src/scriptlets/json-prune-xhr-response.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import {
2222
getWildcardPropertyInChain,
2323
shouldAbortInlineOrInjectedScript,
2424
getNativeRegexpTest,
25+
backupRegExpValues,
26+
restoreRegExpValues,
2527
} from '../helpers/index';
2628

2729
/**
@@ -365,4 +367,6 @@ jsonPruneXhrResponse.injections = [
365367
getWildcardPropertyInChain,
366368
shouldAbortInlineOrInjectedScript,
367369
getNativeRegexpTest,
370+
backupRegExpValues,
371+
restoreRegExpValues,
368372
];

src/scriptlets/json-prune.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {
1010
toRegExp,
1111
getNativeRegexpTest,
1212
shouldAbortInlineOrInjectedScript,
13+
backupRegExpValues,
14+
restoreRegExpValues,
1315
} from '../helpers/index';
1416

1517
/* eslint-disable max-len */
@@ -158,4 +160,6 @@ jsonPrune.injections = [
158160
toRegExp,
159161
getNativeRegexpTest,
160162
shouldAbortInlineOrInjectedScript,
163+
backupRegExpValues,
164+
restoreRegExpValues,
161165
];

src/scriptlets/log-on-stack-trace.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
// following helpers should be imported and injected
77
// because they are used by helpers above
88
isEmptyObject,
9+
backupRegExpValues,
10+
restoreRegExpValues,
911
} from '../helpers/index';
1012

1113
/* eslint-disable max-len */
@@ -37,6 +39,8 @@ export function logOnStacktrace(source, property) {
3739
}
3840

3941
const refineStackTrace = (stackString) => {
42+
const regExpValues = backupRegExpValues();
43+
4044
// Split stack trace string by lines and remove first two elements ('Error' and getter call)
4145
// Remove ' at ' at the start of each string
4246
const stackSteps = stackString.split('\n').slice(2).map((line) => line.replace(/ {4}at /, ''));
@@ -68,6 +72,11 @@ export function logOnStacktrace(source, property) {
6872
/* eslint-disable-next-line prefer-destructuring */
6973
logInfoObject[pair[0]] = pair[1];
7074
});
75+
76+
if (regExpValues.length && regExpValues[0] !== RegExp.$1) {
77+
restoreRegExpValues(regExpValues);
78+
}
79+
7180
return logInfoObject;
7281
};
7382

@@ -120,4 +129,6 @@ logOnStacktrace.injections = [
120129
hit,
121130
logMessage,
122131
isEmptyObject,
132+
backupRegExpValues,
133+
restoreRegExpValues,
123134
];

src/scriptlets/set-constant.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import {
2121
getNativeRegexpTest,
2222
setPropertyAccess,
2323
toRegExp,
24+
backupRegExpValues,
25+
restoreRegExpValues,
2426
} from '../helpers/index';
2527

2628
/* eslint-disable max-len */
@@ -456,4 +458,6 @@ setConstant.injections = [
456458
getNativeRegexpTest,
457459
setPropertyAccess,
458460
toRegExp,
461+
backupRegExpValues,
462+
restoreRegExpValues,
459463
];

src/scriptlets/trusted-prune-inbound-object.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {
1212
getNativeRegexpTest,
1313
shouldAbortInlineOrInjectedScript,
1414
isEmptyObject,
15+
backupRegExpValues,
16+
restoreRegExpValues,
1517
} from '../helpers/index';
1618

1719
/* eslint-disable max-len */
@@ -148,4 +150,6 @@ trustedPruneInboundObject.injections = [
148150
getNativeRegexpTest,
149151
shouldAbortInlineOrInjectedScript,
150152
isEmptyObject,
153+
backupRegExpValues,
154+
restoreRegExpValues,
151155
];

src/scriptlets/trusted-replace-outbound-text.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
getNativeRegexpTest,
1010
toRegExp,
1111
isEmptyObject,
12+
backupRegExpValues,
13+
restoreRegExpValues,
1214
} from '../helpers/index';
1315

1416
/* eslint-disable max-len */
@@ -295,4 +297,6 @@ trustedReplaceOutboundText.injections = [
295297
getNativeRegexpTest,
296298
toRegExp,
297299
isEmptyObject,
300+
backupRegExpValues,
301+
restoreRegExpValues,
298302
];

src/scriptlets/trusted-set-constant.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import {
2121
// following helpers should be imported and injected
2222
// because they are used by helpers above
2323
shouldAbortInlineOrInjectedScript,
24+
backupRegExpValues,
25+
restoreRegExpValues,
2426
} from '../helpers/index';
2527

2628
/* eslint-disable max-len */
@@ -278,4 +280,6 @@ trustedSetConstant.injections = [
278280
// following helpers should be imported and injected
279281
// because they are used by helpers above
280282
shouldAbortInlineOrInjectedScript,
283+
backupRegExpValues,
284+
restoreRegExpValues,
281285
];

src/scriptlets/trusted-suppress-native-method.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import {
2020
isStringMatched,
2121
isArrayMatched,
2222
isObjectMatched,
23+
backupRegExpValues,
24+
restoreRegExpValues,
2325
} from '../helpers/index';
2426

2527
/* eslint-disable max-len */
@@ -230,4 +232,6 @@ trustedSuppressNativeMethod.injections = [
230232
isStringMatched,
231233
isArrayMatched,
232234
isObjectMatched,
235+
backupRegExpValues,
236+
restoreRegExpValues,
233237
];

tests/helpers/regexp-utils.spec.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { restoreRegExpValues, backupRegExpValues } from '../../src/helpers';
2+
3+
test('restoreRegExpValues() check if correct value have been set', async () => {
4+
restoreRegExpValues(['foo']);
5+
expect(RegExp.$1).toBe('foo');
6+
expect(RegExp.$2).toBe('');
7+
});
8+
9+
test('restoreRegExpValues() check if correct values have been set', async () => {
10+
restoreRegExpValues(['test', 'abc', 'xyz', 'aaaa', '123']);
11+
expect(RegExp.$1).toBe('test');
12+
expect(RegExp.$2).toBe('abc');
13+
expect(RegExp.$3).toBe('xyz');
14+
expect(RegExp.$4).toBe('aaaa');
15+
expect(RegExp.$5).toBe('123');
16+
expect(RegExp.$6).toBe('');
17+
});
18+
19+
test('backupRegExpValues() and restoreRegExpValues(), modify values and restore them', async () => {
20+
const regExp = /(\w+)\s(\w+)/;
21+
const string = 'div a';
22+
string.replace(regExp, '$2, $1');
23+
24+
expect(RegExp.$1).toBe('div');
25+
expect(RegExp.$2).toBe('a');
26+
27+
const backupRegexp = backupRegExpValues();
28+
29+
const regExp2 = /(\w+)\s(\w+)/;
30+
const string2 = 'qwerty zxcvbn';
31+
string2.replace(regExp2, '$2, $1');
32+
33+
expect(RegExp.$1).toBe('qwerty');
34+
expect(RegExp.$2).toBe('zxcvbn');
35+
36+
restoreRegExpValues(backupRegexp);
37+
38+
expect(RegExp.$1).toBe('div');
39+
expect(RegExp.$2).toBe('a');
40+
});

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,3 +492,41 @@ test('abort RegExp, matches stack', (assert) => {
492492
);
493493
assert.strictEqual(window.hit, 'FIRED', 'hit fired');
494494
});
495+
496+
test('abort document.createElement, matches stack', (assert) => {
497+
const property = 'document.createElement';
498+
const stackMatch = 'Object.createElemenTest';
499+
const scriptletArgs = [property, stackMatch];
500+
let testPassed = false;
501+
502+
runScriptlet(name, scriptletArgs);
503+
504+
function testCreateElement() {
505+
const regExp = /(\w+)\s(\w+)/;
506+
const string = 'div a';
507+
const testString = string.replace(regExp, '$2, $1');
508+
const div = document.createElement(RegExp.$1);
509+
div.textContent = testString;
510+
testPassed = true;
511+
}
512+
513+
const matchTestCreateElement = {
514+
createElemenTest: () => {
515+
const regExp = /(\w+)\s(\w+)/;
516+
const string = 'div a';
517+
const testString = string.replace(regExp, '$2, $1');
518+
const div = document.createElement(RegExp.$1);
519+
div.textContent = testString;
520+
},
521+
};
522+
523+
testCreateElement();
524+
525+
assert.throws(
526+
matchTestCreateElement.createElemenTest,
527+
/ReferenceError/,
528+
`Reference error thrown when trying to access property ${property}`,
529+
);
530+
assert.strictEqual(testPassed, true, 'testPassed set to true');
531+
assert.strictEqual(window.hit, 'FIRED', 'hit fired');
532+
});

0 commit comments

Comments
 (0)