Hey everyone,
When previously browsing through the community I came across a particular response which has been replaying in the back of my head. This response of course being “They’re all the same. Commercial tools that are easy to detect. The best antidetect I’ve seen were all custom made.” I personally had never thought of it, can you really just make an antidetect browser that was better?
I went down the rabbit hole, did my fair share of reading, and decided I wanted to take a stab at this. If this actually works then I’d be cutting some costs and keeping things in my own hands.
So this is my journey about how I created my own antidetect browser (mostly). It’s been a rollercoaster of trial and error, and although it isn’t 100% there yet, it’s definitely a much more solid start than I expected it to be.
If you’re interested in building your own tool for handling browser fingerprinting, here’s the rundown of what worked, what didn’t, and how this setup stacks up against commercial solutions.
1. Starting Out with Python and Selenium – Rough Beginnings
I started with Python and Selenium. Honestly, I thought it’d be pretty straightforward. Spoiler alert: it wasn’t. Selenium’s powerful for automation, but when it came to spoofing all the tricky stuff — WebRTC, WebGL, canvas — it just didn’t cut it (at least not for me, I could have just missed the mark really).
- The main issue: I couldn’t get past basic detection tests. Selenium kept outing itself, and getting it to spoof key details was a nightmare.
- The solution? After banging my head against the wall, I switched over to Node.js with Puppeteer, which turned out to be much better for controlling fingerprinting.
2. Moving to Puppeteer + Stealth Mode
When it was clear that Selenium wasn’t cutting it, I made the shift to Puppeteer with the puppeteer-extra-plugin-stealth, which was a significant turning point. This switch added essential cloaking capabilities.
Puppeteer’s stealth plugin automatically overrides key properties that give away automation, like navigator.webdriver
. This adjustment immediately reduced red flags on detection tools like BrowserLeaks, which had been throwing up warnings with Selenium. For the first time, the setup looked like it could pass undetected.
3. Tackling Proxy Authentication Pop-Ups
The next hurdle was proxy setup. Masking my IP address was important, but I was immediately hit with login prompts for authenticated proxies. This broke the session flow, as each new instance asked for credentials. Talk about frustrating
- The Fix: After trial and error, I added
page.authenticate()
in Puppeteer, which handled the proxy login invisibly in the background. - Outcome: Finally got rid of those pesky pop-ups and could mask my IP properly.
With the pop-up problem solved, sessions could finally run without interruption.
4. Blocking WebRTC and Preventing IP Leaks
WebRTC was another big concern. WebRTC is infamous for leaking IP addresses even when proxies are in place, so it needed to be fully disabled. To stop WebRTC leaks, I used both command-line flags and API overrides. The command-line flags --disable-webrtc
and --webrtc-ip-handling-policy=disable_non_proxied_udp
took care of the initial blocking, while I also injected code to override WebRTC-related APIs like RTCPeerConnection
and RTCSessionDescription
. After these changes, BrowserLeaks stopped reporting my real IP address in the WebRTC section, showing that the leak was effectively patched.
5. WebGL Spoofing to Hide GPU Details
WebGL is one of the trickiest parts since it reveals GPU info, which is a dead giveaway if you’re trying to spoof hardware. The goal was to spoof these details without totally disabling WebGL, so sites wouldn’t detect anything missing.
- How I Did It: Overrode
WebGLRenderingContext.getParameter
to spoof both renderer and vendor details. I set fake values like “Intel Inc.” for the vendor and “Intel Iris OpenGL Engine” for the renderer, and it actually worked! - Outcome: BrowserLeaks showed spoofed hardware details instead of my actual GPU info.
6. Canvas and Screen Properties Randomization
To dodge canvas fingerprinting, I had to add slight randomizations to how canvas is rendered, so it wasn’t producing the same unique “signature” every time.
- Canvas Spoofing: Small changes to the canvas output were added to introduce randomization.
- Screen Spoofing: Played around with screen properties, randomising things like resolution and colour depth.
- Result: BrowserLeaks showed a bit of variance in these properties, meaning less of a fingerprint trail.
What’s Next?
This whole project’s been a great learning experience. While it’s a functional anti-detect browser, it doesn’t yet reach the level of commercial anti-detect browsers with features like full rotation, advanced randomization, and refined UI. There’s still plenty of room for improvement, but this setup lays a solid foundation.
What are my next steps? I’m looking to add rotating proxies, better randomization, and exploring more fingerprint vectors (audio fingerprinting might be next on the hit list). This DIY approach might not match the heavyweights just yet, but it’s getting there, one workaround at a time.
If you’ve been interested in browser fingerprinting, or want to give spoofing them a go yourself, I hope this shows you that even someone with no particular knowledge on the matter managed to stumble their way through it.
Code so far:
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
// Use stealth plugin to avoid detection
puppeteer.use(StealthPlugin());
(async () => {
const browser = await puppeteer.launch({
headless: false,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-web-security',
'--disable-blink-features=AutomationControlled',
// Disable WebRTC
'--disable-webrtc',
'--disable-features=WebRTC',
'--disable-webrtc-hw-decoding',
'--disable-webrtc-hw-encoding',
'--webrtc-ip-handling-policy=disable_non_proxied_udp',
'--force-webrtc-ip-handling-policy',
// Proxy settings
'--REDACTED',
]
});
const page = await browser.newPage();
// Proxy authentication
await page.authenticate({
username: 'REDACTED',
password: 'REDACTED'
});
// WebRTC and WebGL spoofing
await page.evaluateOnNewDocument(() => {
// Block WebRTC by overriding the WebRTC-related APIs
delete window.RTCPeerConnection;
delete window.RTCSessionDescription;
// Spoof WebGL renderer and vendor
const originalGetParameter = WebGLRenderingContext.prototype.getParameter;
WebGLRenderingContext.prototype.getParameter = function(parameter) {
if (parameter === 37445) return 'Intel Inc.'; // Fake Vendor
if (parameter === 37446) return 'Intel Iris OpenGL Engine'; // Fake Renderer
return originalGetParameter.call(this, parameter);
};
// Prevent WebGL extension probing
const originalGetExtension = WebGLRenderingContext.prototype.getExtension;
WebGLRenderingContext.prototype.getExtension = function(name) {
return null;
};
// Canvas spoofing
const originalToDataURL = HTMLCanvasElement.prototype.toDataURL;
HTMLCanvasElement.prototype.toDataURL = function() {
// Return a base64 string, simulating a different fingerprint
return originalToDataURL.call(this, 'image/png');
};
// Randomize screen properties
const getRandomInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
const screenWidth = [1920, 1366, 1600, 1440, 1536][Math.floor(Math.random() * 5)];
const screenHeight = screenWidth === 1920 ? 1080 : getRandomInt(700, 900);
Object.defineProperty(window.screen, 'width', {
get: () => screenWidth
});
Object.defineProperty(window.screen, 'height', {
get: () => screenHeight
});
Object.defineProperty(window.screen, 'availWidth', {
get: () => screenWidth
});
Object.defineProperty(window.screen, 'availHeight', {
get: () => screenHeight
});
Object.defineProperty(window.screen, 'colorDepth', {
get: () => 24
});
Object.defineProperty(window.screen, 'pixelDepth', {
get: () => 24
});
});
// Navigate to browserleaks
await page.goto('https://browserleaks.com', { waitUntil: 'networkidle2' });
})();