# captchaforge
[](https://github.com/santhsecurity/captchaforge/actions/workflows/ci.yml)
[](#license)
Automatic CAPTCHA detection and multi-strategy solving for
[`chromiumoxide`](https://crates.io/crates/chromiumoxide)-driven headless
browsers — Cloudflare Turnstile, reCAPTCHA v2/v3, hCaptcha, image
grids, audio challenges, sliders.
> Part of the [Santh](https://santh.dev) security research ecosystem.
> **Beta** — used in production by [wafrift](https://github.com/santhsecurity/wafrift)
> and [golemn-browser](https://github.com/santhsecurity/golemn) but the
> public API may shift before 1.0. Pin the version in `Cargo.toml`.
## Quick start
Add to `Cargo.toml`:
```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:
```rust
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](https://ollama.com) `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`](src/solver.rs) 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`:
```rust
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](https://github.com/santhsecurity/golemn)
(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](https://github.com/santhsecurity/wafrift) for managed-WAF
challenge handling and any future Santh tool that needs a captcha
layer).
## Testing
```sh
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`](https://github.com/santhsecurity/wafrift/tree/main/crates/captchaforge-bridge)
where the chromium runtime is available.
## License
MIT OR Apache-2.0 at your option. Copyright 2026 CORUM COLLECTIVE LLC.