# 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, plus a community-extensible TOML rule
pack for vendors like AWS WAF Captcha, DataDome, Akamai Bot Manager,
PerimeterX/HUMAN, Arkose/FunCaptcha, Geetest v3+v4, Friendly Captcha,
MTCaptcha, and WordPress math captchas.
> 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
async fn handle_captcha(page: &chromiumoxide::Page) -> anyhow::Result<()> {
// One-call convenience: detect, then run the default solver chain
// if a captcha is present. Returns Ok(None) for no-captcha pages.
if let Some(outcome) = captchaforge::auto_solve(page).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");
}
}
Ok(())
}
```
For full control — telemetry, token cache, custom solvers — drop down
to the explicit chain:
```rust
use std::sync::Arc;
use captchaforge::{detect, solver::{CaptchaSolverChain, TokenCache}};
let cache = Arc::new(TokenCache::new()); // 60s TTL by default
let chain = CaptchaSolverChain::default_chain()
.with_token_cache(cache); // hit cache before solving
let info = detect::detect(page).await?;
if detect::is_captcha(&info) {
let outcome = chain.solve(page, &info).await;
// ...
}
```
## Architecture surface (post-extraction)
- **`detect::Detector`** — single per-vendor detector trait. Eleven
built-ins (`TurnstileDetector`, `RecaptchaDetector`, …) live one per
file under `src/detect/`. Add a new detector by writing
`src/detect/<vendor>.rs` and registering it.
- **`detect::rules`** — community-extensible TOML rule layer. Drop a
`[[provider]]` entry in `rules/community.toml` (or your own file)
and the vendor is detected without writing any Rust. Bundled pack
covers DataDome, Akamai, PerimeterX/HUMAN, Arkose/FunCaptcha,
Geetest v3+v4, AWS WAF Captcha, Friendly Captcha, MTCaptcha,
WordPress math captchas.
- **`provider::CaptchaProvider`** — bundles a detector + the
recommended solver methods for that vendor in one trait. Built-in
providers wired via macro; new providers implement both traits in
one file.
- **`provider::ProviderRegistry::with_built_in_rules()`** — single
call returning the built-in detectors PLUS the bundled community
rule pack, all priority-sorted.
- **`solver::CaptchaSolverChain`** — multi-strategy chain with cache
+ telemetry + pattern-store routing. See "Strategies" below.
- **`solver::TokenCache`** — per-`(domain, captcha_type)` solved
token cache with configurable TTL. Cache hits skip the solver run
entirely and report through telemetry as `solver: "TokenCache"`.
- **`telemetry::SolverTelemetry`** — implement to capture every
solver attempt as a `SolveEvent`. Default is `NoopTelemetry`.
Production deployments plug in Prometheus / OTel / SQLite / Sentry.
- **`solver::PatternStore`** — per-domain learned routing.
`save_to_path` / `load_from_path` / `merge` for cross-restart and
cross-instance sharing.
- **`captchaforge::auto_solve()`** — top-level one-call API for the
90% case.
## Strategies
For each detected captcha the chain runs through a layered short
circuit + strategy stack. First match wins.
0. **Token cache** — if a `TokenCache` is installed and holds a
fresh `(domain, captcha_type)` token (default 60s TTL), return it
immediately tagged as `CrowdSourced`. Telemetry sees this as a
`Success` with `solver: "TokenCache"`. Skip the rest of the chain.
1. **Provider routing** — for `DetectedCaptcha::Custom` (TOML rule
layer), only solvers whose `SolveMethod` appears in the bundled
provider's `recommended_solver_methods` are eligible, in declared
order. Built-in detectors fall back to the standard `supports()`
filter.
2. **Pattern store ordering** — for built-in kinds, solvers eligible
under (1) get re-ordered so the historically-best method for this
domain runs first. PatternStore can be persisted across restarts
(`save_to_path` / `load_from_path`) and merged across worker
instances (`merge`).
3. **Behavioural** — realistic mouse movement + timing for Turnstile
and reCAPTCHA v3. Solvers verify the response token is actually
present before reporting `success`. On success, captures session
cookies into `CaptchaSolveResult::cookies` for replay.
4. **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. Captures session cookies on success.
5. **Audio bypass** — click the accessibility audio challenge, run
STT, type the transcribed answer, then verify the reCAPTCHA token
was accepted. Captures session cookies on success.
6. **Telemetry** — every attempt at every layer fires a `SolveEvent`
to the installed `SolverTelemetry` impl (default `NoopTelemetry`).
Outcomes: `Success`, `Failure`, `Error`, `Timeout`. Cache hits
surface as `Success` with `solver: "TokenCache"` so dashboards
distinguish solved-from-scratch from cache-served.
7. **Human fallback** — when every solver fails, return unsolved
with a base64 JPEG screenshot in `CaptchaSolveResult::screenshot`
so a downstream human-in-the-loop can finish.
Apply captured cookies to subsequent navigations via
`captchaforge::cookies::apply_to_page(&page, &result.cookies)` so the
WAF/vendor's trusted session is preserved and the next request does
not re-trigger the captcha.
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
Built-in detectors (one per file under `src/detect/`):
- 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"]`)
- Generic image / audio CAPTCHAs
- PoW CAPTCHAs (ALTCHA, mCaptcha, Friendly Captcha, Cap.dev)
- Slider / swipe / rotate / drag CAPTCHAs
- Canvas / SVG / math / icon / color CAPTCHAs
- Multi-step / wizard CAPTCHAs
- Shadow-DOM-hidden CAPTCHAs
Bundled community rule pack (`rules/community.toml`, registered
via `ProviderRegistry::with_built_in_rules()`):
- AWS WAF Captcha
- DataDome
- Akamai Bot Manager
- PerimeterX / HUMAN
- Arkose Labs / FunCaptcha
- Geetest v3 + v4
- Friendly Captcha
- MTCaptcha
- WordPress math captchas (Captcha by BestWebSoft, generic forms)
Adding a new family is two lines of TOML — see `rules/community.toml`
for the schema. Per-rule positive + negative HTML fixtures live under
`tests/rule_fixtures/<vendor>/`; the `community_rules` integration
test asserts every rule fires on its positive and rejects its
negative AND doesn't cross-fire on any other vendor's negative.
## Configuration
Two layers, lightest to heaviest:
**Env vars (zero-config)**
- `CAPTCHAFORGE_VLM_ENDPOINT` — Ollama endpoint URL.
- `CAPTCHAFORGE_VLM_MODEL` — VLM model name.
- `CAPTCHAFORGE_THIRDPARTY_API_KEY` — 2captcha-compatible service key.
- `CAPTCHAFORGE_THIRDPARTY_ENDPOINT` — override the service base URL.
- `CAPTCHAFORGE_CONFIG` — explicit Tier-A config file path.
**Tier-A operational config (`.captchaforge.toml`)**
Drop `.captchaforge.toml` in your project (or use the discovery search
path: `$CAPTCHAFORGE_CONFIG`, `./.captchaforge.toml`,
`./captchaforge.toml`, `$XDG_CONFIG_HOME/captchaforge/config.toml`).
The schema covers chain timeouts, cache TTL, VLM endpoint/model,
third-party service selection, and every `SolveConfig` field. See
`.captchaforge.toml.example` for the reference. Unknown keys are a
parse error so typos surface instead of silently being ignored.
```toml
per_solver_timeout_ms = 60000
[cache]
ttl_seconds = 90
[vlm]
endpoint = "http://gpu-host:11434"
[third_party]
service = "cap_solver"
api_key = "..."
```
**Builder methods (fine-grained)**
For fine-grained control inside one process, use the builder methods —
they always beat env + TOML.
```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`.
**Third-party solver service**
`ThirdPartyCaptchaSolver` talks the 2captcha-compatible HTTP protocol
used by 2captcha, CapMonster, and CapSolver — every TOML-bundled vendor
that recommends `ThirdPartyService` now actually has a solver to land
on. Without an API key configured, `supports()` returns false across
the board so the solver is skipped instead of issuing 401s.
```rust
use captchaforge::solver::{ThirdPartyCaptchaSolver, CaptchaSolverChain};
let mut chain = CaptchaSolverChain::empty();
chain.add_solver(
// Pick the service flavour, then pass the key:
ThirdPartyCaptchaSolver::cap_solver().with_api_key("..."),
);
```
## CLI
```sh
cargo install --path cli
captchaforge inspect-rules # list bundled + extra rules
captchaforge detect https://example.com/login # navigate, run detector
captchaforge solve https://example.com/login # navigate, run full chain
captchaforge inspect-rules --rules my-pack.toml # additive extra rules
```
## Telemetry
```rust
use std::sync::Arc;
use captchaforge::solver::CaptchaSolverChain;
use captchaforge::telemetry::{SolveEvent, SolverTelemetry};
struct ToProm;
impl SolverTelemetry for ToProm {
fn record(&self, evt: &SolveEvent<'_>) {
// ship evt.outcome, evt.time_ms, evt.solver, evt.domain to your collector
}
}
let chain = CaptchaSolverChain::default_chain()
.with_telemetry(Arc::new(ToProm));
```
The chain fires one event per solver attempt at every outcome
(`Success` / `Failure` / `Error` / `Timeout`). Cache hits also fire as
`Success` with `solver: "TokenCache"` so dashboards distinguish
solved-from-scratch from cache-served.
## 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 --workspace # unit + integration (no chromium required)
cargo test --doc # doctest in lib.rs
cargo test --test community_rules # asserts every TOML rule fires on its
# positive HTML fixture, rejects its negative,
# and doesn't cross-fire on other vendors' negatives
cargo test --test integration_chain_wires # cache + telemetry + chain wires
```
## Local proof (benches)
```sh
cargo run --example architecture_proof --release
```
Prints a markdown report covering the latency-critical paths added by
the architecture sweep:
- TokenCache get hit / miss
- ProviderRegistry::find_by_kind for Custom kinds
- RuleDetector probe generation (one-shot at registration)
- Per-rule HTML fixture match latency (one row per bundled vendor)
Browser-free, runs in well under a second after the release build.
Use to sanity-check that no future change regresses the hot path.
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.