Leon Weinmann portrait
ShigShag

Security Audit and Comparison of DSGVO CAPTCHAs in the DACH Region

In my major thesis two years ago, I analyzed non-interactive CAPTCHA solutions regarding security against automated abuse. Among the vendors were Friendly Captcha and CaptchaFox. Both vendors are privacy-focused alternatives to Big Tech solutions like Google reCAPTCHA and Cloudflare Turnstile. As good as this sounds, the audit revealed major security issues in both vendors.

Now, almost two years later, I will do a new security audit of privacy-focused CAPTCHAs, but this time covering more vendors. To the best of my knowledge, this is the first public side-by-side security review of this set of vendors. This blog should serve as an overview. For each CAPTCHA there is a lot more to analyze but that would be beyond the scope of this research.

Overview of the CAPTCHAs

All vendors listed below offer non-interactive CAPTCHA solutions.

Security Criteria

Before evaluating, we need to define at which point a non-interactive CAPTCHA can be considered secure. For that, we need to understand how bots can be detected when no interactive challenge is required. Below is only a brief overview:

Browser fingerprinting

Browser fingerprinting is the process of collecting a browser’s characteristics using client-side JavaScript. Automated browsers often differ from their legitimate counterparts in detectable ways. For example, a simple bot built with Python Selenium can be identified by checking the navigator.webdriver property, which is set to true.

As one might expect, there are many more signals available to detect automation frameworks. A good overview can be found in the bot-detect repository.

It is worth noting that fingerprinting-based detection has become less reliable over time. As of May 2025, a V8 change in Chromium patched a widely-used CDP-based signal, silently breaking detection methods that relied on getter side effects during error object inspection.

Behavior Analysis

Behavioral signals measure how a user interacts with the page rather than what browser they are using. The two most common categories are keyboard and mouse behavior.

Keyboard dynamics track timing properties of keystrokes: dwell time (how long a key is held), flight time (the gap between releasing one key and pressing the next), and key rollover (whether keys overlap, as they naturally do when humans type quickly). Bots tend to produce unnaturally consistent timing, zero key overlap, and missing modifier events — for example, typing uppercase letters without a preceding Shift keypress.

Mouse dynamics capture movement patterns such as speed, acceleration, curvature, and click precision. Human mouse movement is inherently noisy with slight deviations, while automated input tends to follow perfectly straight paths or fixed intervals.

However, there is a limit to effectiveness. In order to observe behavior, it must actually occur. If few or no keypresses are made, there is nothing to analyze. Additionally, some automation frameworks bypass keyboard events entirely. For example, DrissionPage implements value injection by directly setting the DOM element’s value rather than dispatching actual keyboard events. Since no keydown/keyup events are generated, standard keystroke listeners never fire. While this can be countered by hooking the input element’s value setter to detect programmatic writes, doing so reliably is difficult in practice — password managers, browser autofill, and extensions also set field values programmatically, leading to false positives.

JavaScript Obfuscation

This is a must-have. Browser fingerprinting and behavior analysis happen client-side. This means an adversary has full control over the execution of the code. Imagine this simple scenario:

// Check if client is bot
let is_bot = navigator.webdriver;

fetch("https://challenge.captcha.com/api/verify", {
  method: "POST",
  body: JSON.stringify({ bot: is_bot }),
});

Without obfuscation, an attacker can simply read the source, understand the logic, and override the result before it is sent. Below is the same code as above, obfuscated with obfuscator.io:

Show obfuscated code
const a0_0x268927 = a0_0x332e;
function a0_0x332e(_0xb8a3c6, _0x9f7eab) {
  _0xb8a3c6 = _0xb8a3c6 - 0xcf;
  const _0x1f7715 = a0_0x1f77();
  let _0x332ecc = _0x1f7715[_0xb8a3c6];
  return _0x332ecc;
}
(function (_0x38b331, _0x4f1176) {
  const _0x2b1416 = a0_0x332e,
    _0x3b3445 = _0x38b331();
  while (!![]) {
    try {
      const _0x41e1f3 =
        -parseInt(_0x2b1416(0xd3)) / 0x1 +
        parseInt(_0x2b1416(0xdd)) / 0x2 +
        (-parseInt(_0x2b1416(0xd7)) / 0x3) *
          (-parseInt(_0x2b1416(0xd8)) / 0x4) +
        (parseInt(_0x2b1416(0xd1)) / 0x5) * (-parseInt(_0x2b1416(0xda)) / 0x6) +
        parseInt(_0x2b1416(0xd0)) / 0x7 +
        (-parseInt(_0x2b1416(0xdb)) / 0x8) * (parseInt(_0x2b1416(0xd5)) / 0x9) +
        (parseInt(_0x2b1416(0xcf)) / 0xa) * (parseInt(_0x2b1416(0xd9)) / 0xb);
      if (_0x41e1f3 === _0x4f1176) break;
      else _0x3b3445["push"](_0x3b3445["shift"]());
    } catch (_0x5d410d) {
      _0x3b3445["push"](_0x3b3445["shift"]());
    }
  }
})(a0_0x1f77, 0x94653);
let is_bot = navigator[a0_0x268927(0xd4)];
fetch(a0_0x268927(0xd2), {
  method: a0_0x268927(0xd6),
  body: JSON[a0_0x268927(0xdc)]({ bot: is_bot }),
});
function a0_0x1f77() {
  const _0x549edf = [
    "5TobDUP",
    "/api/verify",
    "620695BTshiN",
    "webdriver",
    "45NlgJgm",
    "POST",
    "50235xnaXUt",
    "172FfYGeV",
    "4139267DaRnMO",
    "3003054SMyfcS",
    "1405264UjRknB",
    "stringify",
    "588716irqduX",
    "40dIpPNR",
    "614180kxTBeW",
  ];
  a0_0x1f77 = function () {
    return _0x549edf;
  };
  return a0_0x1f77();
}

However, there are only a few reliable JavaScript obfuscators publicly available. obfuscator.io is one of them. But given its popularity, dedicated deobfuscators targeting it also exist — notably webcrack and synchrony. Below is the deobfuscated version of the above output, produced with webcrack:

let is_bot = navigator.webdriver;
fetch("https://challenge.captcha.com/api/verify", {
  method: "POST",
  body: JSON.stringify({
    bot: is_bot,
  }),
});

Time constraint

A time constraint can be either Proof-of-Work (PoW), Time-to-Pass (TTP), or both. Even if an attacker reverses the JavaScript and spoofs the verification, a PoW or TTP will act as a soft rate limiter. Friendly Captcha, for example, implements smart-scaling proof-of-work. However, relying on proof-of-work alone makes this susceptible to optimized hardware attacks. Combining it with a smart-scaling TTP is the best approach.

Warning

Contrary to what CAPTCHA vendors claim, PoW is not a means to defend against bots. It is solely a rate-limiting measure.

Server-side

Server-side logic must be solid. The main attack vectors here are the reuse of solved challenges or tokens. In my research two years ago, I came across only one server-side issue — however, this single issue allowed a complete bypass of the CAPTCHA. The bug allowed a challenge solution, which on success generates a token used to pass the CAPTCHA, to be reused an infinite number of times, enabling the generation of unlimited tokens from a single solved challenge. The bug has since been fixed, and I am not going to disclose which vendor was affected.

Testing Methodology

All testing was conducted using the free or trial version of each CAPTCHA (except Friendly Captcha). Each CAPTCHA is evaluated in two scenarios:

Automated Testing Against Automation Frameworks

For automated testing, we use the following frameworks:

Each framework is run in headed mode, so a real browser window is visible. In each scenario, the bot will enter an email and a password. Each CAPTCHA is tested five times per framework to account for inconsistencies.

Friendly Captcha

During testing, it was discovered that the free version does not include the Smart Difficulty Scaling setting, which can be previewed on the Demo. This revealed a significant difference: Friendly Captcha does not outright block detected bots, but instead serves them a very difficult PoW challenge, making large-scale spam attacks impractical. Therefore, to give Friendly Captcha a fair comparison, we tested the demo. Even though it is a demo, no forms were submitted during the test.

CaptchaFox

The non-interactive version is not included in the free plan of CaptchaFox, therefore we will test the interactive version using a self-hosted offline setup with the free plan. If a bot is detected, the client will be blocked, even if the challenge was correctly solved. The solving itself will be done by hand.

MYRA

An offline setup was used with the free version of the CAPTCHA. The configuration settings were as follows:

  • EU Captcha Protection: On
  • Initial difficulty value per challenge: 1
  • Initial delay in seconds per challenge: 2
  • Max parallel challenges per IP address: 1

captcha.eu

A self-hosted offline setup was used with the free version. No configuration options are available. captcha.eu returns a success field indicating whether the request is considered legitimate, along with a botability score between 0 and 1. Additionally, the PoW difficulty increases when bot activity is suspected. Each test run was performed from a different IP address. In the table below, the average botability scores are listed where available.

TrustCaptcha

A self-hosted offline setup was used with the free version. The free plan does not offer any configuration options.

Cloudflare Turnstile

A self-hosted offline setup was used with the widget set to non-interactive mode. Turnstile is included solely as a Big-Tech reference point to compare the smaller DSGVO-focused vendors against.

Manual Analysis

In this part, the client-side JavaScript loaded by each CAPTCHA is analyzed to understand how the solutions work under the hood. Where possible, a spoofing method is demonstrated that allows a simple bot to bypass the CAPTCHA.

Results - Automated Testing Against Automation Frameworks

Each framework was run five times per CAPTCHA. The results are shown as Not Blocked / Blocked (e.g. 5 / 0 means all five attempts passed undetected).

FrameworkFriendly Captcha (Demo)CaptchaFoxMYRAcaptcha.euTrustCaptchaCloudflare Turnstile
Selenium (Headed)High POW (Bot Detected)0 / 5High POW (Bot Detected)5 / 0 (Avg. Bot-Score 0.72)0 / 5 (Avg. Bot-Score 1.0)0 / 5
Selenium StealthHigh POW (Bot Detected)0 / 5High POW (Bot Detected)5 / 0 (Avg. Bot-Score 0.44)5 / 0 (Avg. Bot-Score 0.36)0 / 5
PuppeteerHigh POW (Bot Detected)0 / 5High POW (Bot Detected)5 / 0 (Avg. Bot-Score 0.73)0 / 5 (Avg. Bot-Score 1.0)0 / 5
Puppeteer (webdriver=false)High POW (Bot Detected)5 / 0High POW (Bot Detected)5 / 0 (Avg. Bot-Score 0.54)1 / 4 (Avg. Bot-Score 0.44)0 / 5
Puppeteer-Extra-StealthHigh POW (Bot Detected)5 / 0High POW (Bot Detected)5 / 0 (Avg. Bot-Score 0.55)3 / 2 (Avg. Bot-Score 0.47)0 / 5
DrissionPageDefault POW (Not Detected)5 / 05 / 05 / 0 (Avg. Bot-Score 0.62)0 / 5 (Avg. Bot-Score 0.78)5 / 0
Undetected-ChromedriverDefault POW (Not Detected)5 / 05 / 05 / 0 (Avg. Bot-Score 0.55)5 / 0 (Avg. Bot-Score 0.28)5 / 0
PlaywrightHigh POW (Bot Detected)0 / 5High POW (Bot Detected)5 / 0 (Avg. Bot-Score 0.75)0 / 5 (Avg. Bot-Score 1.0)0 / 5
HeroDefault POW (Not Detected)5 / 05 / 05 / 0 (Avg. Bot-Score 0.40)5 / 0 (Avg. Bot-Score 0.44)5 / 0

Friendly Captcha and MYRA match the detection rate of Cloudflare Turnstile, detecting the same set of frameworks. It is worth noting that neither Friendly Captcha nor MYRA outright blocks detected bots. Instead, they serve a significantly harder PoW challenge, which acts as a rate limiter rather than a hard block.

CaptchaFox detects basic frameworks (Selenium, Puppeteer, Playwright) but fails to detect alteration of the navigator.webdriver property. Its detection relies primarily on the navigator.webdriver flag and headless browser indicators (not shown above), which are spoofed by stealth frameworks.

captcha.eu does not block any of the tested frameworks. While it assigns higher bot scores to detected automation, every attempt still passed. This suggests that captcha.eu relies on the integrator to enforce thresholds based on the returned bot score, rather than blocking on its own. With that in mind, if a threshold of 0.7 were enforced, captcha.eu would have blocked Selenium, Puppeteer, and Playwright.

TrustCaptcha shows inconsistent results. It reliably detects Selenium, Puppeteer, and Playwright, but struggles with stealth frameworks. Notably, it flags DrissionPage with a high bot score of 0.78, though after closer observation this is most likely due to rate limiting rather than actual detection, since individual test runs have returned scores well below 0.5.

Overall, the three strongest vendors in terms of detection (Friendly Captcha, MYRA, and Cloudflare Turnstile) all fail against the same three frameworks: DrissionPage, Undetected-Chromedriver, and Hero.

Results - Manual Analysis and Why Obfuscation is Important

A detailed breakdown of each CAPTCHA’s internals is beyond the scope of this blog. The table below provides an overview. If you want to replicate this yourself, inspect the JavaScript files loaded in the browser — the one containing the bot detection logic is typically the largest. When in doubt, search for webdriver.

The key takeaway is that without proper obfuscation, the detection logic can be analyzed and bypassed (see further below).

Overview

CriteriaFriendly CaptchaCaptchaFoxMYRAcaptcha.euTrustCaptchaCloudflare Turnstile
ObfuscatedNo (minified only)Yes (obfuscator.io)No (minified only)No (minified only)No (minified only)Yes (heavy)
Can be deobfuscatedN/AYes (synchrony)N/AN/AN/ANo
FingerprintingYes (56 signals)Yes (48 signals)Yes (27 probes, 7 categories)Yes (BotD + custom)Yes (6 hashes + 40 props)Unknown
Behavior AnalysisYes (via SDK)NoYesYesYesUnknown
Proof-of-WorkYes (BLAKE3 WASM)NoYes (SipHash + Argon2id)Yes (SHA-256)Yes (SHA-256)Unknown
Time-to-PassImplicit (PoW duration)Server-side onlyImplicit (PoW duration)Yes (triple-layered)Implicit (PoW duration)Unknown
CDP DetectionYes (Error.prepareStackTrace)Yes (console.debug)Yes (Error stack getter)NoNoUnknown
WebDriver Descriptor CheckYesNoYesNoNoUnknown
Native Function IntegrityYes (stack trace recording)NoYes (Function.toString check)NoNoUnknown
Automation Globals ScanningNoNoYes (25 properties)Yes (BotD + regex)Yes (Selenium, WebDriver, PhantomJS, CDP)Unknown
Canvas FingerprintingYes (48 fonts + emoji)NoYes (5 tests + tampering)NoYes (text rendering)Unknown
Audio FingerprintingYes (oscillator + compressor)NoYes (5 oscillator configs)NoYes (oscillator frequency)Unknown
Input Value Setter HookNoNoNoNoNoUnknown
Response IntegrityNo (plain JSON)NoWeak (XOR + base64)Weak (Caesar cipher)No (base64 only)Unknown

Except for Cloudflare and CaptchaFox, no CAPTCHA vendor implements obfuscation. Even CaptchaFox, which uses obfuscator.io, is trivially deobfuscatable using synchrony, a deobfuscator that specifically targets obfuscator.io output. To put this into perspective, the entire process of deobfuscating and understanding CaptchaFox’s detection logic took about one hour, without using AI. Cloudflare, on the other hand, implements strong custom obfuscation that cannot easily be broken (I really tried), so its detection logic remains hidden.

How to abuse the lack of obfuscation

In this section, I demonstrate a proof-of-concept bypass for each CAPTCHA (except Cloudflare) using a vanilla Selenium bot, the same bot that was detected by every vendor in the automated tests above. By modifying the JavaScript file loaded in the bot’s browser, the data sent to the CAPTCHA vendor is altered to make the bot appear as a legitimate browser.

This section is for educational purposes only. Since the goal of this blog is not to provide ready-made exploits, only video demonstrations are shown. However, the general strategy is always the same:

  1. Find the chokepoint where all collected data is sent to the server
  2. Add a console.log at that point, run it in a normal browser, and copy the values
  3. Alter or hardcode the collected data using the legitimate values gathered in step 2
  4. Bypass the CAPTCHA

Setup

The bot code stays the same with one addition. The bot will optionally route through a Burp Suite proxy which has a match-and-replace rule active that modifies the JavaScript to imitate a legitimate Chromium browser.

The video will show the first attempt with the proxy and then without the proxy. Note that Cloudflare Turnstile is not included since its JavaScript could not be reversed.

Friendly Captcha

You can use the code below to test the form on Friendly Captcha’s website. Without the proxy, there is a high PoW requirement when solving the CAPTCHA. However, with the proxy enabled, the CAPTCHA is solved as if by a legitimate browser. As stated, I will not publish the match-and-replace code.

Show Selenium Code
import argparse
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from time import sleep
import datetime

TARGET_URL = "https://friendlycaptcha.com#demo"
BURP_PROXY = "http://127.0.0.1:8081"
NUM_RUNS = 1

parser = argparse.ArgumentParser()
parser.add_argument("--proxy", action="store_true", help="Route traffic through Burp proxy")
args = parser.parse_args()

for i in range(NUM_RUNS):
    print(f"\n--- Run {i+1}/{NUM_RUNS} at {datetime.datetime.now()} ---")
    options = Options()
    if args.proxy:
        options.add_argument(f"--proxy-server={BURP_PROXY}")
        options.add_argument("--ignore-certificate-errors")
    driver = webdriver.Chrome(options=options)
    try:
        driver.get(TARGET_URL)
        # Scroll the form into view so it's visible on screen
        form = WebDriverWait(driver, 10).until(
            EC.presence_of_element_located((By.XPATH, '//*[@id="wpforms-636-field_1"]'))
        )
        driver.execute_script("arguments[0].scrollIntoView({block:'center'});", form)
        sleep(1)
        # Fill in the contact form
        name_field = WebDriverWait(driver, 10).until(
            EC.element_to_be_clickable((By.XPATH, '//*[@id="wpforms-636-field_1"]'))
        )
        name_field.send_keys("TestName")
        driver.find_element(By.XPATH, '//*[@id="wpforms-636-field_8"]').send_keys("This is a test message")
        # Dismiss cookie banner if present
        try:
            cookie_btn = WebDriverWait(driver, 5).until(
                EC.element_to_be_clickable((By.CSS_SELECTOR, "[data-cky-tag='accept-button'], .cookie-accept, #onetrust-accept-btn-handler, button[id*='accept'], button[class*='accept']"))
            )
            cookie_btn.click()
            sleep(0.5)
        except Exception:
            pass
        # Switch into the captcha iframe and click the start button
        iframe = WebDriverWait(driver, 10).until(
            EC.presence_of_element_located((By.CSS_SELECTOR, "iframe.frc-i-widget"))
        )
        driver.execute_script("arguments[0].scrollIntoView({block:'center'});", iframe)
        sleep(0.5)
        driver.switch_to.frame(iframe)
        btn = WebDriverWait(driver, 10).until(
            EC.presence_of_element_located((By.CSS_SELECTOR, "button[role='checkbox']"))
        )
        driver.execute_script("arguments[0].click();", btn)
        driver.switch_to.default_content()
        # Wait for the captcha to solve
        WebDriverWait(driver, 30).until(
            lambda d: d.find_element(By.CSS_SELECTOR, "input[name='frc-captcha-response']").get_attribute("value") not in ["", ".UNACTIVATED", ".UNSTARTED", ".FETCHING", ".SOLVING"]
        )
        sleep(1)
        submit = driver.find_element(By.XPATH, '//*[@id="wpforms-submit-636"]')
        driver.execute_script("arguments[0].scrollIntoView({block:'center'});", submit)
        sleep(0.5)
        driver.execute_script("arguments[0].click();", submit)
        sleep(3)
        print(f"Response:\n{driver.page_source}")
    finally:
        driver.quit()

The video shows that in the first run, with the Burp Suite proxy match-and-replace rule in place, the solving time is normal. In contrast, the second run uses the same bot framework without the proxy, where it is detected by Friendly Captcha and receives a significantly harder PoW challenge.

CaptchaFox

Show Selenium Code
import argparse
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

BURP_PROXY = "http://127.0.0.1:8081"
BASE_URL = "http://127.0.0.1:8080"
NUM_RUNS = 1

def main():
    parser = argparse.ArgumentParser(description="CaptchaFox Selenium test with optional Burp proxy")
    parser.add_argument("--proxy", action="store_true", help="Route traffic through Burp proxy")
    args = parser.parse_args()

    options = webdriver.ChromeOptions()
    options.add_experimental_option("detach", True)

    if args.proxy:
        options.add_argument(f"--proxy-server={BURP_PROXY}")
        options.add_argument("--ignore-certificate-errors")

    for i in range(NUM_RUNS):
        print(f"\n--- Run {i+1}/{NUM_RUNS} ---")

        driver = webdriver.Chrome(options=options)

        try:
            driver.get(f"{BASE_URL}/captcha_fox")
            WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.ID, "email")))
            driver.find_element(By.ID, "email").send_keys("[email protected]")
            driver.find_element(By.ID, "password").send_keys("TestPassword123")

            print("Form filled. Solve the CAPTCHA and submit manually.")
            input("Press Enter after submitting to continue to next run...")
            print(f"Response:\n{driver.page_source}")
        finally:
            driver.quit()


if __name__ == "__main__":
    main()

Since CaptchaFox is obfuscated, we need to replace the entire JavaScript file with a deobfuscated version that contains an array of hardcoded legitimate values. This array spoofs Selenium and makes the challenge solvable.

captcha.eu

Show Selenium Code
import argparse
from time import sleep

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

BURP_PROXY = "http://127.0.0.1:8081"
NUM_RUNS = 1
BASE_URL = "http://localhost:8080"

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--proxy", action="store_true", help="Use Burp proxy")
    args = parser.parse_args()

    for i in range(NUM_RUNS):
        print(f"\n--- Run {i+1}/{NUM_RUNS} ---")

        options = webdriver.ChromeOptions()
        if args.proxy:
            options.add_argument(f"--proxy-server={BURP_PROXY}")
            options.add_argument("--ignore-certificate-errors")

        driver = webdriver.Chrome(options=options)

        try:
            driver.get(f"{BASE_URL}/captcha_eu")
            WebDriverWait(driver, 10).until(
                EC.element_to_be_clickable((By.ID, "email"))
            )
            driver.find_element(By.ID, "email").send_keys("[email protected]")
            driver.find_element(By.ID, "password").send_keys("TestPassword123")
            driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
            sleep(20)
            print(f"Response:\n{driver.page_source}")
        finally:
            driver.quit()


if __name__ == "__main__":
    main()

After the form is submitted, the widget appears in the bottom-right corner of the screen, which is not visible in the video. In the first run, the spoofed bot receives a bot score of 0.375 and a low PoW difficulty, while the unmodified bot receives a bot score of 0.745 and a significantly higher PoW difficulty.

Myra

Show Selenium Code
import argparse
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from time import sleep

BASE_URL = "http://127.0.0.1:8080"
BURP_PROXY = "http://127.0.0.1:8081"
NUM_RUNS = 1

parser = argparse.ArgumentParser()
parser.add_argument("--proxy", action="store_true", help="Route traffic through Burp proxy")
args = parser.parse_args()

for i in range(NUM_RUNS):
    print(f"\n--- Run {i+1}/{NUM_RUNS} ---")
    options = Options()
    if args.proxy:
        options.add_argument(f"--proxy-server={BURP_PROXY}")
        options.add_argument("--ignore-certificate-errors")
    driver = webdriver.Chrome(options=options)
    try:
        driver.get(f"{BASE_URL}/eucaptcha")
        # Fill in the form
        WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.ID, "email")))
        driver.find_element(By.ID, "email").send_keys("[email protected]")
        driver.find_element(By.ID, "password").send_keys("TestPassword123")
        # Wait for MYRA captcha to solve (PoW runs automatically)
        if not args.proxy:
            sleep(20)
        else:
            sleep(10)
        # Submit the form
        driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
        sleep(3)
        print(f"Response:\n{driver.page_source}")
    finally:
        driver.quit()

Similar to Friendly Captcha, the spoofed bot receives a low PoW difficulty and passes, while the unmodified bot receives a PoW challenge that cannot be solved even after 20 seconds.

Trust Captcha

Show Selenium Code
import argparse
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from time import sleep

BASE_URL = "http://127.0.0.1:8080"
BURP_PROXY = "http://127.0.0.1:8081"
NUM_RUNS = 1

parser = argparse.ArgumentParser()
parser.add_argument("--proxy", action="store_true", help="Route traffic through Burp proxy")
args = parser.parse_args()

for i in range(NUM_RUNS):
    print(f"\n--- Run {i+1}/{NUM_RUNS} ---")
    options = Options()
    if args.proxy:
        options.add_argument(f"--proxy-server={BURP_PROXY}")
        options.add_argument("--ignore-certificate-errors")
    driver = webdriver.Chrome(options=options)
    try:
        driver.get(f"{BASE_URL}/trustcaptcha")
        WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.ID, "email")))
        driver.find_element(By.ID, "email").send_keys("[email protected]")
        driver.find_element(By.ID, "password").send_keys("TestPassword123")
        sleep(12)
        driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
        sleep(3)
        print(f"Response:\n{driver.page_source}")
    finally:
        driver.quit()

In the first run, the spoofed bot receives a bot score of 0.488. The unmodified bot, as previously shown, is consistently detected with a score of 1.0.

Final words

This blog illustrated that DSGVO/DACH-based CAPTCHA vendors can compete with Big Tech companies like Cloudflare when it comes to automation framework detection. However, one critical issue remains: without proper obfuscation, none of the tested vendors can be considered secure. As demonstrated in the bypass section, every vendor except Cloudflare was bypassed using a simple Selenium bot, solely because their detection logic was readable and modifiable. Obfuscation is not optional — it is the baseline requirement.

Friendly Captcha is, in my opinion, the best choice among the tested vendors (excluding Cloudflare). It delivers strong detection results and, though not covered in detail here, offers the best usability of all the CAPTCHAs tested.

MYRA matches Friendly Captcha in detection capability but has an outdated user interface, and verifications take noticeably longer.

CaptchaFox is the only DSGVO vendor that implements obfuscation, but since it relies on obfuscator.io, it can be reversed using publicly available deobfuscators within minutes. Additionally, it lacks both behavior analysis and sophisticated bot detection, leaving it with fewer defensive layers than its competitors.

captcha.eu and TrustCaptcha both implement comprehensive behavior analysis and proof-of-work, but without obfuscation, their detection logic is fully exposed and trivially bypassable.

In summary, the detection capabilities across vendors are promising, but they are undermined by the lack of obfuscation. Until this is addressed, an attacker with basic JavaScript knowledge can bypass any of these CAPTCHAs.

Responsible Disclosure

This review is intended as independent security research to help organizations make informed decisions when choosing a CAPTCHA provider. All testing was conducted against self-hosted setups using free or trial versions of each CAPTCHA, with the exception of Friendly Captcha, which was tested against the vendor-provided demo. No production systems were targeted and no forms were submitted on third-party websites. The findings are based on analysis of each product’s client-side JavaScript, which is publicly delivered to every visitor’s browser. The bypass demonstrations were performed exclusively against self-hosted environments. No exploit code is published in this blog, only video demonstrations of the bypasses are provided.