[Python] How can I spoof WebGL and fingerprinting attributes using Playwright with...

Discussão em 'Python' iniciado por Stack, Maio 26, 2025.

  1. Stack

    Stack Membro Participativo

    I'm working on stealth browser fingerprint spoofing using Playwright and BrowserForge, targeting anti-bot detection tools like BrowserScan.

    I want to spoof multiple fingerprinting surfaces, including:


    • WebGL Vendor & Renderer


    • Canvas fingerprint


    • Audio fingerprint


    • navigator.hardwareConcurrency, deviceMemory, etc.


    • screen.colorDepth


    • Element.getClientRects noise

    Despite injecting the spoofing script via add_init_script, BrowserScan shows that WebGL is unsupported, and some spoofed values do not reflect correctly.

    import asyncio
    from patchright.async_api import async_playwright
    from browserforge.fingerprints import FingerprintGenerator
    import requests
    from browserforge.fingerprints import Screen
    import random
    import uuid
    import os

    screen = Screen(
    min_width=100,
    max_width=1280,
    min_height=400,
    max_height=720,
    )

    URLS = ["https://browserscan.net/"]
    USER_DATA_DIR = f"./user_profile/{uuid.uuid4()}"
    fp_gen = FingerprintGenerator()
    fingerprint = fp_gen.generate(os="windows")

    def make_webgl_spoof_script(fp):
    return f"""
    (function () {{
    const spoofed = {{
    webglVendor: "{fp['webgl_vendor']}",
    webglRenderer: "{fp['webgl_renderer']}",
    hardwareConcurrency: {fp['hardware_concurrency']},
    deviceMemory: {fp['device_memory']},
    colorDepth: {fp['color_depth']},
    maxTouchPoints: {fp['touch_support']},
    clientRectVariation: {fp['client_rect_variation']}
    }};

    const getParameterProxyHandler = {{
    apply: function(target, thisArg, args) {{
    const param = args[0];
    if (param === 37445) return spoofed.webglVendor;
    if (param === 37446) return spoofed.webglRenderer;
    return Reflect.apply(target, thisArg, args);
    }}
    }};

    const hookWebGL = () => {{
    const original = HTMLCanvasElement.prototype.getContext;
    HTMLCanvasElement.prototype.getContext = new Proxy(original, {{
    apply(target, thisArg, args) {{
    const ctx = Reflect.apply(target, thisArg, args);
    if (ctx && (args[0] === 'webgl' || args[0] === 'webgl2')) {{
    ctx.getParameter = new Proxy(ctx.getParameter, getParameterProxyHandler);
    }}
    return ctx;
    }}
    }});
    }};
    hookWebGL();

    Object.defineProperty(navigator, 'hardwareConcurrency', {{
    get: () => spoofed.hardwareConcurrency
    }});
    Object.defineProperty(navigator, 'deviceMemory', {{
    get: () => spoofed.deviceMemory
    }});
    Object.defineProperty(screen, 'colorDepth', {{
    get: () => spoofed.colorDepth
    }});
    Object.defineProperty(navigator, 'maxTouchPoints', {{
    get: () => spoofed.maxTouchPoints
    }});

    const originalGetClientRects = Element.prototype.getClientRects;
    Element.prototype.getClientRects = function() {{
    const rectList = originalGetClientRects.apply(this);
    const variation = spoofed.clientRectVariation;

    const fakeRects = Array.from(rectList).map(rect => {{
    const copy = new DOMRect(rect.x, rect.y, rect.width, rect.height);
    copy.width += variation;
    copy.height += variation;
    return copy;
    }});
    return fakeRects;
    }};

    function addCanvasNoise(ctx) {{
    const originalGetImageData = ctx.getImageData;
    ctx.getImageData = function(x, y, w, h) {{
    const imageData = originalGetImageData.call(this, x, y, w, h);
    for (let i = 0; i < imageData.data.length; i += 4) {{
    imageData.data += Math.random() * 5;
    imageData.data[i + 1] += Math.random() * 5;
    imageData.data[i + 2] += Math.random() * 5;
    }}
    return imageData;
    }};
    }}

    function patchCanvasProto(proto) {{
    const originalToDataURL = proto.toDataURL;
    proto.toDataURL = function(...args) {{
    addCanvasNoise(this.getContext('2d'));
    return originalToDataURL.apply(this, args);
    }};
    const originalToBlob = proto.toBlob;
    proto.toBlob = function(...args) {{
    addCanvasNoise(this.getContext('2d'));
    return originalToBlob.apply(this, args);
    }};
    }}
    patchCanvasProto(HTMLCanvasElement.prototype);

    const getChannelDataHandler = {{
    apply: function(target, thisArg, args) {{
    const data = Reflect.apply(target, thisArg, args);
    const shift = 1e-7;
    for (let i = 0; i < data.length; i += 100) {{
    data += shift;
    }}
    return data;
    }}
    }};

    const patchAudioContext = (ctxName) => {{
    const proto = window[ctxName]?.prototype;
    if (!proto || !proto.createBuffer) return;
    const original = proto.createBuffer;
    proto.createBuffer = function(...args) {{
    const buffer = original.apply(this, args);
    if (buffer?.getChannelData) {{
    buffer.getChannelData = new Proxy(buffer.getChannelData, getChannelDataHandler);
    }}
    return buffer;
    }};
    }};
    patchAudioContext("AudioContext");
    patchAudioContext("OfflineAudioContext");
    }})();
    """

    def generate_realistic_fingerprint():
    navigator = fingerprint.navigator
    card = fingerprint.videoCard
    vendor, renderer = f"{card.vendor}", f"{card.renderer}"

    return {{
    "webgl_vendor": vendor,
    "webgl_renderer": renderer,
    "hardware_concurrency": navigator.hardwareConcurrency,
    "device_memory": navigator.deviceMemory,
    "color_depth": random.choice([24, 30, 32]),
    "touch_support": 0,
    "audio_fingerprint_seed": random.randint(10000, 99999),
    "client_rect_variation": random.uniform(0.001, 0.01)
    }}

    class BrowserSession:
    async def run(self):
    async with async_playwright() as p:
    context = await p.chromium.launch_persistent_context(
    user_data_dir=USER_DATA_DIR,
    channel="chrome",
    headless=False,
    bypass_csp=True,
    color_scheme="dark",
    locale="en-US",
    timezone_id="America/New_York",
    viewport={{"width": 1280, "height": 720}},
    args=[
    "--disable-webrtc",
    "--disable-webrtc-encryption",
    "--disable-rtc-smoothness-algorithm",
    "--disable-rtc-udp",
    "--force-webrtc-ip-handling-policy=disable_non_proxied_udp",
    "--webrtc-ip-handling-policy=disable_non_proxied_udp",
    "--disable-features=WebRtcHideLocalIpsWithMdns,WebRtcAllowLegacyTLSProtocols",
    ],
    )

    fingerprint = generate_realistic_fingerprint()
    spoof_script = make_webgl_spoof_script(fingerprint)
    await context.add_init_script(spoof_script)

    page = context.pages[0] if context.pages else await context.new_page()
    for url in URLS:
    page = await context.new_page()
    await page.goto(url)

    await asyncio.to_thread(input, "Press Enter to close browser...")
    await context.close()

    if __name__ == "__main__":
    asyncio.run(BrowserSession().run())


    I've tried:


    • Injected the spoofing script before any pages are created.


    • Ensured no Chrome flags are disabling WebGL.


    • Verified spoofed values in the dev console.


    • Used real GPU info from BrowserForge's videoCard.

    What I'm looking for:


    • Why might WebGL show as unsupported even when spoofed?


    • Are there Playwright or Chromium flags that can block or disable WebGL?


    • Is there a more reliable way to patch WebGL context methods across all usage patterns?

    Are there suggestions to improve the spoofing approach or script?

    Continue reading...

Compartilhe esta Página