From b735ac6b6abab7d5f45e15bbba3b4ba6cbf43935 Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Tue, 22 Sep 2020 09:59:04 -0400 Subject: [PATCH] Add abort-on-stack-trace scriptlet This new scriplet has become necessary as a countermeasure to new bypass mechanisms by some websites, as discussed with filter list maintainers. Also related discussion: https://github.com/AdguardTeam/Scriptlets/issues/82 Scriptlet: abort-on-stack-trace Alias: aost Argument 1: The property to trap in order to launch the stack trace matching code, ex. Math.random Argument 2: The string (needle) to match against the stack trace. If the empty string, always match. There is a special string which can be used to match inline script context, . Argument 3: Whether to log, and if so how: Empty string: do not log 1: log stack trace for all access to trapped property 2: log stack trace for defused access to trapped property 3: log stack trace for non-defused access to trapped property --- assets/resources/scriptlets.js | 95 ++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/assets/resources/scriptlets.js b/assets/resources/scriptlets.js index d09ebc7e0..8125f0369 100644 --- a/assets/resources/scriptlets.js +++ b/assets/resources/scriptlets.js @@ -191,6 +191,101 @@ })(); +/// abort-on-stack-trace.js +/// alias aost.js +// Status is currently experimental +(function() { + let chain = '{{1}}'; + let needle = '{{2}}'; + let logLevel = '{{3}}'; + const reRegexEscape = /[.*+?^${}()|[\]\\]/g; + if ( needle === '' || needle === '{{2}}' ) { + needle = '^'; + } else if ( /^\/.+\/$/.test(needle) ) { + needle = needle.slice(1,-1); + } else { + needle = needle.replace(reRegexEscape, '\\$&'); + } + const reNeedle = new RegExp(needle, 'im'); + const magic = String.fromCharCode(Math.random() * 26 + 97) + + Math.floor( + (0.25 + Math.random() * 0.75) * Number.MAX_SAFE_INTEGER + ).toString(36).slice(-8); + const log = console.log.bind(console); + const ErrorCtor = self.Error; + const mustAbort = function(err) { + let docURL = self.location.href; + const pos = docURL.indexOf('#'); + if ( pos !== -1 ) { + docURL = docURL.slice(0, pos); + } + const reDocURL = new RegExp(docURL.replace(reRegexEscape, '\\$&'), 'g'); + const stack = err.stack + .replace(/^.*?\b[gs]et\b[^\n\r]+?[\n\r]*/m, '') + .replace(reDocURL, ''); + const r = reNeedle.test(stack); + if ( + logLevel === '1' || + logLevel === '2' && r || + logLevel === '3' && !r + ) { + log(stack); + } + return r; + }; + const makeProxy = function(owner, chain) { + const pos = chain.indexOf('.'); + if ( pos === -1 ) { + let v = owner[chain]; + Object.defineProperty(owner, chain, { + get: function() { + if ( mustAbort(new ErrorCtor(magic)) ) { + throw new ReferenceError(magic); + } + return v; + }, + set: function(a) { + if ( mustAbort(new ErrorCtor(magic)) ) { + throw new ReferenceError(magic); + } + v = a; + }, + }); + return; + } + const prop = chain.slice(0, pos); + let v = owner[prop]; + chain = chain.slice(pos + 1); + if ( v ) { + makeProxy(v, chain); + return; + } + const desc = Object.getOwnPropertyDescriptor(owner, prop); + if ( desc && desc.set !== undefined ) { return; } + Object.defineProperty(owner, prop, { + get: function() { return v; }, + set: function(a) { + v = a; + if ( a instanceof Object ) { + makeProxy(a, chain); + } + } + }); + }; + const owner = window; + makeProxy(owner, chain); + const oe = window.onerror; + window.onerror = function(msg, src, line, col, error) { + if ( typeof msg === 'string' && msg.indexOf(magic) !== -1 ) { + return true; + } + if ( oe instanceof Function ) { + return oe(msg, src, line, col, error); + } + }.bind(); +})(); + + /// addEventListener-defuser.js /// alias aeld.js (function() {