security fix(gateway): defend pre-auth device-signature verify against CPU DoS#77492
security fix(gateway): defend pre-auth device-signature verify against CPU DoS#77492fede-kamel wants to merge 1 commit into
Conversation
|
Codex review: needs real behavior proof before merge. Reviewed June 12, 2026, 5:15 PM ET / 21:15 UTC. Summary PR surface: Source +152, Tests +452, Other +349. Total +953 across 11 files. Reproducibility: yes. Current main’s unauthenticated device path reaches Ed25519 parsing and verification, and the supplied PoC provides a concrete running-gateway flood with measured latency degradation; this read-only review did not rerun destructive load. Review metrics: 2 noteworthy metrics.
Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Proof guidance:
Risk before merge
Maintainer options:
Copy recommended automerge instructionNext step before merge
Security Review findings
Review detailsBest possible solution: Wrap the complete device-signature admission, crypto verification, authorization result, and limiter accounting in the existing keyed serializer; add concurrent forged-burst and shared-IP reset coverage; then provide current-head running-gateway proof. Do we have a high-confidence way to reproduce the issue? Yes. Current main’s unauthenticated device path reaches Ed25519 parsing and verification, and the supplied PoC provides a concrete running-gateway flood with measured latency degradation; this read-only review did not rerun destructive load. Is this the best way to solve the issue? No. The layered bounds and shape checks are useful, but the limiter must reuse the repository’s atomic keyed serializer across the entire expensive attempt before this is the best fix. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 8d9ce35b92c4. Label changesLabel justifications:
Evidence reviewedPR surface: Source +152, Tests +452, Other +349. Total +953 across 11 files. View PR surface stats
Security concerns:
Acceptance criteria:
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
3bf503e to
4a844cf
Compare
|
Addressing the two Codex findings on 1. Limiter now runs before any pre-auth crypto ( Hoisted the 2. Bucket reset deferred until after Two changes:
Net behavior: an attacker with their own keypair producing a valid self-signature still consumes the bucket because Coverage added in
Test results: Two pre-existing assertions in Re-review progress:
|
4a844cf to
49334e7
Compare
|
@steipete when you have a moment — security fix for pre-auth device-signature CPU DoS, CI green. Will rebase to clear the conflicts before merge. Thanks! |
4b6b34a to
3166348
Compare
|
cc @openclaw/openclaw-secops for visibility — rebased onto main, mergeable. Just routing through the right team. No rush. Thanks! |
3166348 to
6233fc0
Compare
|
Rebased onto current @openclaw/openclaw-secops — flagging for review (pre-auth device-signature CPU-DoS hardening). |
592b198 to
6233fc0
Compare
6233fc0 to
facb002
Compare
|
ClawSweeper PR egg 🎁 Pass real behavior proof to wake the egg and unlock a hatchable treat. Where did the egg go?
|
facb002 to
1fc8137
Compare
6d0036f to
d3ca8cf
Compare
…lification DoS Pre-auth handshakes that include a `device` block previously ran `crypto.createPublicKey` plus a v3-then-v2 `crypto.verify` per request, gated only by frame-size limits — an unauthenticated remote attacker could pin a single Gateway core (~500 forged handshakes/s/IP measured in PoC, 50x p50 / 95x p99 legit handshake degradation). Layered defense: - Schema: cap device.publicKey at 1024 chars and device.signature at 256 chars (every valid Ed25519 form fits with margin) so oversized strings never reach the crypto path. - Pre-check: short-circuit verifyDeviceSignature, deriveDeviceIdFromPublicKey, and normalizeDevicePublicKeyBase64Url on inputs that cannot decode to a 32-byte raw key or 64-byte signature. - Rate limit: gate the verify behind a new AUTH_RATE_LIMIT_SCOPE_DEVICE_SIGNATURE bucket (per-IP, fires through the always-constructed browser-origin limiter and through the regular limiter when configured). In-process integration test exercises the full WS handshake and proves the rate-limit gate truncates attacker work after maxAttempts; reverting the gate makes the test fail.
d3ca8cf to
6a6a120
Compare
Closes #77979
Summary
deviceblock rancrypto.createPublicKeyplus a v3-then-v2crypto.verifyper request, gated only by frame-size limits. An unauthenticated remote attacker could pin a single Gateway core: PoC against a real running gateway hits ~510 forged handshakes/s/IP, degrading legit handshake p50 by 48.7× (1.16 ms → 56.6 ms) and p99 by 94.7×.device.publicKey(1024) anddevice.signature(256); shape pre-check onverifyDeviceSignature/deriveDeviceIdFromPublicKey/normalizeDevicePublicKeyBase64Urlrejecting inputs that cannot decode to a 32-byte raw key or 64-byte signature; newAUTH_RATE_LIMIT_SCOPE_DEVICE_SIGNATUREbucket gating the verify per IP. The browser-origin rate limiter is always constructed (server.impl.ts:424), so the gate fires for browser-origin clients even when nogateway.auth.rateLimitis configured. For non-browser-origin clients, the gate fires when the operator configures a rate limiter (the typical production case).Reproduction
The PoC (
scripts/poc-preauth-flood-ws.mjs) opens N concurrent WS connections, each completing the connect-challenge handshake with a real Ed25519 PEM public key and a random 64-byte signature. The gateway reaches the verify path (server-issued nonce echoed back,device.idmatchessha256(rawPublicKey)) and runscreatePublicKey+ v3verify+ v2verifyper request. A separate measurement client probes time-to-first-event in parallel.Against a vulnerable gateway:
Observed:
To validate the fix end-to-end without exposing a dev gateway to the LAN, the in-process integration test runs the same forged-handshake sequence against a real
withGatewayServerinstance configured withrateLimit.maxAttempts: 1, exemptLoopback: true:The test asserts:
error.details.reason === "device-signature"(the verify ran).error.details.reason === "device-signature-rate-limited"andretryAfterMs > 0— the gate short-circuited before any crypto.Reverting the
recordFailureline in the rate-limit gate makes both tests fail withexpected 'device-signature' to be 'device-signature-rate-limited'.Test plan
src/gateway/server.preauth-signature-rate-limit.test.ts— new in-process WS integration test (2 cases). Both fail when the fix is disabled.src/infra/device-identity.test.ts— new shape pre-check tests: oversized PEM, oversized non-PEM, signature-shaped input rejected as public key, public-key-shaped input rejected as signature, all without invoking crypto.src/gateway/protocol/connect-device-bounds.test.ts— new schema-cap tests:validateConnectParamsrejects oversizeddevice.publicKeyanddevice.signature; accepts them at the documented bounds.src/gateway/server/ws-connection/handshake-auth-helpers.test.ts— 27/27 pass, no regression.Verification
pnpm testof the four affected files: 41/41 pass.grep AUTH_RATE_LIMIT_SCOPE_DEVICE_SIGNATUREon the bundle).pnpm tsgoare pre-existing and unrelated (pi-embedded-runner.*AgentMessage casts and missingweb-tree-sitterdep).Real behavior proof
deviceblocks rancrypto.createPublicKeyplus a v3-then-v2crypto.verifyper request with no per-IP cap. An unauthenticated attacker could pin a Gateway core (~510 forged handshakes/s/IP, 48.7× p50 / 94.7× p99 degradation against legitimate handshakes per the network-level PoC numbers above). The fix layers three defenses: ajv schema caps reject oversizeddevice.publicKeyanddevice.signaturebefore any handler runs;isPlausibleDevicePublicKeyInput/isPlausibleDeviceSignatureInputshape pre-checks reject inputs that cannot decode to a 32-byte raw key or 64-byte signature without invoking crypto; andAUTH_RATE_LIMIT_SCOPE_DEVICE_SIGNATUREgates the verify per IP, so once the bucket is exhausted nocreatePublicKeyorverifyruns.security/preauth-signature-rate-limitHEAD6233fc0e60). No vitest, no mocks. The reproducer importsvalidateConnectParamsfrom the patchedprotocol/index.ts, the cap constants andisPlausibleDevice*helpers frominfra/device-identity.ts, andcreateAuthRateLimiterfromgateway/auth-rate-limit.ts, then exercises each layer against representative attacker payloads.device.publicKey/device.signature(including a 60 KB representative attacker payload), shape pre-checks against malformed inputs that decode to wrong-length raw bytes, and theAUTH_RATE_LIMIT_SCOPE_DEVICE_SIGNATUREbucket fired against a single attacker IP. Run asnode --import tsx <local-copy>.mtsfrom a worktree on this branch.PASSand every cap+1 / 60 KB / wrong-length / wrong-shape attacker input validatedREJECTacross both schema (Layer 1) and shape pre-check (Layer 2). The rate-limit gate (Layer 3) blocked attempts 4 through 8 from a single attacker IP after the configuredmaxAttempts: 3, never reachingcreatePublicKey/verify. Layer 3b measured the per-op cost of the full Ed25519 verify path (0.043 ms/op) versus the gate-blocked path (0.013 ms/op) — a 3.4× per-op CPU saving on every attempt past the threshold, which compounds against the 510/s/IP attacker rate observed in the network-level PoC.server.preauth-signature-rate-limit.test.ts(in-process WS server) and by the livescripts/poc-preauth-flood-ws.mjsPoC against a runningpnpm gateway:watch. Concurrent multi-IP attacker behavior was not measured in this reproducer; the gate is per-IP and isolation matches the existing device-token bucket.Notes for reviewer
triage: dirty-candidatebecause the diff touches schema, device-identity helpers, and the WS message handler. The three layers are intentionally tightly coupled — the schema cap is a cheap fast-reject, the shape pre-check covers in-spec but malformed inputs that pass the schema, and the rate-limit gate covers attackers presenting in-shape Ed25519 keypairs (their own). Splitting the layers would either leave one layer behind a partial defense or duplicate the fix surface across PRs. Happy to split if reviewers prefer one layer per PR.recordFailureis intentionally pessimistic before crypto runs — an attacker with their own keypair producing valid self-signatures should still consume the bucket.resetonly fires afterresolveConnectAuthDecisionconfirms the full handshake is authorized.