fix(gateway): bound connect frame auth.* fields at the protocol schema#77538
fix(gateway): bound connect frame auth.* fields at the protocol schema#77538fede-kamel wants to merge 3 commits into
Conversation
|
Codex review: needs maintainer review before merge. Reviewed July 6, 2026, 3:27 PM ET / 19:27 UTC. Summary PR surface: Source +67, Tests +220. Total +287 across 5 files. Reproducibility: yes. from source inspection: current main lets unbounded auth.* strings through validateConnectParams, and safeEqualSecret pads comparisons to the larger UTF-8 byte length. I did not run a live current-main flood, so this is source-reproducible rather than locally reproduced. Review metrics: 1 noteworthy metric.
Stored data model Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Maintainer decision needed
Security Review detailsBest possible solution: Land the schema-boundary hardening after gateway/security owners accept the new auth caps as the protocol contract, keeping the mint-boundary check and regression coverage in the same change. Do we have a high-confidence way to reproduce the issue? Yes, from source inspection: current main lets unbounded auth.* strings through validateConnectParams, and safeEqualSecret pads comparisons to the larger UTF-8 byte length. I did not run a live current-main flood, so this is source-reproducible rather than locally reproduced. Is this the best way to solve the issue? Yes, schema validation is the best layer for rejecting oversized pre-auth wire payloads before handshake and secret-compare work. A lower-level safeEqualSecret-only change would leave malformed protocol payloads accepted deeper into the auth pipeline and would need separate per-caller contracts. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against e938461aa2ea. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +67, Tests +220. Total +287 across 5 files. View PR surface stats
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
Review history (5 earlier review cycles)
|
|
Good catch — addressed in 127a2855d9. The 256 cap is right for Split into two primitives: Tests updated to assert each field at its own cap. 6/6 pass; 54/54 on the regression suite ( |
|
@steipete when you have a moment — bounds connect-frame auth.* fields at the protocol schema (security), CI green. Will rebase to clear conflicts before merge. Thanks! |
127a285 to
c600814
Compare
|
cc @openclaw/openclaw-secops for visibility — rebased onto main, mergeable. No rush. Thanks! |
…ecrets Codex review (PR openclaw#77538) flagged that the original 256-char cap on `auth.token` was tighter than the operator-configured shared-secret credential contract. `gateway.auth.token` and `gateway.auth.password` remain unconstrained `SecretInput`, so an existing gateway with a 257+ char shared token would have started rejecting every WS connect as invalid params before auth comparison. Split the wire caps to match the actual amplification surface: - `auth.bootstrapToken` and `auth.deviceToken` keep the 256-char bound (`HandshakeBootstrapTokenString`). These are protocol-issued 32-byte tokens and the verifier iterates them under the bootstrap-state mutex against every stored pairing/device entry, so unbounded provided lengths amplify per-entry `safeEqualSecret` work. - `auth.token` and `auth.password` move to a generous 4096-char bound (`HandshakeSharedSecretString`). These are matched once per connect against a single resolved configured value, so the per-entry amplification risk does not apply; the cap is defense-in-depth that preserves any reasonable operator config.
c600814 to
0b41308
Compare
|
Rebased onto current @openclaw/openclaw-secops — flagging for review (connect-frame auth.* schema bounds). |
1f4a6b2 to
0b41308
Compare
…ecrets Codex review (PR openclaw#77538) flagged that the original 256-char cap on `auth.token` was tighter than the operator-configured shared-secret credential contract. `gateway.auth.token` and `gateway.auth.password` remain unconstrained `SecretInput`, so an existing gateway with a 257+ char shared token would have started rejecting every WS connect as invalid params before auth comparison. Split the wire caps to match the actual amplification surface: - `auth.bootstrapToken` and `auth.deviceToken` keep the 256-char bound (`HandshakeBootstrapTokenString`). These are protocol-issued 32-byte tokens and the verifier iterates them under the bootstrap-state mutex against every stored pairing/device entry, so unbounded provided lengths amplify per-entry `safeEqualSecret` work. - `auth.token` and `auth.password` move to a generous 4096-char bound (`HandshakeSharedSecretString`). These are matched once per connect against a single resolved configured value, so the per-entry amplification risk does not apply; the cap is defense-in-depth that preserves any reasonable operator config.
auth.token/bootstrapToken/deviceToken/password/approvalRuntimeToken were unbounded Type.String() in the connect frame, so a pre-auth client could send up to the 64KB frame cap. safeEqualSecret pads both operands to the longer length before timingSafeEqual, and bootstrap/device tokens are scanned against every stored entry under the bootstrap mutex, amplifying an oversized value per entry. Split the caps: 256 chars for machine bootstrap/device tokens (well above any real value), 4096 for operator-configured shared secrets to preserve compatibility with long configured tokens/passphrases.
TypeBox maxLength counts grapheme clusters, so one base char plus N combining marks passed every cap as a single grapheme while carrying unbounded bytes into safeEqualSecret padding. Enforce printable-ASCII token shape on machine-minted fields (bootstrapToken/deviceToken at 256, new 2048-cap runtime-token primitive for approvalRuntimeToken and agentRuntimeIdentityToken sized for the base64url payload.signature shape), and bound operator shared secrets by code points via a u-flag pattern quantifier so Unicode passphrases keep working with <=4 bytes per point. Adds direct coverage for both runtime-token fields and a combining-mark regression test across all six auth fields.
|
Both review findings addressed in 1e63b37. 1. Grapheme-count
2. New regression coverage: the single-grapheme combining-mark payload is asserted rejected on all six auth fields, plus emoji-at-cap/over-cap boundary tests proving code-point (not grapheme) bounding, plus a Unicode-passphrase accept test. Verification: |
The 2048-char schema cap on auth.agentRuntimeIdentityToken embeds a caller sessionKey that nothing bounded before minting, so a degenerate key could mint a token the gateway would silently reject at connect. mintAgentRuntimeIdentityToken now fails fast with a descriptive error when the serialized token exceeds HANDSHAKE_RUNTIME_TOKEN_MAX_LENGTH, where the oversized agentId/sessionKey is actionable. Regression tests mint from the longest chat-send-supported session key (512 chars, ~900-char token) and prove it verifies and passes validateConnectParams, plus the mint-time failure path.
|
@clawsweeper re-review The July 6 finding is addressed in 14defa0: @openclaw/openclaw-secops — one acceptance item the review correctly routes to you: the 4096-code-point cap on operator shared secrets ( |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
This pull request has been automatically marked as stale due to inactivity. |
Closes #77977
Summary
The connect-frame schema at
src/gateway/protocol/schema/frames.tsacceptedauth.token,auth.bootstrapToken,auth.deviceToken, andauth.passwordas unboundedType.String(). The only effective cap was the 64KB pre-auth frame limit (MAX_PREAUTH_PAYLOAD_BYTES).safeEqualSecret(src/security/secret-equal.ts) pads both operands toMath.max(provided, expected)beforetimingSafeEqualand only post-checks lengths. So a remote client could submit a ~60KBauth.bootstrapTokenand force every stored-pairing/bootstrap comparison on the pre-auth path to allocate and constant-time-scan the inflated payload. Combined with the mutex-serialized pairing-state walk indevice-bootstrap.ts, oversized tokens amplify per-comparison work for legitimate node onboarding.This PR rejects oversized values at schema validation, before any handshake or secret-compare work runs:
HandshakeBootstrapTokenString(256 chars) andHandshakeSharedSecretString(4096 chars) primitives. Real shared-secret/bootstrap/device tokens are 32-byte base64url (43 chars) or hex (64 chars), so 256 is well above any legitimate value while bounding the work an attacker can force on pre-auth code paths. The shared-secret cap is generous to preserve compatibility with arbitrarily-sized operator configs.auth.*fields inConnectParamsSchema:bootstrapToken/deviceTokenuse the 256-char cap;token/passworduse the 4096-char cap.Test plan
src/gateway/protocol/connect-auth-bounds.test.ts(6 tests):token/bootstrapToken/deviceToken/passwordexactly at the documented caps.bootstrapToken(representative attacker payload).pnpm test src/gateway/protocol/connect-auth-bounds.test.ts src/gateway/server/ws-connection/handshake-auth-helpers.test.ts src/gateway/server.auth.modes.test.ts src/gateway/server.auth.browser-hardening.test.ts→ 54/54 pass.pnpm exec oxfmt --checkandoxlintpass on touched files.Notes
auth.passwordfor the legacy shared-secret path get 4096 chars of headroom.Real behavior proof
auth.token,auth.bootstrapToken,auth.deviceToken, andauth.passwordas unboundedType.String(). A remote client could submit a ~60KB value and propagate it tosafeEqualSecret, which pads both operands toMath.max(provided, expected)beforetimingSafeEqual— amplifying per-comparison work on the mutex-serialised bootstrap-pairing walk. With this PR, the values are bounded at the schema layer (256 chars for bootstrap/device tokens, 4096 chars for shared secrets/passwords), and the ajv-compiled validator rejects oversized payloads before any handshake or secret-compare work runs.validateConnectParamsand the cap constants from this branch (security/connect-schema-auth-boundsHEAD0b413080b8). No vitest, no mocks. The reproducer importsvalidateConnectParamsandHANDSHAKE_BOOTSTRAP_TOKEN_MAX_LENGTH/HANDSHAKE_SHARED_SECRET_MAX_LENGTHdirectly from the patchedprotocol/index.tsandprotocol/schema/primitives.ts.auth.*field at-cap, one-over-cap, and at 60KB — and times schema validation. Run asnode --import tsx <local-copy>.mtsfrom a worktree on this branch.PASSand every cap+1 / 60KB attacker-shaped payload validatedREJECT. No case produced an unexpected outcome. Sustained 60KB-rejection throughput was ~5,900 validations/sec (~0.17ms each), which is the upper bound on the cost an attacker can force at the schema layer — versus the pre-patch behavior where the 60KB payload would propagate tosafeEqualSecretand run atimingSafeEqualagainst every stored pairing entry under the bootstrap-state mutex.wsserver through the protocol decoder is not driven by this reproducer; it exercises only the ajv-compiled validator that sits between the decoded JSON and the handshake handler. The integration shape is covered by the existingconnect-auth-bounds.test.tsunit suite and the broader gateway server auth tests.Notes for reviewer
fix(gateway): split connect-frame auth caps for shared vs bootstrap secrets) splits the cap into 256 (bootstrap/device) and 4096 (shared-secret/password) — different from the original 256/1024 in this PR's earlier description. The reproducer reads the live constants so the captured numbers track whatever is on the branch at the time.