fix: Hook ingress token unlocks password-mode gateway auth

This commit is contained in:
clawsweeper
2026-05-25 12:21:36 +00:00
parent b2d39a5a01
commit 7c796b22ec
4 changed files with 27 additions and 37 deletions
+1
View File
@@ -43,6 +43,7 @@ For webhook ingress, it warns when:
- overrides are enabled without `hooks.allowedSessionKeyPrefixes`
If Gateway password auth is supplied only at startup, pass the same value to `openclaw security audit --auth password --password <password>` so the audit can check it against `hooks.token`.
Password-mode reuse is an audit finding for compatibility; rotate one of the secrets instead of expecting Gateway startup to reject that configuration.
It also warns when sandbox Docker settings are configured while sandbox mode is off, when `gateway.nodes.denyCommands` uses ineffective pattern-like/unknown entries (exact node command-name matching only, not shell-text filtering), when `gateway.nodes.allowCommands` explicitly enables dangerous node commands, when global `tools.profile="minimal"` is overridden by agent tool profiles, when write/edit tools are disabled but `exec` is still available without a constraining sandbox filesystem boundary, when open groups expose runtime/filesystem tools without sandbox/workspace guards, and when installed plugin tools may be reachable under permissive tool policy.
It also flags `gateway.allowRealIpFallback=true` (header-spoofing risk if proxies are misconfigured) and `discovery.mdns.mode="full"` (metadata leakage via mDNS TXT records).
+1 -1
View File
@@ -688,7 +688,7 @@ Validation and safety notes:
- `hooks.enabled=true` requires a non-empty `hooks.token`.
- `hooks.token` must be distinct from `gateway.auth.token` / `OPENCLAW_GATEWAY_TOKEN`; reusing the Gateway token fails startup validation.
- `openclaw security audit` also flags `hooks.token` reuse of active Gateway password auth (`gateway.auth.password` / `OPENCLAW_GATEWAY_PASSWORD`, or `--auth password --password <password>`) as a critical finding.
- `openclaw security audit` also flags `hooks.token` reuse of active Gateway password auth (`gateway.auth.password` / `OPENCLAW_GATEWAY_PASSWORD`, or `--auth password --password <password>`) as a critical finding; password-mode reuse stays startup-compatible and should be repaired by rotating one of the secrets.
- `hooks.path` cannot be `/`; use a dedicated subpath such as `/hooks`.
- If `hooks.allowRequestSessionKey=true`, constrain `hooks.allowedSessionKeyPrefixes` (for example `["hook:"]`).
- If a mapping or preset uses a templated `sessionKey`, set `hooks.allowedSessionKeyPrefixes` and `hooks.allowRequestSessionKey=true`. Static mapping keys do not require that opt-in.
-32
View File
@@ -1,32 +0,0 @@
import { normalizeOptionalString } from "../shared/string-coerce.js";
import type { ResolvedGatewayAuth } from "./auth.js";
export type GatewayAuthSharedSecretLabel = "gateway auth token" | "gateway auth password";
type ActiveGatewaySharedSecret = {
label: GatewayAuthSharedSecretLabel;
value?: string;
};
function listActiveGatewaySharedSecrets(auth: ResolvedGatewayAuth): ActiveGatewaySharedSecret[] {
if (auth.mode === "token") {
return [{ label: "gateway auth token", value: auth.token }];
}
if (auth.mode === "password" || auth.mode === "trusted-proxy") {
return [{ label: "gateway auth password", value: auth.password }];
}
return [];
}
export function findGatewayAuthLabelMatchingHooksToken(params: {
hooksToken?: string;
auth: ResolvedGatewayAuth;
}): GatewayAuthSharedSecretLabel | undefined {
const hooksToken = normalizeOptionalString(params.hooksToken);
if (!hooksToken) {
return undefined;
}
return listActiveGatewaySharedSecrets(params.auth).find(
(candidate) => normalizeOptionalString(candidate.value) === hooksToken,
)?.label;
}
+25 -4
View File
@@ -11,10 +11,6 @@ import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { AgentToolsConfig } from "../config/types.tools.js";
import { resolveGatewayAuth, type ResolvedGatewayAuth } from "../gateway/auth.js";
import { resolveAllowedAgentIds } from "../gateway/hooks-policy.js";
import {
findGatewayAuthLabelMatchingHooksToken,
type GatewayAuthSharedSecretLabel,
} from "../gateway/hooks-token-auth-reuse.js";
import {
DEFAULT_DANGEROUS_NODE_COMMANDS,
listDangerousPluginNodeCommands,
@@ -50,6 +46,12 @@ export type GatewayHttpNoAuthAuditOptions = {
gatewayAuthOverride?: Pick<GatewayAuthConfig, "mode" | "token" | "password">;
};
type GatewayAuthSharedSecretLabel = "gateway auth token" | "gateway auth password";
type ActiveGatewaySharedSecret = {
label: GatewayAuthSharedSecretLabel;
value?: string;
};
// --------------------------------------------------------------------------
// Helpers
// --------------------------------------------------------------------------
@@ -93,6 +95,25 @@ function formatHooksTokenReuseDetail(reusedGatewayAuthLabel: GatewayAuthSharedSe
return "hooks.token matches gateway.auth token; compromise of hooks expands blast radius to the Gateway API.";
}
function listActiveGatewaySharedSecrets(auth: ResolvedGatewayAuth): ActiveGatewaySharedSecret[] {
if (auth.mode === "token") {
return [{ label: "gateway auth token", value: auth.token }];
}
if (auth.mode === "password" || auth.mode === "trusted-proxy") {
return [{ label: "gateway auth password", value: auth.password }];
}
return [];
}
function findGatewayAuthLabelMatchingHooksToken(params: {
hooksToken: string;
auth: ResolvedGatewayAuth;
}): GatewayAuthSharedSecretLabel | undefined {
return listActiveGatewaySharedSecrets(params.auth).find(
(candidate) => normalizeOptionalString(candidate.value) === params.hooksToken,
)?.label;
}
function findHooksTokenGatewayAuthReuseLabel(params: {
hooksToken: string;
configGatewayAuth: ResolvedGatewayAuth;