Open Bullet 2 is an open-source tool built for credential stuffing attacks, automated attempts to gain access to user accounts using stolen credentials from data breaches. It supports both website and mobile application targets and has become a staple in the fraud ecosystem due to its flexibility, extensibility, and active community.
As of July 2025, Open Bullet 2 has accumulated over 2,000 GitHub stars and 500 forks, underscoring its widespread adoption among fraudsters.
One major reason for its popularity is that Open Bullet 2 provides a graphical user interface (GUI) to create and run bots, no coding required. Even novice attackers can assemble credential stuffing workflows visually. What lowers the barrier even further is the massive ecosystem of pre-built configurations (“configs”) shared on Telegram and other fraud forums, targeting specific websites and mobile apps. In practice, this means you don’t need to know how to build a bot or reverse a login flow to launch attacks at scale.
Another strength of Open Bullet 2 is its support for multiple bot execution modes (cf screenshot below):
- HTTP client bots, which don’t execute JavaScript.
- Automated browser bots, which can:

This modularity allows attackers to adapt to different levels of bot protection. For example, traditional defenses like CAPTCHAs are no longer sufficient, Open Bullet 2 integrates directly with CAPTCHA solvers and farms to bypass them at scale.
In this article, we focus on one specific mode: Open Bullet 2’s Puppeteer integration. We’ll walk through how it works under the hood and show how to detect it using fingerprinting inconsistencies caused by its stealth plugins and automation artifacts.
If you are just interested in the detection techniques, you can jump to the end of the next section, where we provide JavaScript code snippets for the detection.
Analyzing the fingerprint of Open Bullet 2 Puppeteer bots
To better understand how Open Bullet 2 (OB2) behaves in Puppeteer mode, and how we can detect it, we created a minimal OB2 bot and analyzed its browser fingerprint.
We built the bot using OB2’s visual interface (Stacker
), and pointed it to https://deviceandbrowserinfo.com/are_you_a_bot, a page that collects detailed fingerprinting data into the window.fingerprint
object. This helps us surface any potential fingerprint inconsistencies engendered by OB2.
The bot flow is simple:
- Open a browser using the
Open Browser
(Puppeteer) block - Navigate to the target page
- Wait 1 second
- Read the value of
window.fingerprint

Below is the full bot log and the fingerprint it collected. For clarity, we’ve stripped out some of the attributes that are not relevant to this analysis:
>> Open Browser (PuppeteerOpenBrowser) <<
Headless Browser opened successfully!
>> Navigate To (PuppeteerNavigateTo) <<
Navigated to https://deviceandbrowserinfo.com/are_you_a_bot
>> Delay (Delay) <<
Waited 1000 ms
>> Execute JS (PuppeteerExecuteJs) <<
Evaluated window.fingerprint
Got result: {
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
"platform": "Mac OS X",
"userAgentData": {},
"timezone": "Europe/Paris",
"localeLanguage": "en-US",
"languages": [
"en-US",
"en"
],
"hardwareConcurrency": 4,
"deviceMemory": 8,
"workerData": {
"webGLVendor": "Google Inc. (Apple)",
"webGLRenderer": "ANGLE (Apple, ANGLE Metal Renderer: Apple M4 Pro, Unspecified Version)",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
"languages": [
"en-US",
"en"
],
"platform": "MacIntel",
"hardwareConcurrency": 14,
"cdpCheck1": false,
"isSameAsMainJsContext": false
},
"plugins": "PDF Viewer::Portable Document Format::internal-pdf-viewer:: - Chrome PDF Viewer::Portable Document Format::internal-pdf-viewer:: - Chromium PDF Viewer::Portable Document Format::internal-pdf-viewer:: - Microsoft Edge PDF Viewer::Portable Document Format::internal-pdf-viewer:: - WebKit built-in PDF::Portable Document Format::internal-pdf-viewer::",
"mimeTypes": [
"Portable Document Format~~application/pdf~~pdf",
"Portable Document Format~~text/pdf~~pdf"
],
"language": "en-US",
"screenWidth": 800,
"screenHeight": 600,
"colorDepth": 24,
"availWidth": 800,
"availHeight": 600,
"innerWidth": 756,
"innerHeight": 417,
"maxTouchPoints": 0,
"webGLb64Value": "data:image/png;base64,iVBORw0KGg...",
"webGLVendor": "Intel Inc.",
"webGLRenderer": "Intel Iris OpenGL Engine",
"canvas": "data:image/png;base64,iVBORw0KGg...",
"webdriver": false,
"playwright": false,
"iframeOverriddenTest1": false,
"webdriverInIframe": false,
"iframeOverriddenTest2": false,
"cdpCheck1": false,
"chromeObject": true,
"webGPUAdapterInfo": {
"vendor": "apple",
"architecture": "metal-3",
"device": "",
"description": ""
},
"GPUDeviceLimits": {
"maxTextureDimension1D": 16384,
"maxTextureDimension2D": 16384,
...
"maxComputeWorkgroupSizeZ": 64,
"maxComputeWorkgroupsPerDimension": 65535
},
"speakers": 1,
"micros": 1,
"webcams": 1,
"seleniumChromeDefault": false,
}
Assigned value to variable 'puppeteerExecuteJsOutput'
>> Close Browser (PuppeteerCloseBrowser) <<
Browser closed successfully!
BOT ENDED AFTER 3717 ms WITH STATUS: NONE
Even though Open Bullet 2 makes some effort to avoid common detection signals, like setting navigator.webdriver = false
and preventing Chrome DevTools Protocol leaks, we still observe several inconsistencies in the fingerprint. We’ll go into more detail later about how these evasions are implemented, but even at this stage, subtle mismatches start to emerge.
1. WebGL vs WebGPU mismatch
In the main JavaScript context, the WebGL vendor and renderer are spoofed:
"webGLVendor": "Intel Inc.",
"webGLRenderer": "Intel Iris OpenGL Engine"
However, the WebGPU adapter info correctly reports values tied to Apple’s Metal architecture:
"webGPUAdapterInfo": {
"vendor": "apple",
"architecture": "metal-3",
"device": "",
"description": ""
}
This mismatch between WebGL and WebGPU indicates that Open Bullet is overriding some, but not all, graphics APIs. On real devices, these values would be consistent.
2. Main thread vs worker discrepancies
The fingerprinting script collects data in both the main JavaScript execution context and a Web Worker. These contexts are usually aligned in a legitimate browser, but here we see a key inconsistency:
"webGLRenderer" (main): "Intel Iris OpenGL Engine"
"webGLRenderer" (worker): "ANGLE (Apple, ANGLE Metal Renderer: Apple M4 Pro, Unspecified Version)"
This is a strong signal of tampering. The WebGL spoofing used by OB2 is applied only in the main thread, not inside workers, likely because plugins like PuppeteerExtraSharp don't inject scripts into Web Workers.
3. Headless screen resolution
Finally, the screen dimensions are suspiciously set to the default headless Chrome size (800x600 px
):
"screenWidth": 800,
"screenHeight": 600,
"availWidth": 800,
"availHeight": 600,
While not definitive on their own, these values are rarely seen in genuine user sessions and are a common artifact of headless automation.
Detection logic
You can use the following fingerprinting techniques to flag bots running in Open Bullet 2’s Puppeteer mode:
// Screen resolution
if (screen.width === 800 && screen.height === 600) {
console.log('Bot detected!');
}
if (screen.availWidth === 800 && screen.availHeight === 600) {
console.log('Bot detected!');
}
// JS worker vs main context mismatch
const canvas = document.createElement('canvas');
var ctx = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
const webGLRendererMainJSContext = ctx.getParameter(ctx.getExtension('WEBGL_debug_renderer_info').UNMASKED_RENDERER_WEBGL);
const workerCode = `try {
var fingerprintWorker = {};
var canvas = new OffscreenCanvas(1, 1);
var gl = canvas.getContext('webgl');
var glExt = gl.getExtension('WEBGL_debug_renderer_info');
fingerprintWorker.webGLRenderer = gl.getParameter(glExt.UNMASKED_RENDERER_WEBGL);
self.postMessage(fingerprintWorker);
} catch (e) {
self.postMessage(fingerprintWorker);
}`
var blob = new Blob([workerCode], { type: 'application/javascript' });
var workerUrl = URL.createObjectURL(blob);
var worker = new Worker(workerUrl);
worker.onmessage = function (e) {
if (e.data.webGLRenderer !== webGLRendererMainJSContext) {
console.log('Bot detected!');
}
}
We don’t include the Intel vs Apple check for WebGL/WebGPU mismatch here, as that test is device-specific and less broadly applicable. The two examples above are more generic and should work across a wider range of environments.
Why Open Bullet 2’s Puppeteer bots leak fingerprinting inconsistencies
The fingerprint mismatches we observed earlier aren’t random. They’re a direct result of how Open Bullet 2 initializes its Puppeteer bots under the hood.
When a Puppeteer browser is created in Open Bullet 2, the underlying code path is defined in RuriLib/Blocks/Puppeteer/Browser/Methods.cs
. A key detail is that Open Bullet imports the PuppeteerExtraSharp
library:
using PuppeteerExtraSharp;
using PuppeteerExtraSharp.Plugins.ExtraStealth;
using PuppeteerSharp;
PuppeteerExtraSharp is a C# port of the original Puppeteer-extra project from the Node.js ecosystem. Like the original, it supports a plugin system, where each plugin modifies the browser to make it harder to detect, or easier to bypass CAPTCHAs and fingerprinting defenses.

Stealth plugin: removing navigator.webdriver
One of the most common detection techniques relies on the presence of navigator.webdriver = true
, which signals a headless or automated browser. Open Bullet 2 disables this by including a stealth plugin called WebDriver.cs
.
Located in PuppeteerExtraSharp/Plugins/ExtraStealth/Evasions/WebDriver.cs
, this plugin adds the --disable-blink-features=AutomationControlled
argument to Chrome’s launch configuration:
public override void BeforeLaunch(LaunchOptions options)
{
var args = options.Args.ToList();
var idx = args.FindIndex(e => e.StartsWith("--disable-blink-features="));
if (idx != -1)
{
var arg = args[idx];
args[idx] = $"{arg}, AutomationControlled";
return;
}
args.Add("--disable-blink-features=AutomationControlled");
options.Args = args.ToArray();
}
As discussed in our Playwright detection article, this flag prevents Chrome from setting navigator.webdriver = true
.
Stealth plugin: spoofing WebGL
The inconsistency between WebGL and WebGPU values comes from another plugin: WebGl.cs
.
If the user doesn’t explicitly override the WebGL fingerprint, the plugin defaults to:
"Intel Inc."
as the vendor"Intel Iris OpenGL Engine"
as the renderer
public WebGl(StealthWebGLOptions options) : base("stealth-webGl")
{
_options = options ?? new StealthWebGLOptions("Intel Inc.", "Intel Iris OpenGL Engine");
}
When a new page is created, it injects a spoofing script into the browser using page.EvaluateFunctionOnNewDocumentAsync
:
public override async Task OnPageCreated(IPage page)
{
var script = Utils.GetScript("WebGL.js");
await page.EvaluateFunctionOnNewDocumentAsync(script, _options.Vendor, _options.Renderer);
}
The content of the injected WebGL.js
script is shown below. It overrides the getParameter()
function in the WebGL rendering context using a JavaScript proxy:
(vendor, renderer) => {
const getParameterProxyHandler = {
apply: function(target, ctx, args) {
const param = (args || [])[0];
// UNMASKED_VENDOR_WEBGL
if (param === 37445) {
return vendor || 'Intel Inc.'; // default in headless: Google Inc.
}
// UNMASKED_RENDERER_WEBGL
if (param === 37446) {
return renderer || 'Intel Iris OpenGL Engine'; // default in headless: Google SwiftShader
}
return utils.cache.Reflect.apply(target, ctx, args);
}
};
const addProxy = (obj, propName) => {
utils.replaceWithProxy(obj, propName, getParameterProxyHandler);
};
addProxy(WebGLRenderingContext.prototype, 'getParameter');
addProxy(WebGL2RenderingContext.prototype, 'getParameter');
}
This is how Open Bullet ensures the spoofed WebGL values appear in the main JS context. But that spoofing has two key limitations:
- It doesn’t affect Web Workers, which run in a separate execution context.
- It doesn’t touch WebGPU, a newer API used for graphics and compute workloads.
Why inconsistencies arise
These implementation gaps are exactly why we saw discrepancies like:
- Different WebGL renderers in main JS vs worker
- Intel WebGL but Apple Metal in WebGPU
That’s not something you'd see in a real device. These mismatches are a direct result of PuppeteerExtraSharp only patching part of the browser environment. And since the spoofing is JS-based, it fails to propagate across context boundaries.
Even the use of JavaScript proxies introduces detectable side effects. While some can be tested directly (e.g. via toString
checks), others can be inferred by triggering controlled exceptions or validating consistency across calls.
In short, while Open Bullet 2 tries to avoid obvious detection flags like navigator.webdriver
or Chrome DevTools side effects, it still leaks automation through deeper fingerprint inconsistencies, especially across JavaScript contexts (main vs. worker) and between fingerprinting surfaces (like WebGL vs. WebGPU). These mismatches are difficult for attackers to fully mask and remain strong, reliable signals in production environments.
We explore this further in this article, which shows how newer anti-detect frameworks, built on top of Chrome’s updated headless mode, have begun to address these issues. Unlike the older puppeteer-stealth
approach, these newer frameworks (like nodriver
) aim to produce more internally consistent, human-like fingerprints that are harder to catch with simple heuristics.
Key takeaways for detecting Open Bullet 2 bots
Open Bullet 2’s Puppeteer mode is designed to look like a real browser but it leaves behind detectable artifacts. While it disables obvious signals like navigator.webdriver
and Chrome DevTools Protocol side effects, deeper inconsistencies still emerge, especially when fingerprinting across execution contexts.
In this article, we showed how Open Bullet 2 uses PuppeteerExtraSharp to inject stealth plugins that spoof WebGL properties and other fingerprinting surfaces. But since those evasions only operate in the main JavaScript context, they fail to replicate consistently in Web Workers or other APIs like WebGPU.
By comparing fingerprint data across JavaScript execution contexts and looking for mismatches, such as between WebGL and WebGPU, or between the main thread and a worker, you can reliably identify bots even when they’re running real browsers.
These checks are lightweight to deploy and difficult for attackers to neutralize without writing custom patches. And because they don’t rely on navigator.webdriver
or reCAPTCHA triggers, they remain effective even against tools like Open Bullet that integrate modern anti-detection libraries out of the box.
Detection today is less about finding the one “bot flag” and more about spotting the inconsistencies that arise when automation tries to look too human.