Skip to content

fix(gateway): bound connect frame auth.* fields at the protocol schema#77538

Open
fede-kamel wants to merge 3 commits into
openclaw:mainfrom
fede-kamel:security/connect-schema-auth-bounds
Open

fix(gateway): bound connect frame auth.* fields at the protocol schema#77538
fede-kamel wants to merge 3 commits into
openclaw:mainfrom
fede-kamel:security/connect-schema-auth-bounds

Conversation

@fede-kamel

@fede-kamel fede-kamel commented May 4, 2026

Copy link
Copy Markdown
Contributor

Closes #77977

Summary

The connect-frame schema at src/gateway/protocol/schema/frames.ts accepted auth.token, auth.bootstrapToken, auth.deviceToken, and auth.password as unbounded Type.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 to Math.max(provided, expected) before timingSafeEqual and only post-checks lengths. So a remote client could submit a ~60KB auth.bootstrapToken and 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 in device-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:

  • Add HandshakeBootstrapTokenString (256 chars) and HandshakeSharedSecretString (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.
  • Apply the bounded primitives to the four auth.* fields in ConnectParamsSchema: bootstrapToken/deviceToken use the 256-char cap; token/password use the 4096-char cap.

Test plan

  • src/gateway/protocol/connect-auth-bounds.test.ts (6 tests):
    • Accepts token/bootstrapToken/deviceToken/password exactly at the documented caps.
    • Rejects each one byte over the cap.
    • Rejects a 60KB bootstrapToken (representative attacker payload).
  • Regression suite: 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 --check and oxlint pass on touched files.

Notes

  • Caps are well above the legitimate values (32-byte base64url = 43 chars, 64-byte hex signature = 86 chars), so no realistic client should hit them. Operators using auth.password for the legacy shared-secret path get 4096 chars of headroom.
  • This is additive at the schema layer — no behavior change for any in-spec request.

Real behavior proof

  • Behavior addressed: the connect-frame schema accepted auth.token, auth.bootstrapToken, auth.deviceToken, and auth.password as unbounded Type.String(). A remote client could submit a ~60KB value and propagate it to safeEqualSecret, which pads both operands to Math.max(provided, expected) before timingSafeEqual — 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.
  • Real environment tested: local Node v22.22.2 runtime against the real ajv-compiled validateConnectParams and the cap constants from this branch (security/connect-schema-auth-bounds HEAD 0b413080b8). No vitest, no mocks. The reproducer imports validateConnectParams and HANDSHAKE_BOOTSTRAP_TOKEN_MAX_LENGTH / HANDSHAKE_SHARED_SECRET_MAX_LENGTH directly from the patched protocol/index.ts and protocol/schema/primitives.ts.
  • Exact steps or command run after this patch: a node runtime tsx reproducer at https://gist.github.com/fede-kamel/cbba5c994a190be7514997daf04a2d57 enumerates 12 cases — each auth.* field at-cap, one-over-cap, and at 60KB — and times schema validation. Run as node --import tsx <local-copy>.mts from a worktree on this branch.
  • Evidence after fix: live terminal output captured from the node runtime against the patched schema:
Connect-frame auth bounds proof (PR 77538)
Imports real ajv-compiled validateConnectParams + cap constants
from security/connect-schema-auth-bounds worktree.

HANDSHAKE_BOOTSTRAP_TOKEN_MAX_LENGTH = 256
HANDSHAKE_SHARED_SECRET_MAX_LENGTH   = 4096

case                                              | result | took
--------------------------------------------------|--------|------
valid: empty auth                                 | PASS   | 0.928ms
valid: bootstrapToken at cap (256 chars)          | PASS   | 0.038ms
valid: deviceToken at cap (256 chars)             | PASS   | 0.026ms
valid: token at cap (4096 chars)                  | PASS   | 0.318ms
valid: password at cap (4096 chars)               | PASS   | 0.323ms
reject: bootstrapToken cap+1 (257 chars)          | REJECT | 0.062ms
reject: deviceToken cap+1                         | REJECT | 0.352ms
reject: token cap+1 (4097 chars)                  | REJECT | 0.210ms
reject: password cap+1                            | REJECT | 0.029ms
reject: 60KB bootstrapToken (representative attacker payload)| REJECT | 0.189ms
reject: 60KB token                                | REJECT | 0.194ms
reject: 60KB password                             | REJECT | 0.200ms

=== Comparison: schema rejection vs full safeEqualSecret allocation ===

60KB bootstrapToken rejected 591 times in 100.1ms
per-rejection cost: 0.1693ms
  • Observed result after fix: every legitimate at-cap value validated PASS and every cap+1 / 60KB attacker-shaped payload validated REJECT. 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 to safeEqualSecret and run a timingSafeEqual against every stored pairing entry under the bootstrap-state mutex.
  • What was not tested: the actual upgrade from the ws server 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 existing connect-auth-bounds.test.ts unit suite and the broader gateway server auth tests.

Notes for reviewer

  • The latest commit on this branch (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.
  • The cap on bootstrap/device tokens is the load-bearing one for the DoS-amplification surface; the shared-secret cap is generous on purpose because it matches a single operator-configured value and does not iterate.

@fede-kamel
fede-kamel requested a review from a team as a code owner May 4, 2026 21:27
@openclaw-barnacle openclaw-barnacle Bot added app: web-ui App: web-ui gateway Gateway runtime size: S labels May 4, 2026
@clawsweeper

clawsweeper Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed July 6, 2026, 3:27 PM ET / 19:27 UTC.

Summary
Adds bounded gateway connect auth schema primitives for shared secrets, bootstrap/device tokens, and runtime tokens, plus a mint-time cap for agent runtime identity tokens and focused regression tests.

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.

  • Connect Auth Protocol Fields: 6 bounded. Every auth field in the connect frame now has a schema-level shape or length contract, which is the compatibility-sensitive part of the PR.

Stored data model
Persistent data-model change detected: serialized state: src/gateway/agent-runtime-identity-token.ts, unknown-data-model-change: packages/gateway-protocol/src/schema/primitives.ts. Confirm migration or upgrade compatibility proof before merge.

Root-cause cluster
Relationship: fixed_by_candidate
Canonical: #77977
Summary: This PR is the candidate fix for the open connect-frame auth schema bounds issue; related gateway DoS hardening PRs touch adjacent pre-auth paths but do not replace this fix.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge readiness
Overall: 🐚 platinum hermit
Proof: 🦞 diamond lobster
Patch quality: 🐚 platinum hermit
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • [P2] Get gateway/security owner acceptance of the shared-secret and runtime-token protocol caps before merge.

Risk before merge

  • [P1] Merging intentionally rejects operator shared secrets over 4096 code points and machine-token fields outside printable ASCII, so outlier existing gateway token/password deployments could fail at handshake after upgrade.
  • [P1] This changes a pre-auth auth protocol boundary; green CI does not replace gateway/security owner acceptance of the permanent caps.

Maintainer options:

  1. Land After Secops Acceptance (recommended)
    Accept the intentional compatibility tradeoff once gateway/security owners confirm the caps are the desired protocol boundary.
  2. Revise The Shared-Secret Cap
    Change the operator shared-secret cap or add maintainer-requested docs/tests if owners want a broader upgrade envelope.
  3. Pause If Versioning Is Required
    Pause this PR if maintainers decide the auth cap is an incompatible protocol change needing a separate design path.

Next step before merge

  • No ClawSweeper repair is needed; the remaining action is gateway/security owner acceptance of the protocol cap compatibility tradeoff.

Maintainer decision needed

  • Question: Should OpenClaw accept the new connect-frame auth caps, especially the 4096-code-point auth.token/auth.password cap and printable-ASCII machine-token caps, as the permanent gateway protocol contract?
  • Rationale: The code shape is now coherent, but automation cannot decide whether rejecting hypothetical oversized operator secrets is acceptable for existing deployments and security posture.
  • Likely owner: steipete — He merged the adjacent bootstrap-token gateway hardening and has recent ownership signal on local approval auth behavior.
  • Options:
    • Accept Current Caps (recommended): Land the PR as written after owner review, accepting that oversized outlier credentials will be rejected before auth work runs.
    • Adjust Shared-Secret Contract: Raise or document the shared-secret cap before merge if owners want a larger compatibility envelope for operator-configured secrets.
    • Pause For Protocol Design: Hold the PR if owners want versioning, migration guidance, or a different boundary than schema-level rejection.

Security
Cleared: No concrete security or supply-chain regression was found in the diff; the remaining security item is owner acceptance of the auth-boundary caps.

Review details

Best 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 changes

Label changes:

  • add rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • add status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR body and latest comments include copied terminal output from a Node/tsx run against the real compiled validator plus mint-boundary proof for the latest head.
  • remove rating: 🦪 silver shellfish: Current PR rating is rating: 🐚 platinum hermit, so this older rating label is no longer current.
  • remove status: ⏳ waiting on author: Current PR status label is status: 👀 ready for maintainer look.

Label justifications:

  • P2: This is meaningful gateway security hardening with limited blast radius and an existing linked fix PR rather than an emergency outage.
  • merge-risk: 🚨 compatibility: Existing deployments with auth.token/auth.password values over the new cap would be rejected during connect after upgrade.
  • merge-risk: 🚨 auth-provider: The PR changes the gateway auth credential fields accepted during the WebSocket connect handshake.
  • merge-risk: 🚨 security-boundary: The diff changes pre-auth validation for secret-bearing gateway fields before security-sensitive comparison paths run.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR body and latest comments include copied terminal output from a Node/tsx run against the real compiled validator plus mint-boundary proof for the latest head.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body and latest comments include copied terminal output from a Node/tsx run against the real compiled validator plus mint-boundary proof for the latest head.
Evidence reviewed

PR surface:

Source +67, Tests +220. Total +287 across 5 files.

View PR surface stats
Area Files Added Removed Net
Source 3 75 8 +67
Tests 2 220 0 +220
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 5 295 8 +287

What I checked:

Likely related people:

What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

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
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.
Review history (5 earlier review cycles)
  • reviewed 2026-07-03T21:06:36.321Z sha 108997f :: found issues before merge. :: [P1] Bound auth payload bytes, not grapheme count | [P1] Document or narrow the new connect-auth caps | [P2] Cover the current auth field when rebasing
  • reviewed 2026-07-06T03:01:27.481Z sha bad93ed :: found issues before merge. :: [P1] Enforce auth caps in bytes or token shape | [P2] Cover runtime-token fields before capping them
  • reviewed 2026-07-06T03:20:06.316Z sha bad93ed :: found issues before merge. :: [P1] Enforce auth caps in bytes or token shape | [P2] Cover runtime-token fields before capping them
  • reviewed 2026-07-06T03:28:58.568Z sha bad93ed :: found issues before merge. :: [P1] Enforce auth caps in bytes or token shape | [P2] Cover runtime-token fields before capping them
  • reviewed 2026-07-06T16:28:52.991Z sha 1e63b37 :: found issues before merge. :: [P1] Size runtime-token cap from the minted payload

@fede-kamel

Copy link
Copy Markdown
Contributor Author

Good catch — addressed in 127a2855d9.

The 256 cap is right for bootstrapToken/deviceToken (protocol-issued, iterated under the bootstrap-state mutex against every stored entry — that's where the per-entry safeEqualSecret amplification lives), but it was wrong for auth.token/auth.password, which run a single compare against one resolved operator-configured value.

Split into two primitives: HandshakeBootstrapTokenString (256) for the iterated machine-token paths, and HandshakeSharedSecretString (4096) for the operator shared-secret paths. 4096 is well above any reasonable operator config (real tokens are 43–64 chars; long random/passphrase configs all fit) and still bounds pre-auth allocation/scan work to a sane value.

Tests updated to assert each field at its own cap. 6/6 pass; 54/54 on the regression suite (connect-auth-bounds, handshake-auth-helpers, server.auth.modes, server.auth.browser-hardening).

@fede-kamel

Copy link
Copy Markdown
Contributor Author

@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!

@fede-kamel
fede-kamel force-pushed the security/connect-schema-auth-bounds branch from 127a285 to c600814 Compare May 5, 2026 16:32
@openclaw-barnacle openclaw-barnacle Bot added the triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. label May 5, 2026
@fede-kamel

Copy link
Copy Markdown
Contributor Author

cc @openclaw/openclaw-secops for visibility — rebased onto main, mergeable. No rush. Thanks!

fede-kamel added a commit to fede-kamel/openclaw that referenced this pull request May 6, 2026
…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.
@fede-kamel
fede-kamel force-pushed the security/connect-schema-auth-bounds branch from c600814 to 0b41308 Compare May 6, 2026 22:05
@fede-kamel

Copy link
Copy Markdown
Contributor Author

Rebased onto current origin/main (was 372 commits behind). The previous "Real behavior proof" CI failure was a missing-script artifact — scripts/github/real-behavior-proof-check.mjs was added in #77622 after this branch was first pushed, so the workflow could not load on the old base. The script is on HEAD now and CI is re-running.

@openclaw/openclaw-secops — flagging for review (connect-frame auth.* schema bounds).

@openclaw-barnacle openclaw-barnacle Bot added proof: supplied External PR includes structured after-fix real behavior proof. and removed triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. labels May 9, 2026
@clawsweeper clawsweeper Bot added the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 9, 2026
@openclaw-barnacle openclaw-barnacle Bot added scripts Repository scripts size: M and removed size: S labels May 9, 2026
@fede-kamel
fede-kamel force-pushed the security/connect-schema-auth-bounds branch from 1f4a6b2 to 0b41308 Compare May 9, 2026 17:58
@openclaw-barnacle openclaw-barnacle Bot added size: S and removed scripts Repository scripts size: M proof: sufficient ClawSweeper judged the real behavior proof convincing. labels May 9, 2026
@clawsweeper clawsweeper Bot added the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 9, 2026
fede-kamel added a commit to fede-kamel/openclaw that referenced this pull request May 9, 2026
…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.
@clawsweeper clawsweeper Bot added status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. and removed status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. labels Jun 14, 2026
@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. merge-risk: 🚨 availability 🚨 May cause crashes, hangs, restart loops, stalls, or process outages. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. and removed rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. merge-risk: 🚨 availability 🚨 May cause crashes, hangs, restart loops, stalls, or process outages. labels Jun 23, 2026
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.
@fede-kamel

Copy link
Copy Markdown
Contributor Author

Both review findings addressed in 1e63b37.

1. Grapheme-count maxLength → byte-true bounds. Confirmed the finding — and it is worse than a constant factor: TypeBox 1.x IsMaxLength counts grapheme clusters (guard/string.mjs walks NextGraphemeClusterIndex), and one base char + N combining marks is a single grapheme, so a ~120KB-byte value passed every previous cap. Fix, per field class:

  • Machine-minted tokens (bootstrapToken, deviceToken, and now the runtime tokens) get a printable-ASCII shape (^[\x21-\x7E]*$). All are minted from base64url/hex/UUID alphabets (src/infra/pairing-token.ts, src/gateway/operator-approval-runtime-token.ts, src/gateway/agent-runtime-identity-token.ts), so this has zero compatibility risk and makes graphemes == code points == bytes: the caps are now exact byte bounds.
  • Operator shared secrets (token, password) must keep accepting Unicode passphrases, so no charset restriction; instead the pattern quantifier ^[\s\S]{0,4096}$ bounds code points (TypeBox compiles patterns with the u flag — schema/engine/pattern.mjs), capping the value at ≤4 UTF-8 bytes per point. The single per-connect safeEqualSecret comparison is now hard byte-bounded; the per-entry amplification surface (bootstrap/device scan) has exact ASCII byte caps.

2. approvalRuntimeToken / agentRuntimeIdentityToken coverage. Both were also moved off the shared-secret primitive onto a right-sized one: approvalRuntimeToken mints as a 43-char base64url HMAC, and agentRuntimeIdentityToken as <base64url JSON payload>.<base64url HMAC> whose payload carries agentId + sessionKey — so 256 would be too tight and 4096 needlessly loose; they now cap at 2048 with the ASCII shape. Direct validator tests added for both (minted-shape accept, at-cap accept, over-cap reject, non-ASCII reject).

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: node scripts/run-vitest.mjs packages/gateway-protocol/src/connect-auth-bounds.test.ts → 13/13 passed; oxfmt --check clean on touched files.

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.
@fede-kamel

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

The July 6 finding is addressed in 14defa0: mintAgentRuntimeIdentityToken now enforces HANDSHAKE_RUNTIME_TOKEN_MAX_LENGTH at the mint boundary, failing fast with a descriptive error instead of letting an oversized agentId/sessionKey mint a token the gateway would silently reject at connect. Regression tests mint from the longest chat-send-supported session key (CHAT_SEND_SESSION_KEY_MAX_LENGTH = 512 → ~900-char token, well under the 2048 cap), prove it verifies and passes validateConnectParams, and cover the mint-time failure path. A token that mints can now never be rejected by the schema cap.

@openclaw/openclaw-secops — one acceptance item the review correctly routes to you: the 4096-code-point cap on operator shared secrets (auth.token/auth.password) intentionally rejects hypothetical >4096-char configured values. There is no known legitimate config anywhere near that size, but it is a protocol contract call. Details and the byte-bounding rationale are in the PR body and the two comments above.

@clawsweeper

clawsweeper Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@openclaw-barnacle

Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app: web-ui App: web-ui gateway Gateway runtime merge-risk: 🚨 auth-provider 🚨 May break OAuth, tokens, provider routing, model choice, or credentials. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. proof: supplied External PR includes structured after-fix real behavior proof. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. size: M stale Marked as stale due to inactivity status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: gateway connect-frame auth.* fields are not bounded at the protocol schema

1 participant