brave/adblock-rust

Rule `protruyen.com#%#//scriptlet("abort-current-inline-script", "Object", "$._Eu")` crash `Object[`${prop}`]`

tachibana-shin opened this issue · 1 comments

Sorry I don't have time to investigate further. Specific problems are as follows:

In the ABPVN filter there is a filter rule as follows:

protruyen.com#%#//scriptlet("abort-current-inline-script", "Object", "$._Eu")

after i enabled the filter and went to protruyen.com i noticed that a code was forwarded to the site calling max stack call
image
or
image
i checked and found a code injected:

Code brave inject to website:

(function() {
    const scriptletGlobals = new Map();
    let deAmpEnabled = true;

    try {
        (function() {
            const target = 'Object';
            if (target === '' || target === '{{1}}') {
                return;
            }
            const reRegexEscape = /[.*+?^${}()|[\]\\]/g;
            const needle = '$._Eu';
            const reNeedle = (()=>{
                if (needle === '' || needle === '{{2}}') {
                    return /^/;
                }
                if (/^\/.+\/$/.test(needle)) {
                    return new RegExp(needle.slice(1, -1));
                }
                return new RegExp(needle.replace(reRegexEscape, '\\$&'));
            }
            )();
            const context = '{{3}}';
            const reContext = (()=>{
                if (context === '' || context === '{{3}}') {
                    return;
                }
                if (/^\/.+\/$/.test(context)) {
                    return new RegExp(context.slice(1, -1));
                }
                return new RegExp(context.replace(reRegexEscape, '\\$&'));
            }
            )();
            const thisScript = document.currentScript;
            const chain = target.split('.');
            let owner = window;
            let prop;
            for (; ; ) {
                prop = chain.shift();
                if (chain.length === 0) {
                    break;
                }
                owner = owner[prop];
                if (owner instanceof Object === false) {
                    return;
                }
            }
            let value;
            let desc = Object.getOwnPropertyDescriptor(owner, prop);
            if (desc instanceof Object === false || desc.get instanceof Function === false) {
                value = owner[prop];
                desc = undefined;
            }
            const magic = String.fromCharCode(Date.now() % 26 + 97) + Math.floor(Math.random() * 982451653 + 982451653).toString(36);
            const scriptTexts = new WeakMap();
            const getScriptText = elem=>{
                let text = elem.textContent;
                if (text.trim() !== '') {
                    return text;
                }
                if (scriptTexts.has(elem)) {
                    return scriptTexts.get(elem);
                }
                const [,mime,content] = /^data:([^,]*),(.+)$/.exec(elem.src.trim()) || ['', '', ''];
                try {
                    switch (true) {
                    case mime.endsWith(';base64'):
                        text = self.atob(content);
                        break;
                    default:
                        text = self.decodeURIComponent(content);
                        break;
                    }
                } catch (ex) {}
                scriptTexts.set(elem, text);
                return text;
            }
            ;
            const validate = ()=>{
                const e = document.currentScript;
                if (e instanceof HTMLScriptElement === false) {
                    return;
                }
                if (e === thisScript) {
                    return;
                }
                if (reContext !== undefined && reContext.test(e.src) === false) {
                    return;
                }
                if (reNeedle.test(getScriptText(e)) === false) {
                    return;
                }
                throw new ReferenceError(magic);
            }
            ; // owner = Window, prop = "Object", desc = undefined, value = Object // owner[prop]
            Object.defineProperty(owner, prop, {
                get: function() {
                    validate(); // VM8:82 Uncaught RangeError: Maximum call stack size exceeded
                    return desc instanceof Object ? desc.get.call(owner) : value; // or this
                    // because desc is undefined. But `Object` is `window.Object` -> call this function recursive
                },
                set: function(a) {
                    validate();
                    if (desc instanceof Object) {
                        desc.set.call(owner, a);
                    } else {
                        value = a;
                    }
                }
            });
            const oe = window.onerror;
            window.onerror = function(msg) {
                if (typeof msg === 'string' && msg.includes(magic)) {
                    return true;
                }
                if (oe instanceof Function) {
                    return oe.apply(this, arguments);
                }
            }
            .bind();
        }
        )();

    } catch (e) {}
}
)()

and i try accessing anything from Object same error happens. And I conclude that this is due to the code that adblock-rust injected

Solution

My suggestion is that instead of using general-purpose test instanceof Object will store Object to check primitive value:

(function() {
    const scriptletGlobals = new Map();
    let deAmpEnabled = true;

    try {
        (function() {
            const target = 'Object';
            if (target === '' || target === '{{1}}') {
                return;
            }
            const reRegexEscape = /[.*+?^${}()|[\]\\]/g;
            const needle = '$._Eu';
            const reNeedle = (()=>{
                if (needle === '' || needle === '{{2}}') {
                    return /^/;
                }
                if (/^\/.+\/$/.test(needle)) {
                    return new RegExp(needle.slice(1, -1));
                }
                return new RegExp(needle.replace(reRegexEscape, '\\$&'));
            }
            )();
            const context = '{{3}}';
            const reContext = (()=>{
                if (context === '' || context === '{{3}}') {
                    return;
                }
                if (/^\/.+\/$/.test(context)) {
                    return new RegExp(context.slice(1, -1));
                }
                return new RegExp(context.replace(reRegexEscape, '\\$&'));
            }
            )();
            const thisScript = document.currentScript;
            const chain = target.split('.');
            let owner = window;
            let prop;
            for (; ; ) {
                prop = chain.shift();
                if (chain.length === 0) {
                    break;
                }
                owner = owner[prop];
                if (owner instanceof Object === false) {
                    return;
                }
            }
            let value;
            let desc = Object.getOwnPropertyDescriptor(owner, prop);
            if (desc instanceof Object === false || desc.get instanceof Function === false) {
                value = owner[prop];
                desc = undefined;
            }
            const magic = String.fromCharCode(Date.now() % 26 + 97) + Math.floor(Math.random() * 982451653 + 982451653).toString(36);
            const scriptTexts = new WeakMap();
            const getScriptText = elem=>{
                let text = elem.textContent;
                if (text.trim() !== '') {
                    return text;
                }
                if (scriptTexts.has(elem)) {
                    return scriptTexts.get(elem);
                }
                const [,mime,content] = /^data:([^,]*),(.+)$/.exec(elem.src.trim()) || ['', '', ''];
                try {
                    switch (true) {
                    case mime.endsWith(';base64'):
                        text = self.atob(content);
                        break;
                    default:
                        text = self.decodeURIComponent(content);
                        break;
                    }
                } catch (ex) {}
                scriptTexts.set(elem, text);
                return text;
            }
            ;
            const validate = ()=>{
                const e = document.currentScript;
                if (e instanceof HTMLScriptElement === false) {
                    return;
                }
                if (e === thisScript) {
                    return;
                }
                if (reContext !== undefined && reContext.test(e.src) === false) {
                    return;
                }
                if (reNeedle.test(getScriptText(e)) === false) {
                    return;
                }
                throw new ReferenceError(magic);
            }
            ; // owner = Window, prop = "Object", desc = undefined, value = Object // owner[prop]
+         const rawObj = Object
            Object.defineProperty(owner, prop, {
                get: function() {
                    validate(); // VM8:82 Uncaught RangeError: Maximum call stack size exceeded
-                   return desc instanceof Object ? desc.get.call(owner) : value; // or this
+                  return desc instanceof rawObj ? desc.get.call(owner) : value; // or this
                    // because desc is undefined. But `Object` is `window.Object` -> call this function recursive
                },
                set: function(a) {
                    validate();
                    if (desc instanceof Object) {
                        desc.set.call(owner, a);
                    } else {
                        value = a;
                    }
                }
            });
            const oe = window.onerror;
            window.onerror = function(msg) {
                if (typeof msg === 'string' && msg.includes(magic)) {
                    return true;
                }
                if (oe instanceof Function) {
                    return oe.apply(this, arguments);
                }
            }
            .bind();
        }
        )();

    } catch (e) {}
}
)()

As mentioned in brave/brave-browser#31098 - the issue is between the protruyen.com##+js(acs, Object, $._Eu) filter in ABPVN and the site itself, so there's not much we can do here.