Security: expand audit checks for mDNS and real-IP fallback

This commit is contained in:
Brian Mendonca
2026-02-22 02:38:58 -07:00
committed by Peter Steinberger
parent b13fc7eccd
commit bc78b343ba
4 changed files with 155 additions and 25 deletions

View File

@@ -28,6 +28,7 @@ This is for cooperative/shared inbox hardening. A single Gateway shared by mutua
It also warns when small models (`<=300B`) are used without sandboxing and with web/browser tools enabled.
For webhook ingress, it warns when `hooks.defaultSessionKey` is unset, when request `sessionKey` overrides are enabled, and when overrides are enabled without `hooks.allowedSessionKeyPrefixes`.
It also warns when sandbox Docker settings are configured while sandbox mode is off, when `gateway.nodes.denyCommands` uses ineffective pattern-like/unknown entries, when `gateway.nodes.allowCommands` explicitly enables dangerous node commands, when global `tools.profile="minimal"` is overridden by agent tool profiles, when open groups expose runtime/filesystem tools without sandbox/workspace guards, and when installed extension 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).
It also warns when sandbox browser uses Docker `bridge` network without `sandbox.browser.cdpSourceRange`.
It also warns when existing sandbox browser Docker containers have missing/stale hash labels (for example pre-migration containers missing `openclaw.browserConfigEpoch`) and recommends `openclaw sandbox recreate --browser --all`.
It also warns when npm-based plugin/hook install records are unpinned, missing integrity metadata, or drift from currently installed package versions.

View File

@@ -117,31 +117,33 @@ When the audit prints findings, treat this as a priority order:
High-signal `checkId` values you will most likely see in real deployments (not exhaustive):
| `checkId` | Severity | Why it matters | Primary fix key/path | Auto-fix |
| -------------------------------------------------- | ------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -------- |
| `fs.state_dir.perms_world_writable` | critical | Other users/processes can modify full OpenClaw state | filesystem perms on `~/.openclaw` | yes |
| `fs.config.perms_writable` | critical | Others can change auth/tool policy/config | filesystem perms on `~/.openclaw/openclaw.json` | yes |
| `fs.config.perms_world_readable` | critical | Config can expose tokens/settings | filesystem perms on config file | yes |
| `gateway.bind_no_auth` | critical | Remote bind without shared secret | `gateway.bind`, `gateway.auth.*` | no |
| `gateway.loopback_no_auth` | critical | Reverse-proxied loopback may become unauthenticated | `gateway.auth.*`, proxy setup | no |
| `gateway.http.no_auth` | warn/critical | Gateway HTTP APIs reachable with `auth.mode="none"` | `gateway.auth.mode`, `gateway.http.endpoints.*` | no |
| `gateway.tools_invoke_http.dangerous_allow` | warn/critical | Re-enables dangerous tools over HTTP API | `gateway.tools.allow` | no |
| `gateway.nodes.allow_commands_dangerous` | warn/critical | Enables high-impact node commands (camera/screen/contacts/calendar/SMS) | `gateway.nodes.allowCommands` | no |
| `gateway.tailscale_funnel` | critical | Public internet exposure | `gateway.tailscale.mode` | no |
| `gateway.control_ui.insecure_auth` | warn | Insecure-auth compatibility toggle enabled | `gateway.controlUi.allowInsecureAuth` | no |
| `gateway.control_ui.device_auth_disabled` | critical | Disables device identity check | `gateway.controlUi.dangerouslyDisableDeviceAuth` | no |
| `config.insecure_or_dangerous_flags` | warn | Any insecure/dangerous debug flags enabled | multiple keys (see finding detail) | no |
| `hooks.token_too_short` | warn | Easier brute force on hook ingress | `hooks.token` | no |
| `hooks.request_session_key_enabled` | warn/critical | External caller can choose sessionKey | `hooks.allowRequestSessionKey` | no |
| `hooks.request_session_key_prefixes_missing` | warn/critical | No bound on external session key shapes | `hooks.allowedSessionKeyPrefixes` | no |
| `logging.redact_off` | warn | Sensitive values leak to logs/status | `logging.redactSensitive` | yes |
| `sandbox.docker_config_mode_off` | warn | Sandbox Docker config present but inactive | `agents.*.sandbox.mode` | no |
| `tools.exec.host_sandbox_no_sandbox_defaults` | warn | `exec host=sandbox` resolves to host exec when sandbox is off | `tools.exec.host`, `agents.defaults.sandbox.mode` | no |
| `tools.exec.host_sandbox_no_sandbox_agents` | warn | Per-agent `exec host=sandbox` resolves to host exec when sandbox is off | `agents.list[].tools.exec.host`, `agents.list[].sandbox.mode` | no |
| `security.exposure.open_groups_with_runtime_or_fs` | critical/warn | Open groups can reach command/file tools without sandbox/workspace guards | `channels.*.groupPolicy`, `tools.profile/deny`, `tools.fs.workspaceOnly`, `agents.*.sandbox.mode` | no |
| `tools.profile_minimal_overridden` | warn | Agent overrides bypass global minimal profile | `agents.list[].tools.profile` | no |
| `plugins.tools_reachable_permissive_policy` | warn | Extension tools reachable in permissive contexts | `tools.profile` + tool allow/deny | no |
| `models.small_params` | critical/info | Small models + unsafe tool surfaces raise injection risk | model choice + sandbox/tool policy | no |
| `checkId` | Severity | Why it matters | Primary fix key/path | Auto-fix |
| -------------------------------------------------- | ------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -------- |
| `fs.state_dir.perms_world_writable` | critical | Other users/processes can modify full OpenClaw state | filesystem perms on `~/.openclaw` | yes |
| `fs.config.perms_writable` | critical | Others can change auth/tool policy/config | filesystem perms on `~/.openclaw/openclaw.json` | yes |
| `fs.config.perms_world_readable` | critical | Config can expose tokens/settings | filesystem perms on config file | yes |
| `gateway.bind_no_auth` | critical | Remote bind without shared secret | `gateway.bind`, `gateway.auth.*` | no |
| `gateway.loopback_no_auth` | critical | Reverse-proxied loopback may become unauthenticated | `gateway.auth.*`, proxy setup | no |
| `gateway.http.no_auth` | warn/critical | Gateway HTTP APIs reachable with `auth.mode="none"` | `gateway.auth.mode`, `gateway.http.endpoints.*` | no |
| `gateway.tools_invoke_http.dangerous_allow` | warn/critical | Re-enables dangerous tools over HTTP API | `gateway.tools.allow` | no |
| `gateway.nodes.allow_commands_dangerous` | warn/critical | Enables high-impact node commands (camera/screen/contacts/calendar/SMS) | `gateway.nodes.allowCommands` | no |
| `gateway.tailscale_funnel` | critical | Public internet exposure | `gateway.tailscale.mode` | no |
| `gateway.control_ui.insecure_auth` | warn | Insecure-auth compatibility toggle enabled | `gateway.controlUi.allowInsecureAuth` | no |
| `gateway.control_ui.device_auth_disabled` | critical | Disables device identity check | `gateway.controlUi.dangerouslyDisableDeviceAuth` | no |
| `gateway.real_ip_fallback_enabled` | warn/critical | Trusting `X-Real-IP` fallback can enable source-IP spoofing via proxy misconfig | `gateway.allowRealIpFallback`, `gateway.trustedProxies` | no |
| `discovery.mdns_full_mode` | warn/critical | mDNS full mode advertises `cliPath`/`sshPort` metadata on local network | `discovery.mdns.mode`, `gateway.bind` | no |
| `config.insecure_or_dangerous_flags` | warn | Any insecure/dangerous debug flags enabled | multiple keys (see finding detail) | no |
| `hooks.token_too_short` | warn | Easier brute force on hook ingress | `hooks.token` | no |
| `hooks.request_session_key_enabled` | warn/critical | External caller can choose sessionKey | `hooks.allowRequestSessionKey` | no |
| `hooks.request_session_key_prefixes_missing` | warn/critical | No bound on external session key shapes | `hooks.allowedSessionKeyPrefixes` | no |
| `logging.redact_off` | warn | Sensitive values leak to logs/status | `logging.redactSensitive` | yes |
| `sandbox.docker_config_mode_off` | warn | Sandbox Docker config present but inactive | `agents.*.sandbox.mode` | no |
| `tools.exec.host_sandbox_no_sandbox_defaults` | warn | `exec host=sandbox` resolves to host exec when sandbox is off | `tools.exec.host`, `agents.defaults.sandbox.mode` | no |
| `tools.exec.host_sandbox_no_sandbox_agents` | warn | Per-agent `exec host=sandbox` resolves to host exec when sandbox is off | `agents.list[].tools.exec.host`, `agents.list[].sandbox.mode` | no |
| `security.exposure.open_groups_with_runtime_or_fs` | critical/warn | Open groups can reach command/file tools without sandbox/workspace guards | `channels.*.groupPolicy`, `tools.profile/deny`, `tools.fs.workspaceOnly`, `agents.*.sandbox.mode` | no |
| `tools.profile_minimal_overridden` | warn | Agent overrides bypass global minimal profile | `agents.list[].tools.profile` | no |
| `plugins.tools_reachable_permissive_policy` | warn | Extension tools reachable in permissive contexts | `tools.profile` + tool allow/deny | no |
| `models.small_params` | critical/info | Small models + unsafe tool surfaces raise injection risk | model choice + sandbox/tool policy | no |
## Control UI over HTTP

View File

@@ -973,6 +973,102 @@ describe("security audit", () => {
expect(finding?.detail).toContain("tools.exec.applyPatch.workspaceOnly=false");
});
it("scores X-Real-IP fallback risk by gateway exposure", async () => {
const cases: Array<{
name: string;
cfg: OpenClawConfig;
expectedSeverity: "warn" | "critical";
}> = [
{
name: "loopback gateway",
cfg: {
gateway: {
bind: "loopback",
allowRealIpFallback: true,
trustedProxies: ["127.0.0.1"],
auth: {
mode: "token",
token: "very-long-token-1234567890",
},
},
},
expectedSeverity: "warn",
},
{
name: "lan gateway",
cfg: {
gateway: {
bind: "lan",
allowRealIpFallback: true,
trustedProxies: ["10.0.0.1"],
auth: {
mode: "token",
token: "very-long-token-1234567890",
},
},
},
expectedSeverity: "critical",
},
];
for (const testCase of cases) {
const res = await audit(testCase.cfg);
expect(
hasFinding(res, "gateway.real_ip_fallback_enabled", testCase.expectedSeverity),
testCase.name,
).toBe(true);
}
});
it("scores mDNS full mode risk by gateway bind mode", async () => {
const cases: Array<{
name: string;
cfg: OpenClawConfig;
expectedSeverity: "warn" | "critical";
}> = [
{
name: "loopback gateway with full mDNS",
cfg: {
gateway: {
bind: "loopback",
auth: {
mode: "token",
token: "very-long-token-1234567890",
},
},
discovery: {
mdns: { mode: "full" },
},
},
expectedSeverity: "warn",
},
{
name: "lan gateway with full mDNS",
cfg: {
gateway: {
bind: "lan",
auth: {
mode: "token",
token: "very-long-token-1234567890",
},
},
discovery: {
mdns: { mode: "full" },
},
},
expectedSeverity: "critical",
},
];
for (const testCase of cases) {
const res = await audit(testCase.cfg);
expect(
hasFinding(res, "discovery.mdns_full_mode", testCase.expectedSeverity),
testCase.name,
).toBe(true);
}
});
it("evaluates trusted-proxy auth guardrails", async () => {
const cases: Array<{
name: string;

View File

@@ -270,6 +270,8 @@ function collectGatewayConfigFindings(
(auth.mode === "token" && hasToken) || (auth.mode === "password" && hasPassword);
const hasTailscaleAuth = auth.allowTailscale && tailscaleMode === "serve";
const hasGatewayAuth = hasSharedSecret || hasTailscaleAuth;
const allowRealIpFallback = cfg.gateway?.allowRealIpFallback === true;
const mdnsMode = cfg.discovery?.mdns?.mode ?? "minimal";
// HTTP /tools/invoke is intended for narrow automation, not session orchestration/admin operations.
// If operators opt-in to re-enabling these tools over HTTP, warn loudly so the choice is explicit.
@@ -334,6 +336,35 @@ function collectGatewayConfigFindings(
});
}
if (allowRealIpFallback) {
const exposed = bind !== "loopback" || auth.mode === "trusted-proxy";
findings.push({
checkId: "gateway.real_ip_fallback_enabled",
severity: exposed ? "critical" : "warn",
title: "X-Real-IP fallback is enabled",
detail:
"gateway.allowRealIpFallback=true trusts X-Real-IP when trusted proxies omit X-Forwarded-For. " +
"Misconfigured proxies that forward client-supplied X-Real-IP can spoof source IP and local-client checks.",
remediation:
"Keep gateway.allowRealIpFallback=false (default). Only enable this when your trusted proxy " +
"always overwrites X-Real-IP and cannot provide X-Forwarded-For.",
});
}
if (mdnsMode === "full") {
const exposed = bind !== "loopback";
findings.push({
checkId: "discovery.mdns_full_mode",
severity: exposed ? "critical" : "warn",
title: "mDNS full mode can leak host metadata",
detail:
'discovery.mdns.mode="full" publishes cliPath/sshPort in local-network TXT records. ' +
"This can reveal usernames, filesystem layout, and management ports.",
remediation:
'Prefer discovery.mdns.mode="minimal" (recommended) or "off", especially when gateway.bind is not loopback.',
});
}
if (tailscaleMode === "funnel") {
findings.push({
checkId: "gateway.tailscale_funnel",