Context
POST /api/leads/v1/register-interest allows source: 'desktop-settings' to bypass Turnstile because the Tauri desktop app can't render the browser captcha. The source field is an unsigned client-supplied string — an attacker can send source: 'desktop-settings' to skip the captcha entirely.
Flagged by @koala73 in the #3207 review (PR #3242). Partially mitigated in the review-response commit by adding a tighter 2/hr per-IP cap specifically for the desktop path (matches the legacy in-memory cap that was lost during the sebuf migration). That caps the blast radius of the bypass but does not close it.
The actual fix
Require the desktop sidecar to send a signed header — a short-lived HMAC of the request body keyed on a shared secret that ships only inside the desktop binary. Handler rejects desktop-source requests that lack a valid signature. Non-desktop requests (browser, pro-test, curl) continue to use the Turnstile path as today.
Why this is a separate issue
- Requires shipping a new env/secret to the Tauri build pipeline.
- All deployed desktop app versions need to know the signature scheme — either roll it out over a version gate (new desktop builds send signature, handler accepts unsigned from old versions for a deprecation window), or accept a hard cutover.
- Out of scope for the sebuf-migration PR which is purely a refactor.
Suggested design
- Secret in
WM_DESKTOP_SHARED_SECRET env on the Vercel side; baked into the desktop binary at build time.
- Header:
X-Desktop-Auth: <base64url(hmac-sha256(secret, timestamp + body))>
X-Desktop-Auth-Timestamp: unix ms, reject if > 5min skew.
- Handler logic:
- If
source === 'desktop-settings': require valid signature. If invalid → fall through to Turnstile path (or 403).
- If no
source === 'desktop-settings': ignore the header entirely, require Turnstile.
- Deprecation window: if env secret unset, allow the legacy unsigned path but emit a
desktop_unsigned_bypass_used metric.
Related
Context
POST /api/leads/v1/register-interestallowssource: 'desktop-settings'to bypass Turnstile because the Tauri desktop app can't render the browser captcha. Thesourcefield is an unsigned client-supplied string — an attacker can sendsource: 'desktop-settings'to skip the captcha entirely.Flagged by @koala73 in the #3207 review (PR #3242). Partially mitigated in the review-response commit by adding a tighter 2/hr per-IP cap specifically for the desktop path (matches the legacy in-memory cap that was lost during the sebuf migration). That caps the blast radius of the bypass but does not close it.
The actual fix
Require the desktop sidecar to send a signed header — a short-lived HMAC of the request body keyed on a shared secret that ships only inside the desktop binary. Handler rejects desktop-source requests that lack a valid signature. Non-desktop requests (browser, pro-test, curl) continue to use the Turnstile path as today.
Why this is a separate issue
Suggested design
WM_DESKTOP_SHARED_SECRETenv on the Vercel side; baked into the desktop binary at build time.X-Desktop-Auth: <base64url(hmac-sha256(secret, timestamp + body))>X-Desktop-Auth-Timestamp: unix ms, reject if > 5min skew.source === 'desktop-settings': require valid signature. If invalid → fall through to Turnstile path (or 403).source === 'desktop-settings': ignore the header entirely, require Turnstile.desktop_unsigned_bypass_usedmetric.Related