Repository navigation
Conversation
The public subscribe form was only rate limited, so it could be used to send verification emails to addresses that never asked for them. A new Captcha settings page lets the provider be chosen from the dashboard: none (default), ALTCHA (self-hosted proof of work, no keys or external service), Cloudflare Turnstile, hCaptcha or Google reCAPTCHA v2. Providers sit behind a Captcha interface resolved by CaptchaManager, so more can be added as drivers. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8e892df6e6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const widget = document.currentScript.previousElementSibling | ||
| const form = widget.closest('form') | ||
| const input = widget.querySelector('input[name="altcha"]') | ||
| const buttons = form ? form.querySelectorAll('[type="submit"]') : [] |
There was a problem hiding this comment.
Query the submit button after the form is parsed
When this inline script runs during normal HTML parsing, the <button> later in status-page/subscribe.blade.php has not been added to the DOM yet, so this static NodeList is empty. setState() therefore never disables it; a visitor who submits while the proof-of-work is still running sends an empty altcha value and gets a validation failure. Initialize after parsing or manage the state via the repository's Alpine x-data pattern instead.
AGENTS.md reference: AGENTS.md:L82-L85
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 7610694. The widget is now an Alpine component (resources/js/altcha.js, loaded only when an ALTCHA widget is on the page) and its init() runs after the form is parsed. It disables the submit buttons until the challenge is solved, and it also blocks submit events in the meantime, so a forced submit can't send an empty payload. The inline script is gone.
| } | ||
|
|
||
| const sha256 = async (text) => { | ||
| const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text)) |
There was a problem hiding this comment.
Provide hashing when Web Crypto is unavailable
On status pages served over plain HTTP from a non-loopback host, Web Crypto's subtle API is unavailable because it is restricted to secure contexts. sha256() then rejects every run, the widget enters error, and with ALTCHA enabled the server rejects every subscription, even though this package can be installed into consumer hosts whose routing and configuration it does not control. Add a compatible fallback or prevent ALTCHA from being selected where a secure context is not guaranteed.
AGENTS.md reference: AGENTS.md:L100-L107
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 7610694. sha256() now falls back to a plain JavaScript SHA-256 when crypto.subtle is unavailable, so ALTCHA also works on pages served over plain HTTP. I checked the fallback against Node's crypto on 2,000+ inputs, including padding edge cases and multibyte text. It hashes 50k candidates in about 0.3 s. I also ran the form end to end in Chromium over plain HTTP from a non-loopback host.
|
|
||
| public function verify(Request $request): bool | ||
| { | ||
| $payload = json_decode(base64_decode((string) $request->input('altcha'), true) ?: '', true); |
There was a problem hiding this comment.
Reject array ALTCHA payloads before casting
With ALTCHA enabled, a request such as altcha[]=x makes $request->input('altcha') an array. Casting it to string raises E_WARNING, which Laravel converts to ErrorException, so this public endpoint returns a 500 instead of the expected captcha validation error. Check is_string before decoding, as the hosted-driver path already does.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 7610694. Non-string or empty altcha input is rejected before decoding, and a test now covers altcha[]=x.
- Move the widget to an Alpine component (resources/js/altcha.js), loaded only when the page has an ALTCHA widget. It runs after the form is parsed, so the submit button is actually disabled (and submits blocked) until the challenge is solved. - Fall back to a plain JavaScript SHA-256 when Web Crypto is unavailable, so ALTCHA also works on pages served over plain HTTP. - Reject non-string ALTCHA payloads instead of failing with a 500. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Problem
The public subscribe form (
POST /subscribe) is only protected bythrottle:6,1. Every submission for an unverified address queues a new verification email, so the form can be used to send emails to addresses that never asked for them.Solution
A new Settings → Manage Captcha page lets admins choose a provider for the subscribe form:
Implementation
Cachet\Captcha\Captchainterface (enabled(),render(),verify()), resolved byCaptchaManager(anIlluminate\Support\Manager) from the newCaptchaSettings(groupcaptcha,secret_keyencrypted). Adding a provider means adding a driver.SiteVerifyCaptcha, which posts to each provider'ssiteverifyendpoint. Network or HTTP errors fail closed and are logged.GET /subscribe/captcha(throttled) issues HMAC-signed challenges that expire after 5 minutes, and each solution can only be used once (cache). A small inline script solves the challenge withcrypto.subtleand enables the submit button.<x-cachet::captcha />renders the active widget inside the form, along with its validation error.enandpt_BR. Other locales fall back toen.Tests
SubscribeCaptchaTest: the default stays unchanged; ALTCHA happy path plus missing, malformed, wrong-number, unsigned, expired and replayed solutions; each hosted provider withHttp::fakefor success, refusal, missing token, connection error and HTTP 500; providers stay disabled until both keys are set.ManageCaptchaTest: page renders, fields show and hide per provider, keys are required for hosted providers, and the secret key is stored encrypted.The full suite, Pint and PHPStan pass locally.