Skip to content

Feat/cap captcha and override remediation - #152

Open
tommy-ng0 wants to merge 5 commits into
crowdsecurity:mainfrom
tommy-ng0:feat/cap-captcha-and-override-remediation
Open

Feat/cap captcha and override remediation#152
tommy-ng0 wants to merge 5 commits into
crowdsecurity:mainfrom
tommy-ng0:feat/cap-captcha-and-override-remediation

Conversation

@tommy-ng0

Copy link
Copy Markdown
Contributor

No description provided.

Adds CAPTCHA_PROVIDER=cap, a self-hosted proof-of-work captcha
(https://capjs.js.org) that needs no third-party service.

Cap is reCAPTCHA-shaped but not reCAPTCHA-compatible, so it gets its own
verification path: a JSON POST to <endpoint>/<site_key>/siteverify, which
reports failures as a plain "error" string rather than reCAPTCHA's
"error-codes" array. Reusing the existing path would crash on pairs(nil).

Because the instance is self-hosted, its URLs are configuration rather than
constants. The browser and the bouncer can reach it on different addresses,
so CAPTCHA_API_ENDPOINT is the public base the widget is pointed at, and the
optional CAPTCHA_VERIFY_ENDPOINT lets verification stay on a private network.
Both compose with SITE_KEY, which cap embeds in its paths.

The widget is a custom element loaded as an ES module, not a script plus a
div, so the template now receives the markup pre-rendered as
captcha_frontend_js_tag and captcha_widget. The three previous variables are
still populated so custom templates keep rendering. Naming the widget's
hidden input via data-cap-hidden-field-name keeps GetCaptchaBackendKey()
unchanged for every provider.

An unknown CAPTCHA_PROVIDER is now rejected in New() instead of surfacing
later as a nil concatenation while rendering.
…tion

Serves one remediation for every bounced decision, whatever type the LAPI or
the appsec component returned. Setting OVERRIDE_REMEDIATION=captcha
challenges every blocked visitor rather than dropping them outright, which
keeps false positives recoverable.

Valid values are captcha and ban, mirroring FALLBACK_REMEDIATION; empty (the
default) honours the remediation as received, so existing configs are
unaffected. The override is applied before the fallback, so forcing captcha
while the captcha provider is misconfigured still degrades to
FALLBACK_REMEDIATION rather than serving nothing.
- pass remote_ip into ValidateCap and send it as X-Real-IP, so the cap
  instance sees the address that solved the challenge rather than the
  bouncer's own, which is all a server-to-server call would otherwise show
- assert the forwarded header in the stub siteverify used by test 23
A self-hosted cap instance is commonly reached over TLS it terminates
itself, and resty.http verifies by default, so a self-signed certificate
failed verification with "18: self-signed certificate".

- thread SSL_VERIFY from the config through captcha.New into the cap
  siteverify request
- normalise the value inside New: it is called before crowdsec.lua turns
  SSL_VERIFY into a boolean, and the raw "false" string is truthy in Lua
- leave the hosted providers verifying unconditionally, as they present
  publicly trusted certificates
ValidateCap returned true, meaning "solved", for any outcome short of a
decoded JSON body. A transport error or an error page from a proxy in
front of cap therefore let the visitor through and cached the result as
VALIDATED_STATE for CAPTCHA_EXPIRATION, for a token nothing had checked.

Cap is self-hosted and commonly rate limited per client IP, so a visitor
can provoke that outcome for themselves: spend their own bucket, submit
junk, and the 429 the edge returns is read as a solve. Every path now
returns false, leaving them on the captcha page to try again.

- fail closed on transport errors and on responses that are not JSON
- reject non-200 responses, kept separate from the decode failure so an
  outage or a tripped rate limit stays distinguishable in the logs from a
  visitor submitting a bad token
- add t/25, which stubs the 429 error page an nginx edge returns when
  limit_req trips, and asserts the captcha page is served again rather
  than the redirect that the previous behaviour produced

The hosted providers are left as they are: their endpoints are not
something a visitor can knock over to skip the captcha.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant