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

@@ -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",