fix(gateway): block node role when device identity is missing

This commit is contained in:
Peter Steinberger
2026-02-21 19:34:00 +01:00
parent 764b1f2932
commit ddcb2d79b1
3 changed files with 24 additions and 1 deletions

View File

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

View File

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

View File

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