DIY: Making my own antidetect browser

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 :face_exhaling:

  • 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' });
})();
26 Likes

Wow! That’s a lot of research. Good job and thanks for sharing!

3 Likes

Thank you! It was tedious but thankfully paid off.

2 Likes

Impressive stuff, man. I didnt even think this was possible.

3 Likes

Glad you like it! There were multiple instances throughout where I also thought this wasn’t possible :eyes: :joy:

2 Likes

Great job. I am however curious about something. How did switching to Puppeteer’s stealth mode change your approach to handling fingerprinting?

3 Likes

Thanks! Switching to stealth helped with handling the usual fingerprinting stuff (WebRTC, WebGL, canvas) that was going haywire, meaning I didn’t need to custom-build/tweak every fix.

tl;dr it ended up saving me loads of time.

4 Likes

Love what you’re doing here :clap: :clap:! Building your own antidetect browser from scratch is no small feat—it’s got huge potential.

4 Likes

Thank you so much! Im glad you like it. And i couldnt agree more, skies the limit, cant wait to see how far i can take it. :grin:

2 Likes

Wow! You know this is something I’ve always been thinking about doing myself, I just didn’t think it was possible. Well done @RestingGlitchFace :partying_face:

3 Likes

This is marvelous! Well done for taking on such a gargantuan task. Are there any surprising wins or moments of progress that kept you motivated throughout this project?

3 Likes

Very cool stuff! Ill have to try this myself someday.

2 Likes

Wow! You know this is something I’ve always been thinking about doing myself, I just didn’t think it was possible. Well done @RestingGlitchFace :partying_face:

Thank you for the kind words! And I highly recommend you give it a go, even if you hit some speed bumps along the way, you’ll most likely learn a lot more than you initially expected.

This is marvelous! Well done for taking on such a gargantuan task. Are there any surprising wins or moments of progress that kept you motivated throughout this project?

I wouldn’t say it was gargantuan but it definitely was a tricky project to figure out.

Honestly, the one “eureka” moment was getting the spoofed WebGL parameters to work, that took the longest to get down and almost made me to give up entirely. :melting_face:

Very cool stuff! Ill have to try this myself someday.

Glad you find it interesting! I’d happily help if you need any assistance :smile:

2 Likes

Hey, I tried running the code and it works fine but I did unfortunately notice that some webRTC elements are still leaking my local IP. Did you do anything else besides deleting window.RTCPeerConnection? I feel like I missed a step.

1 Like

Hey @EmmsKnows, if your WebRTC is still leaking, you might need to also block RTCPeerConnection-related methods like RTCPeerConnection.createDataChannel() or RTCPeerConnection.getStats() which can be used to retrieve network information. Another thing you can do is completely remove or override navigator.mediaDevices to prevent access to media devices.

These extra measures might help push you in the right direction. Make sure you’re also adding a hard block for any remaining WebRTC-related functions by using --disable-webrtc flags during browser launch, that really did the trick for me.

Here’s some links that could also prove to be useful:

I hope this helps!

1 Like

The Puppeteer stealth plugin seems effective in some tests but still trips up detection on a few stricter platforms. Did you find any specific stealth configurations or additional plugin settings that helped minimize detection even further?

The stealth plugin worked well, but to slightly increase the spoofed nature of what I was doing, I tinkered with window.screen properties, it did the job for the most part, but my level of testing has not been the most extensive (I was honestly just glad that it got to the point it’s at) .

You could even up the spoofing by randomizing navigator.language and navigator.languages to make things less predictable.

e.g.

await page.evaluateOnNewDocument(() => { Object.defineProperty(navigator, 'language', { get: () => 'en-US' }); Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] }); });

Do keep in mind, although randomizing multiple elements could help solidify the security of the whole thing, applying too much randomization might break things. It will definitely be plenty of trial and error.

1 Like

Hey, solid setup you’ve got here! For the proxy rotation part, here’s something that might help: you can create a simple list of proxies and randomly pick one each time you launch Puppeteer.

You could do something like this:

const proxies = {
  'session_1':
  'session_2':
  'session_3':
};
const server = new ProxyChain.Server({
  port: 8080,
  prepareRequestFunction: ({ request }) => {
      if (Object.keys(proxies).length === 0) {
          console.error('No proxies available for routing.');
          return { upstreamProxyUrl: null }; // Graceful handling
      }
      const sessionId = request.headers['session-id'];
      let proxy;
      if (sessionId && proxies[sessionId]) {
          proxy = proxies[sessionId];
          console.log([${new Date().toISOString()}] Routing request with session-id: ${sessionId} to proxy: ${proxy});
      } else {
          // Fallback to a random proxy if session-id is undefined or invalid
          proxy = Object.values(proxies)[Math.floor(Math.random() * Object.values(proxies).length)];
          console.log([${new Date().toISOString()}] Routing request without valid session-id to a random proxy: ${proxy});
      }
      return { upstreamProxyUrl: proxy };
  }
});
server.listen(() => console.log([${new Date().toISOString()}] Rotating proxy server started on port 8080 with ${Object.keys(proxies).length} available proxies.));

Curious to hear if you’ve tried anything similar or if this fits what you were thinking!

1 Like

Thank you so much! I was going to start doing this myself using Python and Selenium, glad to know its better to just do Puppeteer instead.

1 Like

Hey, solid setup you’ve got here! For the proxy rotation part, here’s something that might help: you can create a simple list of proxies and randomly pick one each time you launch Puppeteer.

Thank you for this, I actually hadn’t tested out incorporating rotating proxies yet, so I’ll definitely be giving this a go :eyes: I’ll let you know how it goes (or if I end up needing an extra pair of eyes :grimacing:)

Thank you so much! I was going to start doing this myself using Python and Selenium, glad to know its better to just do Puppeteer instead.

I would honestly suggest starting this how you intended, as I mentioned, the blunders I faced with Python/Selenium could just have been due to my own mistakes. Also kind of curious to see if you manage to get it working utilizing Selenium.