Skip to content

Commit a085fff

Browse files
committed
AG-32032 fix anti-adblock detection in spoof-css scriptlet. #422
Squashed commit of the following: commit 58247fc Author: slvvko <v.leleka@adguard.com> Date: Tue Jan 27 22:53:14 2026 -0500 fix more anti-adblock detections commit d804fc2 Author: slvvko <v.leleka@adguard.com> Date: Tue Jan 27 09:48:29 2026 -0500 fix toString.toString() detection commit a11819e Author: slvvko <v.leleka@adguard.com> Date: Fri Jan 23 12:20:06 2026 -0500 fix anti-adblock detection in spoof-css scriptlet
1 parent eba8337 commit a085fff

3 files changed

Lines changed: 171 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ The format is based on [Keep a Changelog], and this project adheres to [Semantic
1717
- `prevent-constructor` scriptlet to prevent constructor calls
1818
like `new Promise()` or `new MutationObserver()` [#461].
1919

20+
### Fixed
21+
22+
- Anti-adblock detection in `spoof-css` scriptlet
23+
by using cloaked bound functions instead of Proxies [#422].
24+
25+
[#422]: https://github.com/AdguardTeam/Scriptlets/issues/422
2026
[#461]: https://github.com/AdguardTeam/Scriptlets/issues/461
2127

2228
## [v2.2.15] - 2026-01-22

src/scriptlets/spoof-css.js

Lines changed: 73 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,33 @@ export function spoofCSS(source, selectors, cssPropertyName, cssPropertyValue) {
119119
: realCssValue;
120120
};
121121

122+
/**
123+
* Cloaks a function to make it appear as native code.
124+
*
125+
* This helps avoid detection by anti-adblock scripts that check:
126+
* - `fn.toString()` for native code appearance;
127+
* - `fn.toString.toString()` for nested toString checks;
128+
* - Error stack traces for 'Proxy' keyword.
129+
*
130+
* @param {Function} fn Function to cloak.
131+
* @param {object} thisArg `this` context to bind.
132+
* @param {string} fnName Name to use for the function.
133+
*
134+
* @returns {Function} Cloaked function.
135+
*/
136+
const cloakFunc = (fn, thisArg, fnName) => {
137+
const cloakedToString = () => `function ${fnName}() { [native code] }`;
138+
// toString.toString() should return 'function toString() { [native code] }'
139+
const toStringOfToString = () => 'function toString() { [native code] }';
140+
toStringOfToString.toString = toStringOfToString;
141+
cloakedToString.toString = toStringOfToString;
142+
143+
const bound = fn.bind(thisArg);
144+
Object.defineProperty(bound, 'name', { value: fnName });
145+
Object.defineProperty(bound, 'toString', { value: cloakedToString });
146+
return bound;
147+
};
148+
122149
const setRectValue = (rect, prop, value) => {
123150
Object.defineProperty(
124151
rect,
@@ -129,10 +156,46 @@ export function spoofCSS(source, selectors, cssPropertyName, cssPropertyValue) {
129156
);
130157
};
131158

159+
/**
160+
* Creates a cloaked toString function for native-looking output.
161+
*
162+
* @param {string} fnName Function name to use in the output.
163+
*
164+
* @returns {Function} `toString` function that returns native code format.
165+
*/
166+
const cloakedToStringFactory = (fnName) => {
167+
// Use named function expression so that .name returns 'toString'
168+
const toString = function toString() {
169+
return `function ${fnName}() { [native code] }`;
170+
};
171+
// toString.toString() should return 'function toString() { [native code] }'
172+
const toStringOfToString = function toString() {
173+
return 'function toString() { [native code] }';
174+
};
175+
toStringOfToString.toString = toStringOfToString;
176+
toString.toString = toStringOfToString;
177+
return toString;
178+
};
179+
180+
// Properties that need to be bound to avoid 'Proxy' appearing in error stack traces
181+
const propsToBindSet = new Set([
182+
'__defineGetter__',
183+
'__defineSetter__',
184+
'__lookupGetter__',
185+
'__lookupSetter__',
186+
]);
187+
132188
const getter = (target, prop, receiver) => {
133189
hit(source);
134190
if (prop === 'toString') {
135-
return target.toString.bind(target);
191+
return cloakedToStringFactory(target.name || 'getComputedStyle');
192+
}
193+
// Bind introspection methods to original target to avoid 'Proxy' in error stack
194+
if (propsToBindSet.has(prop)) {
195+
const nativeFn = target[prop];
196+
if (typeof nativeFn === 'function') {
197+
return nativeFn.bind(target);
198+
}
136199
}
137200
return Reflect.get(target, prop, receiver);
138201
};
@@ -154,19 +217,18 @@ export function spoofCSS(source, selectors, cssPropertyName, cssPropertyValue) {
154217
}
155218

156219
if (prop !== 'getPropertyValue') {
157-
return CSSStyleProp.bind(target);
220+
// Use cloakFunc to avoid 'Proxy' in stack traces
221+
return cloakFunc(CSSStyleProp, target, prop);
158222
}
159223

160-
const getPropertyValueFunc = new Proxy(CSSStyleProp, {
161-
apply(target, thisArg, args) {
162-
const cssName = args[0];
163-
const cssValue = thisArg[cssName];
164-
return spoofStyle(cssName, cssValue);
165-
},
166-
get: getter,
167-
});
224+
// Create a cloaked getPropertyValue function instead of using Proxy
225+
// This avoids 'Proxy' appearing in error stack traces
226+
const getPropertyValueWrapper = function getPropertyValue(cssPropName) {
227+
const cssValue = target[cssPropName] || '';
228+
return spoofStyle(cssPropName, cssValue);
229+
};
168230

169-
return getPropertyValueFunc;
231+
return cloakFunc(getPropertyValueWrapper, target, 'getPropertyValue');
170232
},
171233
getOwnPropertyDescriptor(target, prop) {
172234
if (propToValueMap.has(prop)) {

tests/scriptlets/spoof-css.test.js

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,3 +558,95 @@ test('Native code check', (assert) => {
558558
matchElem.remove();
559559
clearGlobalProps('hit');
560560
});
561+
562+
// https://github.com/AdguardTeam/Scriptlets/issues/422
563+
test('Anti-detection: toString.toString() check', (assert) => {
564+
const matchClassName = 'testClassToStringToString';
565+
const matchElem = createElem(matchClassName);
566+
567+
const scriptletArgs = [`.${matchClassName}`, 'display', 'block'];
568+
runScriptlet(name, scriptletArgs);
569+
570+
const elGetComputedStyle = window.getComputedStyle(matchElem);
571+
const getPropertyValueFn = elGetComputedStyle.getPropertyValue;
572+
573+
// Check that toString.toString() also returns native code format
574+
// This is a common anti-adblock detection technique
575+
const toStringToString = getPropertyValueFn.toString.toString();
576+
577+
assert.ok(
578+
toStringToString.includes('[native code]'),
579+
'getPropertyValue.toString.toString() should return native code format',
580+
);
581+
582+
// Check that String(getComputedStyle.toString) returns 'function toString() { [native code] }'
583+
// Some anti-adblock scripts check: String(getComputedStyle.toString).indexOf('toString') === -1
584+
const getComputedStyleToString = String(window.getComputedStyle.toString);
585+
assert.strictEqual(
586+
getComputedStyleToString,
587+
'function toString() { [native code] }',
588+
'String(getComputedStyle.toString) should return toString native code format',
589+
);
590+
591+
// Also verify getPropertyValue.toString.toString() returns correct toString format
592+
assert.strictEqual(
593+
toStringToString,
594+
'function toString() { [native code] }',
595+
'getPropertyValue.toString.toString() should return toString native code format',
596+
);
597+
598+
// Check that toString.name returns 'toString'
599+
// Some anti-adblock scripts check: getComputedStyle.toString.name === 'toString'
600+
assert.strictEqual(
601+
window.getComputedStyle.toString.name,
602+
'toString',
603+
'getComputedStyle.toString.name should be "toString"',
604+
);
605+
606+
matchElem.remove();
607+
clearGlobalProps('hit');
608+
});
609+
610+
// https://github.com/AdguardTeam/Scriptlets/issues/422
611+
test('Anti-detection: __defineGetter__ stack trace should not contain Proxy', (assert) => {
612+
const matchClassName = 'testClassDefineGetter';
613+
const matchElem = createElem(matchClassName);
614+
615+
const scriptletArgs = [`.${matchClassName}`, 'display', 'block'];
616+
runScriptlet(name, scriptletArgs);
617+
618+
const elGetComputedStyle = window.getComputedStyle(matchElem);
619+
const getPropertyValueFn = elGetComputedStyle.getPropertyValue;
620+
621+
// Anti-adblock scripts call __defineGetter__ to trigger an error
622+
// and check if 'Proxy' appears in the stack trace
623+
let stackTrace = '';
624+
try {
625+
// eslint-disable-next-line no-restricted-properties
626+
getPropertyValueFn.__defineGetter__('test', () => {});
627+
} catch (e) {
628+
stackTrace = e.stack || '';
629+
}
630+
631+
assert.notOk(
632+
stackTrace.includes('Proxy'),
633+
'getPropertyValue.__defineGetter__ error stack should not contain "Proxy"',
634+
);
635+
636+
// Also test getComputedStyle.__defineGetter__
637+
let stackTraceGCS = '';
638+
try {
639+
// eslint-disable-next-line no-restricted-properties
640+
window.getComputedStyle.__defineGetter__('test', () => {});
641+
} catch (e) {
642+
stackTraceGCS = e.stack || '';
643+
}
644+
645+
assert.notOk(
646+
stackTraceGCS.includes('Proxy'),
647+
'getComputedStyle.__defineGetter__ error stack should not contain "Proxy"',
648+
);
649+
650+
matchElem.remove();
651+
clearGlobalProps('hit');
652+
});

0 commit comments

Comments
 (0)