Conversation
- resolveAuthProfile(): match both '.clawdbot' and 'clawdbot' suffixes to handle OPENCLAW_HOME=/opt/clawdbot (no dot prefix) - setNestedValue(): reject __proto__/prototype/constructor keys (prototype pollution guard) - Add canonicalization matrix with 6 payload variants for WS auth debugging - Add per-field hash logging and raw challenge capture behind OPENCLAW_WS_DEBUG=1 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
After reading the actual server-side verifier (openclaw/openclaw src/gateway/device-auth.ts, src/infra/device-identity.ts), confirmed: - Server accepts both PEM and raw-base64url public keys - Server decodes signatures in both base64url and standard base64 - Payload format (v3|deviceId|...) matches our v3-default-ms exactly Changes: - clawdbot-v1 profile now uses raw-base64url + base64url (matches server's own signDevicePayload output) instead of PEM + base64 - Add self-verification diagnostic: verify signature locally before sending, check deviceId derivation, verify encode/decode round-trip - Helps identify if the issue is key mismatch vs payload mismatch Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Clawdbot marketplace image may run an older gateway that only supports v2 device auth payloads (no platform/deviceFamily fields). The current server tries v3→v2 fallback, but older versions only have v2. Changes: - clawdbot-v1 profile now uses v2 payload as primary - Added v2 variants to canonicalization matrix (v2-default-ms, v2-default-sec, v2-no-token-ms) - Default profile still uses v3 (unchanged for standard OpenClaw) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add safe v3↔v2 auth payload fallback: on signature rejection, retry once with alternate payload version before giving up - Clean up diagnostic logging: consolidate to single production line, gate verbose output behind OPENCLAW_WS_DEBUG=1 - Add auth reject/fallback counters for observability - Add fallback conformance tests (signature reject → retry → success, and double-reject → fail after one fallback) - Add WS auth version-compat matrix to SKILL.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move clearConnectTimeout() from the unconditional connect-response path into each terminal branch (success, pairing rejection, final rejection). The fallback branch intentionally keeps the original 30s timeout alive so a hanging fallback connection still triggers the timeout instead of leaving the connect promise dangling forever. Fixes Devin review finding on PR #493. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Guard close/error handlers with `this.ws !== ws` to ignore events from superseded WebSocket instances. This fixes two Devin review findings: 1. Error handler could reject connect promise during fallback if the old WS emitted an error during close handshake. 2. Old WS close handler could stomp `authenticated = false` on the new connection if it fired after the fallback succeeded, plus trigger a spurious scheduleReconnect(). The `this.ws !== ws` pattern replaces the `fallbackInProgress` flag with a simpler, more robust guard. Also adds early `stopped` check in sendChatMessage to prevent reconnect after explicit disconnect. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| @@ -423,7 +566,10 @@ export class OpenClawGatewayClient { | |||
Check warning
Code scanning / CodeQL
Log injection Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 2 months ago
In general, to fix log injection you should sanitize any untrusted input before interpolating it into log messages. For plain-text logs, a common minimal approach is to strip carriage return and newline characters so an attacker cannot break out onto new log lines, and optionally remove other control characters. It is also useful to clearly delimit or quote user-provided values in logs.
For this specific issue, the best minimally invasive fix is to sanitize requestId right before it is used in the log statement. Since we only see the problematic log at line 688 and the value is only used for display to the user, we can create a sanitized version (e.g., safeRequestId) by converting requestId to a string and removing \r and \n. Then we use safeRequestId in the console.error message. This preserves existing functionality (the user still sees the same identifier for normal values) while preventing forged log lines from maliciously crafted requestId values. No new imports or external libraries are needed; we can rely on String() and .replace with a suitable regular expression.
Concretely:
- In
packages/openclaw/src/gateway.ts, inside theif (requestId) { ... }block near line 688, introduce aconst safeRequestId = String(requestId).replace(/[\r\n]/g, '');. - Replace the existing
console.errorline that logsrequestIdwith one that logssafeRequestIdinstead. - Optionally, this pattern can be reused elsewhere if you later decide to sanitize other logged values, but for this fix we constrain changes only to the provided snippet.
| @@ -685,12 +685,13 @@ | ||
| const requestId = errObj?.requestId ?? errObj?.request_id ?? ''; | ||
| console.error('[openclaw-ws] Pairing rejected — device is not paired with the OpenClaw gateway.'); | ||
| if (requestId) { | ||
| console.error(`[openclaw-ws] Approve this device: openclaw devices approve ${requestId}`); | ||
| const safeRequestId = String(requestId).replace(/[\r\n]/g, ''); | ||
| console.error(`[openclaw-ws] Approve this device: openclaw devices approve ${safeRequestId}`); | ||
| } | ||
| console.error(`[openclaw-ws] Device ID: ${this.device.deviceId.slice(0, 16)}...`); | ||
| const configHint = getWsAuthCompat() === 'clawdbot' | ||
| ? '~/.clawdbot/clawdbot.json' | ||
| : '~/.openclaw/openclaw.json'; | ||
| : '~/.openclaw/openclaw/openclaw.json'; | ||
| console.error(`[openclaw-ws] Ensure OPENCLAW_GATEWAY_TOKEN matches ${configHint} gateway.auth.token`); | ||
| this.pairingRejected = true; | ||
| } else if (isSignatureInvalid && !this.fallbackAttempted) { |
There was a problem hiding this comment.
🟡 Fallback state (payloadVersionOverride, fallbackAttempted) never resets during auto-reconnect, locking retries to one payload version
When both v3 and v2 payload versions fail authentication, the connect() promise is rejected but the WebSocket close handler still calls scheduleReconnect() (gateway.ts:564-565). scheduleReconnect invokes doConnect() directly (gateway.ts:812,826) which does NOT reset payloadVersionOverride or fallbackAttempted — those are only reset in connect() (gateway.ts:474-476). This means all subsequent auto-reconnect attempts are locked to the last-tried fallback payload version and never cycle back to the original. If the first version was rejected due to a transient server issue (and the isSignatureInvalid regex at line 680 is fairly broad — device.signature matches any error containing "device signature" even without "invalid"), the client would be permanently stuck retrying only the wrong payload version until an explicit connect() call is made.
(Refers to lines 809-813)
Prompt for agents
In packages/openclaw/src/gateway.ts, the scheduleReconnect() method (around lines 797-828) calls doConnect() directly, which does not reset the fallback state (payloadVersionOverride, fallbackAttempted). After both v3 and v2 fail, auto-reconnects are stuck on one version. Consider resetting these fields before calling doConnect() in scheduleReconnect(), similar to how connect() resets them at lines 474-476. For example, in the two setTimeout callbacks that call this.doConnect() (around lines 811-812 and 825-826), add:
this.payloadVersionOverride = null;
this.fallbackAttempted = false;
This way each auto-reconnect cycle gets a fresh chance to try the primary version first and fall back if needed.
Was this helpful? React with 👍 or 👎 to provide feedback.
| ws.on('message', (data) => { | ||
| this.handleMessage(data.toString()); | ||
| }); |
There was a problem hiding this comment.
🟡 Missing this.ws !== ws guard on WebSocket message handler allows stale messages during v3↔v2 fallback
The close and error handlers in doConnect() were given if (this.ws !== ws) return; guards (lines 535, 571) to ignore events from superseded WebSocket instances during v3↔v2 fallback. However, the message handler at line 528 was not given the same guard. During the fallback flow, this.ws is set to null and the old WebSocket is closed (gateway.ts:713-714), but if a buffered message is delivered on the old socket before the close completes, handleMessage would execute against the current (potentially new) connection state. For example, a stale connect.challenge from the old socket could cause a duplicate connect request to be sent on the newly created socket.
Inconsistent guard pattern in doConnect()
The close handler at line 532-567 and error handler at line 569-581 both check if (this.ws !== ws) return; but the message handler at line 528-530 does not:
ws.on('message', (data) => {
this.handleMessage(data.toString()); // no guard
});| ws.on('message', (data) => { | |
| this.handleMessage(data.toString()); | |
| }); | |
| ws.on('message', (data) => { | |
| // Guard: ignore messages from superseded WebSocket instances. | |
| if (this.ws !== ws) return; | |
| this.handleMessage(data.toString()); | |
| }); |
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
Test Plan
Screenshots