Scriptlets icon indicating copy to clipboard operation
Scriptlets copied to clipboard

Improve 'prevent-xhr' — set responseURL on state 2 and skip first state if onreadystatechange was declared after open call

Open AdamWr opened this issue 11 months ago • 0 comments

It seems that there are 2 issues in prevent-xhr.

First one is that we set responseURL on state 4 but it looks like that it should be done on state 2.

https://github.com/AdguardTeam/Scriptlets/blob/3ba245f96237915bbff6fe80c7400b03c0f5014d/src/scriptlets/prevent-xhr.js#L191-L201

Example detection code:
(() => {
    const checkDetection = (detected) => {
        if (detected) {
            alert('AdBlocker detected');
            return;
        }
        const allEventsPassed = xhrEvents.every(state => state);
        if (!allEventsPassed) {
            alert('AdBlocker detected');
            return;
        }
        // No AdBlocker detected, do something
        console.log('No AdBlocker detected');
    };
    const url = 'https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js';
    const xhrEvents = [false, false, false, false];
    const xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function () {
        xhrEvents[xhr.readyState - 1] = true;
        if (xhr.readyState >= 2 && xhr.responseURL !== url) {
            checkDetection(true);
        }
        if (xhr.readyState === 4) {
            checkDetection();
        }
    };
    try {
        xhr.open("GET", url, true);
        xhr.send();
    } catch (ex) {
        console.error(ex);
        checkDetection(true);
    }
})();

Second issue is that onreadystatechange depends on the order how xhr has been called. If onreadystatechange is declared before open call then there will be 4 states (1-4), but if it's declared after open call then there should be 3 states (2-4).

Example detection code:
(() => {
    let detectionState = false;
    const checkDetection = (detected) => {
        if (detectionState) {
            return;
        }
        
        if (detected) {
            alert('AdBlocker detected');
            detectionState = true;
            return;
        }
        const allEventsPassed = xhrEvents.every(state => state);
        if (!allEventsPassed) {
            alert('AdBlocker detected');
            detectionState = true;
            return;
        }
        // No AdBlocker detected, do something
        console.log('No AdBlocker detected');
    };
    const url = 'https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js';
    const xhrEvents = [false, false, false];
    const xhr = new XMLHttpRequest();
    
    try {
        xhr.open("GET", url, true);
        xhr.onreadystatechange = function () {
            if(xhr.readyState === 1) {
                checkDetection(true);
                return;
            }
            xhrEvents[xhr.readyState - 2] = true;
            if (xhr.readyState >= 2 && xhr.responseURL !== url) {
                checkDetection(true);
                return;
            }
            if (xhr.readyState === 4) {
                checkDetection();
            }
        };
        xhr.send();
    } catch (ex) {
        console.error(ex);
        checkDetection(true);
    }
})();

Maybe we could just check in xhr.open if xhr.onreadystatechange is a function, if so, it was declared before xhr.open and 1-4 states should be invoked. But if not, then first state should be skipped. Or perhaps there is a better solution.


Steps to reproduce:

  1. Add to user rules:
example.org#%#//scriptlet('prevent-xhr', 'pagead2.googlesyndication.com')
  1. Go to - https://example.org/
  2. Run mentioned scripts in browser console

AdamWr avatar Mar 18 '25 08:03 AdamWr