← Back to Blog
Published: Wed Oct 07 2026

The Invisible Bot: Bypassing Anti-Bot Systems with JS Injection and Local IPC

In the modern web scraping and automation landscape, the arms race between bot developers and anti-bot protection systems (like Cloudflare Turnstile, DataDome, and Akamai) is relentless. Traditional browser automation frameworks like Selenium, Puppeteer, and Playwright (even stealth variants like Patchright) are increasingly easily detected. They often leave behind subtle fingerprints: modified browser binaries, exposed Chrome DevTools Protocol (CDP) variables, or anomalous TLS handshakes.

But what if you didn't need to spoof a legitimate browser? What if you could just use one?

In this article, we will explore a highly effective, nearly undetectable automation architecture: using userscripts (like Tampermonkey) to inject JavaScript into a target webpage, which then communicates with a local backend application (e.g., a C# controller) via Inter-Process Communication (IPC) such as WebSockets or local HTTP polling. We will dive into advanced techniques used to bypass modern browser security policies, explore how website administrators attempt to detect these attacks, and ultimately demonstrate why most of these defenses can be completely bypassed by a determined attacker.

The Architecture: "Bring Your Own Browser"

The core concept relies on decoupling the browser instance from the automation logic.

Instead of launching a browser via a WebDriver or CDP, the user opens their standard, everyday browser (Chrome, Firefox, Edge). They navigate to the target website and log in manually. Because this is a genuine user session, it effortlessly passes all CAPTCHAs, behavioral checks, and IP reputation scores.

Once the page loads, a Tampermonkey script automatically injects a payload into the DOM. This payload acts as a bridge. It establishes a connection to a local server running on the user's machine (our C# controller).

The flow looks like this:

  1. C# Controller sends a JSON command over the local network (e.g., {"action": "clickCheckout"}).
  2. Injected JS receives the command, locates the element in the real DOM, and dispatches a trusted click event.
  3. Injected JS scrapes the resulting DOM state and sends the data back to the local server.
  4. C# Controller processes the data and decides the next action.

Why This Defeats Standard Anti-Bot Systems

This approach is incredibly powerful because it sidesteps the primary detection vectors used by security vendors:

Advanced Evasion Techniques

Modern web applications employ strict security policies and performance optimizations that can hinder simple DOM scraping. Sophisticated injected scripts utilize several advanced techniques to maintain persistence and extract data.

1. Bypassing CSP with Trusted Types

Many secure sites use Content Security Policy (CSP) to block eval() and inline scripts. However, if the site implements the TrustedTypes API, an injected script can create a custom policy to bypass these restrictions and execute dynamic code.

let secureCode = "console.log('Injected payload running');";
if (window.trustedTypes && window.trustedTypes.createPolicy) {
    const policy = window.trustedTypes.createPolicy('tm-loader-' + Math.random().toString(36).substring(2, 10), {
        createScript: (s) => s
    });
    secureCode = policy.createScript(secureCode);
}
eval(secureCode); // Bypasses standard CSP eval restrictions

2. Defeating Background Tab Throttling

Modern browsers (like Chrome) aggressively throttle setTimeout and setInterval to 1000ms or more when a tab is in the background. To maintain a high-speed polling loop with the local C2 server, injected scripts use two primary workarounds:

3. Visibility and Focus Spoofing

Many Single Page Applications (SPAs) pause rendering or disconnect WebSockets when the user switches tabs. Injected scripts counter this by overriding native browser APIs to force the page to believe it is always visible and focused.

Object.defineProperties(Document.prototype, {
    'hidden': { get: () => false, configurable: true },
    'visibilityState': { get: () => 'visible', configurable: true },
    'hasFocus': { value: () => true, configurable: true }
});

4. Framework Memory Hooking (React/Angular)

Relying on DOM scraping is fragile; UI updates break selectors. Instead, advanced scripts hook directly into the internal state of frontend frameworks. By traversing the DOM and looking for properties like __reactFiber$ (React) or __ngContext__ (Angular), the script can extract the raw JSON state of the application directly from memory, bypassing the UI entirely.

The Code: Building the IPC Bridge

To demonstrate the communication layer, here is an example of how the Tampermonkey script communicates with the local C# server using HTTP polling (which often bypasses mixed-content WebSocket restrictions).

1. The JavaScript Client (Tampermonkey Script)

// ==UserScript==
// @name         Local Automation Bridge
// @namespace    automation.bridge
// @version      1.0.0
// @match        https://target-website.com/*
// @grant        GM_xmlhttpRequest
// @run-at       document-start
// ==/UserScript==

(function () {
    'use strict';

    const API_BASE = 'http://127.0.0.1:8081/api';
    const INSTANCE_ID = 'bot_' + Math.random().toString(36).substr(2, 9);
    const actionHandlers = Object.create(null);

    function registerAction(name, handler) {
        actionHandlers[name] = handler;
    }

    // Use GM_xmlhttpRequest to bypass CORS restrictions
    function tmFetch(url, options = {}) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: options.method || 'GET',
                url: url,
                data: options.body,
                headers: { 'Content-Type': 'application/json' },
                onload: (res) => resolve(JSON.parse(res.responseText)),
                onerror: reject
            });
        });
    }

    async function pollServer() {
        try {
            const data = await tmFetch(`${API_BASE}/command/${INSTANCE_ID}`);
            if (data && data.command) {
                const { id, command } = data;
                const handler = actionHandlers[command.type];
                
                let result = "";
                if (handler) {
                    result = await handler(command.payload);
                } else {
                    result = "Unknown command";
                }

                // Send result back
                await tmFetch(`${API_BASE}/result/${INSTANCE_ID}`, {
                    method: 'POST',
                    body: JSON.stringify({ task_id: id, output: result })
                });
            }
        } catch (e) {
            // Server offline or polling timeout
        } finally {
            setTimeout(pollServer, 1000);
        }
    }

    registerAction('CLICK_CHECKOUT', async (payload) => {
        document.querySelector('.checkout-btn').click();
        return "Clicked";
    });

    pollServer();
})();

2. The C# Controller (Local Server)

On the C# side, a lightweight HttpListener acts as the Command and Control (C2) server.

using System;
using System.Collections.Concurrent;
using System.IO;
using System.Net;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

public class AutomationServer
{
    private HttpListener _listener;
    private ConcurrentQueue<object> _commandQueue = new();

    public void Start(int port = 8081)
    {
        _listener = new HttpListener();
        _listener.Prefixes.Add($"http://127.0.0.1:{port}/api/");
        _listener.Start();
        Task.Run(ListenLoop);
    }

    private async Task ListenLoop()
    {
        while (_listener.IsListening)
        {
            var context = await _listener.GetContextAsync();
            var request = context.Request;
            var response = context.Response;

            // Handle CORS for local requests
            response.AddHeader("Access-Control-Allow-Origin", "*");

            string path = request.Url.AbsolutePath;

            if (path.StartsWith("/api/command/"))
            {
                if (_commandQueue.TryDequeue(out var cmd))
                {
                    SendJson(response, cmd);
                }
                else
                {
                    SendJson(response, new { status = "idle" });
                }
            }
            else if (path.StartsWith("/api/result/"))
            {
                using var reader = new StreamReader(request.InputStream);
                string body = await reader.ReadToEndAsync();
                Console.WriteLine($"Result from browser: {body}");
                SendJson(response, new { status = "acknowledged" });
            }
        }
    }

    private void SendJson(HttpListenerResponse response, object data)
    {
        response.ContentType = "application/json";
        byte[] buffer = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(data));
        response.ContentLength64 = buffer.Length;
        response.OutputStream.Write(buffer, 0, buffer.Length);
        response.Close();
    }
}

The Illusion of Prevention: Bypassing the Defenses

Website administrators and security engineers often attempt to implement countermeasures to detect and block this style of automation. However, because the attacker controls the execution environment (the browser extension), nearly every defense can be systematically dismantled.

Here is how standard preventions are bypassed by advanced bot developers:

1. Content Security Policy (CSP) vs. Extension Privileges

The Defense: Administrators use strict CSPs (e.g., connect-src 'self') to prevent the browser from opening WebSockets or making HTTP requests to localhost or 127.0.0.1. The Bypass: Userscript managers like Tampermonkey operate in a privileged extension context. By using the GM_xmlhttpRequest API instead of the standard fetch() or XMLHttpRequest, the script completely ignores the page's CSP directives. The browser allows the extension to make cross-origin requests to the local C2 server without triggering any security violations.

2. Prototype Tampering Detection

The Defense: Sites check if native functions (like document.hidden or requestAnimationFrame) have been tampered with by inspecting their string representation:

if (!window.requestAnimationFrame.toString().includes('[native code]')) {
    console.warn("Tampering detected!");
}

The Bypass: Attackers simply hook Function.prototype.toString to lie about the nature of their overridden functions. By intercepting calls to toString, the bot can return "[native code]" for any function it has hijacked, rendering this detection method useless.

3. Framework State Protection

The Defense: Modern apps avoid exposing the root application state to the global window object, keeping sensitive data out of easily traversable DOM node properties. The Bypass: Frameworks like React and Angular attach internal metadata to DOM elements for event delegation and rendering. An attacker can simply query the DOM for an element and extract its internal keys (e.g., __reactFiber$xyz or __ngContext__). From there, they can traverse the component tree in memory, reading state, props, and unrendered data directly from the framework's virtual DOM.

4. Behavioral Biometrics & Event Trust

The Defense: Because JavaScript dispatches synthetic events (e.g., element.click()), these events lack the rich metadata of genuine human interaction. Security scripts check the isTrusted property of an event, which is a read-only boolean set to true only if the event was generated by a real user action. The Bypass: Attackers bypass the DOM entirely and invoke the framework's internal event handlers directly. By wrapping a synthetic event in a JavaScript Proxy, they can spoof the isTrusted property.

For example, to bypass event trust on a React-based site (a technique observed in advanced harnesses targeting complex web apps), the script extracts the onClick handler from the React Fiber node and feeds it a spoofed event:

// 1. Find the React internal props on the DOM element
const reactKey = Object.keys(element).find(k => k.startsWith('__reactProps$'));
const props = element[reactKey];

// 2. Create a synthetic event
const nativeEvent = new MouseEvent('click', { bubbles: true, cancelable: true });

// 3. Wrap it in a Proxy to spoof the read-only 'isTrusted' property
const fakeNativeEvent = new Proxy(nativeEvent, {
    get(target, prop) {
        if (prop === 'isTrusted') return true; // The bypass!
        const value = Reflect.get(target, prop);
        return typeof value === 'function' ? value.bind(target) : value;
    }
});

// 4. Fire the React handler directly, bypassing the DOM and trust checks
props.onClick({
    bubbles: true,
    cancelable: true,
    target: element,
    nativeEvent: fakeNativeEvent
});

To the React application, this looks exactly like a legitimate, trusted user click.

Conclusion

The combination of JavaScript injection and local IPC represents a paradigm shift in browser automation. By hijacking a legitimate, user-authenticated browser session, developers can completely bypass the fingerprinting checks that plague Selenium and Playwright.

What makes this method so exceptionally powerful—and dangerous—is that there is very little a website can do to stop it. Because the attacker has ultimate control over the execution environment via browser extensions, they can spoof visibility, bypass CSPs, lie to prototype checks, and even forge the isTrusted property of native events.

Executing these techniques requires extensive knowledge of browser security, JavaScript internals, and modern web framework architectures. However, as these methods become more widely understood, the barrier to entry will lower. At the end of the day, nothing running on the client side is truly secure. This represents a fundamentally different vector for browser automation and website botting, and the cybersecurity community must raise awareness—because right now, most websites are completely defenseless against it.


Terminal Logs

0 Transmissions
Fetching logs...

Initialize Transmission

0/1000
⚠️ System Notification