captchaforge 0.2.0

Automatic CAPTCHA detection and multi-strategy solving for chromiumoxide-driven headless browsers (Cloudflare Turnstile, reCAPTCHA v2/v3, hCaptcha, image grids, audio, sliders).
Documentation

captchaforge

ci license

Automatic CAPTCHA detection and multi-strategy solving for chromiumoxide-driven headless browsers — Cloudflare Turnstile, reCAPTCHA v2/v3, hCaptcha, image grids, audio challenges, sliders.

Part of the Santh security research ecosystem. Beta — used in production by wafrift and golemn-browser but the public API may shift before 1.0. Pin the version in Cargo.toml.

Quick start

Add to Cargo.toml:

[dependencies]
captchaforge = { git = "https://github.com/santhsecurity/captchaforge", branch = "main" }
chromiumoxide = "0.9"
tokio = { version = "1", features = ["full"] }

Detect + solve on the page your headless browser is on:

use captchaforge::{detect, solver::CaptchaSolverChain};

async fn handle_captcha(page: &chromiumoxide::Page) -> anyhow::Result<()> {
    let info = detect::detect(page).await?;
    if !detect::is_captcha(&info) {
        return Ok(());
    }

    let chain = CaptchaSolverChain::default_chain();
    let outcome = chain.solve(page, &info).await;
    if outcome.success {
        tracing::info!(method = ?outcome.method, "captcha solved");
    } else if let Some(screenshot) = outcome.screenshot {
        tracing::warn!(bytes = screenshot.len(), "human review needed; screenshot attached");
    } else {
        tracing::warn!("all strategies failed; human-in-the-loop needed");
    }
    Ok(())
}

Strategies

The default chain tries strategies in order; the first one that succeeds wins. Each result is recorded in a per-domain pattern store so repeat visits prefer the strategy that worked last time.

  1. Behavioural — realistic mouse movement + timing for Turnstile and reCAPTCHA v3. Solvers verify the response token is actually present before reporting success.
  2. Vision-LLM — screenshot the challenge, hand it to a multimodal model (Ollama qwen3-vl:30b out of the box, configurable), parse the model's grid / click answer, simulate the mouse.
  3. Audio bypass — click the accessibility audio challenge, run STT, type the transcribed answer, then verify the reCAPTCHA token was accepted.
  4. Crowd-sourced pattern memory — track which strategy succeeded per-domain and re-prioritise the solver order on next visit.
  5. Human fallback — return unsolved with a base64 JPEG screenshot in CaptchaSolveResult::screenshot so a downstream human-in-the-loop can finish.

You can plug in your own solver by implementing the CaptchaSolver trait and adding it to the chain via chain.add_solver(...) before default_chain() strategies.

Detection coverage

Out of the box detect::detect recognises:

  • Cloudflare Turnstile (.cf-turnstile, [data-turnstile-sitekey], script[src*="challenges.cloudflare.com"])
  • Cloudflare bot interstitial ("Just a moment", #challenge-form)
  • reCAPTCHA v2 + v3 (.g-recaptcha, [data-sitekey], script[src*="google.com/recaptcha"])
  • hCaptcha (.h-captcha, [data-hcaptcha-sitekey], script[src*="hcaptcha.com"])
  • Image grids, audio challenges, sliders (when the surrounding code classifies them via DetectedCaptcha::*)

Adding a new family is one match arm in detect.rs plus a JavaScript selector. PRs welcome.

Configuration

VLM endpoint and model are runtime-overridable on VlmCaptchaSolver:

use captchaforge::solver::{VlmCaptchaSolver, CaptchaSolverChain};

let mut chain = CaptchaSolverChain::empty();
chain.add_solver(
    VlmCaptchaSolver::new()
        .with_endpoint("http://my-gpu-host:11434")
        .with_model("qwen3-vl:30b"),
);

Audio STT endpoint is similarly configurable on AudioCaptchaSolver.

Origin and licensing

Extracted from golemn-browser (originally GPL-3.0). The original author (Santh Project) re-licensed the extracted slice as MIT OR Apache-2.0 so it can be embedded in MIT/Apache-licensed downstream tools (notably wafrift for managed-WAF challenge handling and any future Santh tool that needs a captcha layer).

Testing

cargo test          # unit + integration (no chromium required)
cargo test --doc    # doctest in lib.rs

The test suite does not require a real chromium binary or a real Ollama server — all chromium-bound paths are pure Rust unit tests against the JavaScript / Rust glue layer. Tests that exercise the real headless browser live in wafrift-captchaforge-bridge where the chromium runtime is available.

License

MIT OR Apache-2.0 at your option. Copyright 2026 CORUM COLLECTIVE LLC.