How to detect puppeteer with 100% accuracy

greydingo

Junior Member
Joined
Dec 31, 2014
Messages
152
Reaction score
73
I just posted this in another thread, but I thought it may be better as it's own thread.

I will disclose a method to detect puppeteer with 100% accuracy.

I have never told anyone about this. I've never seen it discussed anywhere, I discovered this on my own a while back.

I'm guessing anti-bot companies already know this trick.

I just uploaded a demo repo.

https://github.com/digitalhurricane-io/puppeteer-detection-100-percent

If your replace a function that puppeteer uses, such as document.querySelector, and throw an error inside of it, you can see the string "__puppeteer_evaluation_script__"

In order to get around this, you need to fork the library and change that string.
 
Last edited:
Bookmarked, awesome. I would rather use nightmare js, but I might come back to this.
 
That string is the sourceURL, it would be the best to remove it instead of renaming.

Not sure about that. I don't remember everything I tried. But if it was better to remove it, or could be removed, I'm guessing I would have done that.

What I did do, was rename it to the same name as common scripts run by common browser plugins.

Legit scripts running on the page have a name anyways.
 
if its so easy, why big companies like IG don't block it that way?
is the page.click() function is also using the querySelector?
 
if its so easy, why big companies like IG don't block it that way?
is the page.click() function is also using the querySelector?

I don't have an answer for you. But if you don't believe me, clone the repo and try for yourself. :)

And who's to say big companies aren't looking at that? They may have reasons for letting you get by with things some of the time. I won't claim to have all the answers.

I worked on a bot for a site before that would purposefully let you get away with something sometimes, in order to trick you into thinking the changes you were making to the bot were working.
 
Or you can handle the exception with a try catch blocks

No you can not.

I'm not throwing an error. Just creating one.

I'll edit the original post to say 'creating' instead of 'throwing'

Edit: NVM. I can't edit the original post.
 
You could avoid the detection by running the javascript in it's own context with createIsolatedWorld
But unfortunately it's not in puppeteer yet, see #2671

So you need to send raw devtools-protocol commands to get it working
Code:
const mainFrame = page.mainFrame()
const iso = await page._client.send('Page.createIsolatedWorld', {frameId: mainFrame._id, worldName: 'iso'});
await page._client.send('Runtime.evaluate', {
  expression: 'document.querySelector("#text-field").focus()',
  contextId: iso.executionContextId
});
await page.keyboard.type('hi');
 
You could avoid the detection by running the javascript in it's own context with createIsolatedWorld
But unfortunately it's not in puppeteer yet, see #2671

So you need to send raw devtools-protocol commands to get it working
Code:
const mainFrame = page.mainFrame()
const iso = await page._client.send('Page.createIsolatedWorld', {frameId: mainFrame._id, worldName: 'iso'});
await page._client.send('Runtime.evaluate', {
  expression: 'document.querySelector("#text-field").focus()',
  contextId: iso.executionContextId
});
await page.keyboard.type('hi');

That does the trick. Great work sockpuppet!
 
You could avoid the detection by running the javascript in it's own context with createIsolatedWorld
But unfortunately it's not in puppeteer yet, see #2671

So you need to send raw devtools-protocol commands to get it working
Code:
const mainFrame = page.mainFrame()
const iso = await page._client.send('Page.createIsolatedWorld', {frameId: mainFrame._id, worldName: 'iso'});
await page._client.send('Runtime.evaluate', {
  expression: 'document.querySelector("#text-field").focus()',
  contextId: iso.executionContextId
});
await page.keyboard.type('hi');

This is awesome :)

Do you need to instantiate a new isolated context for each task, or can you just initialise it once?

For example:

Code:
const puppeteer = require('puppeteer-extra');

class Actor {

   browser;
   page;
   iso;
 
   async init() {
      this.browser = await puppeteer.launch();
      this.page = await this.browser.newPage();
      const mainFrame = await this.page.mainFrame();
      this.iso = await this.page._client.send('Page.createIsolatedWorld', { frameId: mainFrame._id, worldName: 'iso' });
   }

   async evaluate(expression) {
      await this.page._client.send('Runtime.evaluate', {
         expression: expression,
         contextId: this.iso.executionContextId
      });
   }

   async foo() {
      // ...
      await this.page.goto('https://foo.com', { waitUntil: 'networkidle2' });
      let expression = 'document.querySelector("#text-field").focus()';
      await this.evaluate(expression);
      await this.page.goto('https://bar.com', { waitUntil: 'networkidle2' });
      await this.evaluate(expression); // Will this fail?
   }
}

Or would this fail each time the page changes via navigation?
 
Or would it need to look like

Code:
async evaluate(expression) {
   const mainFrame = await this.page.mainFrame();
   const worldName = 'iso' + process.hrtime.bigint();
   const iso = await this.page._client.send('Page.createIsolatedWorld', { frameId: mainFrame._id, worldName: worldName });

   await this.page._client.send('Runtime.evaluate', {
      expression: expression,
      contextId: iso.executionContextId
   });
}
 
The context gets destroyed in the navigation.
I would do something like this:
Code:
async createContext() {
   const mainFrame = await this.page.mainFrame();
   const worldName = 'iso' + process.hrtime.bigint();
   this.iso = await this.page._client.send('Page.createIsolatedWorld', { frameId: mainFrame._id, worldName: worldName });
}

async evaluate(expression) {
   await this.page._client.send('Runtime.evaluate', {
     expression: expression,
     contextId: this.iso.executionContextId
   });
}
 
The context gets destroyed in the navigation.

Hmmm ok, so would a listener on each navigation change be reliable?

Code:
this.page.on('framenavigated', async frame => {
   if (frame !== this.page.mainFrame()) {
      return;
   }
   const worldName = 'iso' + process.hrtime.bigint();
   this.iso = await this.page._client.send('Page.createIsolatedWorld', { frameId: frame._id, worldName: worldName });
});

I've heard that there's a "lag" on some listeners, but that seems like it would run each time the mainFrame's navigation changes right?

Thanks for replying!
 
Hmmm ok, so would a listener on each navigation change be reliable?
No idea if that works, you have to try it. But that looks overcomplicated, what is your use case?
I would just call createContext on every navigation step.
 
No idea if that works, you have to try it. But that looks overcomplicated, what is your use case?
I would just call createContext on every navigation step.

It seems to work but it's hard to know if it will be stable, I think I will just call it manually each time as you suggest to be sure. Use case was more just code brevity (ie not having to add a line to reinitialise each time it navigates haha)...

Loving your posts on this btw, do you currently use Puppeteer or something else?

I'm always trying to harden my Twitter actor as much as possible, so there's been a ton of improvements I've been able to make from you and greydingo's info so far...
 
Back
Top