Root cause
Authentication & access control introduced
fix: allow token auth to bypass device identity requirement
How AI contributed: Direct introductionPublished Mar 3, 2026 in
openclaw/openclawAll 35 findings in this repository
Summary A client authenticated with a shared gateway token could connect as role=node without device identity/pairing, then call node.event to trigger agent.request and voice.transcript flows. Affected Packages / Versions · Package: npm openclaw · Affected versions: <= 2026.2.21-2 · Patched version: 2026.2.22 (planned next release) Details The WebSocket connect path allowed device-less bypass whenever shared auth succeeded. That bypass did not restrict role, so a client could claim role=node with no device identity and still pass handshake auth. Because node.event is node-role allowed, this enabled unauthorized node event injection into agent-trigger flows. Impact Unauthorized node.event injection can trigger agent execution and voice transcript flows for clients that only hold the shared gateway token, without node device pairing. Remediation Upgrade to 2026.2.22 (or newer) once published. The fix requires device identity for role=node connects, even when shared-token auth succeeds. Fix Commit(s) · ddcb2d79b17bf2a42c5037d8aeff1537a12b931e Release Process Note patched_versions is pre-set to the planned next release so once npm release 2026.2.22 is out, advisory publish is a single step. OpenClaw thanks @tdjackey for reporting.
Token-auth skip of device identity has no role gate, so role=node can connect with device:null; invariant: node must not skip device identity.
Root cause
fix: allow token auth to bypass device identity requirement
Fix
fix(gateway): block node role when device identity is missing
Fix by Peter Steinberger · no AI marker found
Case facts
@@ -254,7 +254,9 @@ export function attachGatewayWsMessageHandler(params: { const device = connectParams.device; let devicePublicKey: string | null = null;- if (!device) {+ // Allow token-authenticated connections (e.g., control-ui) to skip device identity+ const hasTokenAuth = !!connectParams.auth?.token;+ if (!device && !hasTokenAuth) { setHandshakeState("failed"); setCloseCause("device-required", { client: connectParams.client.id,@@ -427,7 +429,7 @@ export function attachGatewayWsMessageHandler(params: { }); let authOk = authResult.ok; let authMethod = authResult.method ?? "none";- if (!authOk && connectParams.auth?.token) {+ if (!authOk && connectParams.auth?.token && device) { const tokenCheck = await verifyDeviceToken({ deviceId: device.id, token: connectParams.auth.token,@@ -363,6 +363,28 @@ describe("gateway server auth/connect", () => { await expectMissingScopeAfterConnect(port, { device: null }); }); + test("rejects node role when device identity is omitted", async () => {+ const ws = await openWs(port);+ const token = resolveGatewayTokenOrEnv();+ try {+ const res = await connectReq(ws, {+ role: "node",+ token,+ device: null,+ client: {+ id: GATEWAY_CLIENT_NAMES.NODE_HOST,+ version: "1.0.0",+ platform: "test",+ mode: GATEWAY_CLIENT_MODES.NODE,+ },+ });+ expect(res.ok).toBe(false);+ expect(res.error?.message ?? "").toContain("device identity required");+ } finally {+ ws.close();+ }+ });+ test("allows health when scopes are empty", async () => { const ws = await openWs(port); try {@@ -125,6 +125,7 @@ Docs: https://docs.openclaw.ai - Gateway/Security: remove shared-IP fallback for canvas endpoints and require token or session capability for canvas access. Thanks @thewilloftheshadow. - Gateway/Security: require secure context and paired-device checks for Control UI auth even when `gateway.controlUi.allowInsecureAuth` is set, and align audit messaging with the hardened behavior. (#20684) Thanks @coygeek and @Vasco0x4 for reporting. - Gateway/Security: scope tokenless Tailscale forwarded-header auth to Control UI websocket auth only, so HTTP gateway routes still require token/password even on trusted hosts. Thanks @zpbrent for reporting.+- Gateway/Security: require device identity for `role: node` websocket connections even when shared-token auth succeeds, preventing unpaired device-less clients from invoking `node.event`. Thanks @tdjackey for reporting. - Docker/Security: run E2E and install-sh test images as non-root by adding appuser directives. Thanks @thewilloftheshadow. - Skills/Security: sanitize skill env overrides to block unsafe runtime injection variables and only allow sensitive keys when declared in skill metadata, with warnings for suspicious values. Thanks @thewilloftheshadow. - Security/Commands: block prototype-key injection in runtime `/debug` overrides and require own-property checks for gated command flags (`bash`, `config`, `debug`) so inherited prototype values cannot enable privileged commands. Thanks @tdjackey for reporting.@@ -490,7 +490,7 @@ export function attachGatewayWsMessageHandler(params: { return true; } clearUnboundScopes();- const canSkipDevice = sharedAuthOk;+ const canSkipDevice = role === "operator" && sharedAuthOk; if (isControlUi && !controlUiAuthPolicy.allowBypass) { const errorMessage =AI-assisted change b94c5c5ced25b9bce426afccf67da47d98569efd9eb060e7500c7277aaa1278b · Fix 2d2b85887dc036ee93d7ea74aae6782f4853e6afa7a70fcfb53583808d23ccf5