mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 20:24:33 +00:00
refactor: clarify strict loopback proxy audit rules
This commit is contained in:
@@ -974,6 +974,20 @@ describe("security audit", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("scores X-Real-IP fallback risk by gateway exposure", async () => {
|
it("scores X-Real-IP fallback risk by gateway exposure", async () => {
|
||||||
|
const trustedProxyCfg = (trustedProxies: string[]): OpenClawConfig => ({
|
||||||
|
gateway: {
|
||||||
|
bind: "loopback",
|
||||||
|
allowRealIpFallback: true,
|
||||||
|
trustedProxies,
|
||||||
|
auth: {
|
||||||
|
mode: "trusted-proxy",
|
||||||
|
trustedProxy: {
|
||||||
|
userHeader: "x-forwarded-user",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const cases: Array<{
|
const cases: Array<{
|
||||||
name: string;
|
name: string;
|
||||||
cfg: OpenClawConfig;
|
cfg: OpenClawConfig;
|
||||||
@@ -1011,36 +1025,22 @@ describe("security audit", () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "loopback trusted-proxy with loopback-only proxies",
|
name: "loopback trusted-proxy with loopback-only proxies",
|
||||||
cfg: {
|
cfg: trustedProxyCfg(["127.0.0.1"]),
|
||||||
gateway: {
|
|
||||||
bind: "loopback",
|
|
||||||
allowRealIpFallback: true,
|
|
||||||
trustedProxies: ["127.0.0.1"],
|
|
||||||
auth: {
|
|
||||||
mode: "trusted-proxy",
|
|
||||||
trustedProxy: {
|
|
||||||
userHeader: "x-forwarded-user",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectedSeverity: "warn",
|
expectedSeverity: "warn",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "loopback trusted-proxy with non-loopback proxy range",
|
name: "loopback trusted-proxy with non-loopback proxy range",
|
||||||
cfg: {
|
cfg: trustedProxyCfg(["127.0.0.1", "10.0.0.0/8"]),
|
||||||
gateway: {
|
expectedSeverity: "critical",
|
||||||
bind: "loopback",
|
},
|
||||||
allowRealIpFallback: true,
|
{
|
||||||
trustedProxies: ["127.0.0.1", "10.0.0.0/8"],
|
name: "loopback trusted-proxy with 127.0.0.2",
|
||||||
auth: {
|
cfg: trustedProxyCfg(["127.0.0.2"]),
|
||||||
mode: "trusted-proxy",
|
expectedSeverity: "critical",
|
||||||
trustedProxy: {
|
},
|
||||||
userHeader: "x-forwarded-user",
|
{
|
||||||
},
|
name: "loopback trusted-proxy with 127.0.0.0/8 range",
|
||||||
},
|
cfg: trustedProxyCfg(["127.0.0.0/8"]),
|
||||||
},
|
|
||||||
},
|
|
||||||
expectedSeverity: "critical",
|
expectedSeverity: "critical",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import type { OpenClawConfig } from "../config/config.js";
|
|||||||
import { resolveConfigPath, resolveStateDir } from "../config/paths.js";
|
import { resolveConfigPath, resolveStateDir } from "../config/paths.js";
|
||||||
import { resolveGatewayAuth } from "../gateway/auth.js";
|
import { resolveGatewayAuth } from "../gateway/auth.js";
|
||||||
import { buildGatewayConnectionDetails } from "../gateway/call.js";
|
import { buildGatewayConnectionDetails } from "../gateway/call.js";
|
||||||
import { isLoopbackAddress } from "../gateway/net.js";
|
|
||||||
import { resolveGatewayProbeAuth } from "../gateway/probe-auth.js";
|
import { resolveGatewayProbeAuth } from "../gateway/probe-auth.js";
|
||||||
import { probeGateway } from "../gateway/probe.js";
|
import { probeGateway } from "../gateway/probe.js";
|
||||||
import { collectChannelSecurityFindings } from "./audit-channel.js";
|
import { collectChannelSecurityFindings } from "./audit-channel.js";
|
||||||
@@ -340,7 +339,7 @@ function collectGatewayConfigFindings(
|
|||||||
|
|
||||||
if (allowRealIpFallback) {
|
if (allowRealIpFallback) {
|
||||||
const hasNonLoopbackTrustedProxy = trustedProxies.some(
|
const hasNonLoopbackTrustedProxy = trustedProxies.some(
|
||||||
(proxy) => !isLoopbackOnlyTrustedProxyEntry(proxy),
|
(proxy) => !isStrictLoopbackTrustedProxyEntry(proxy),
|
||||||
);
|
);
|
||||||
const exposed =
|
const exposed =
|
||||||
bind !== "loopback" || (auth.mode === "trusted-proxy" && hasNonLoopbackTrustedProxy);
|
bind !== "loopback" || (auth.mode === "trusted-proxy" && hasNonLoopbackTrustedProxy);
|
||||||
@@ -508,13 +507,15 @@ function collectGatewayConfigFindings(
|
|||||||
return findings;
|
return findings;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isLoopbackOnlyTrustedProxyEntry(entry: string): boolean {
|
// Keep this stricter than isLoopbackAddress on purpose: this check is for
|
||||||
|
// trust boundaries, so only explicit localhost proxy hops are treated as local.
|
||||||
|
function isStrictLoopbackTrustedProxyEntry(entry: string): boolean {
|
||||||
const candidate = entry.trim();
|
const candidate = entry.trim();
|
||||||
if (!candidate) {
|
if (!candidate) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!candidate.includes("/")) {
|
if (!candidate.includes("/")) {
|
||||||
return isLoopbackAddress(candidate);
|
return candidate === "127.0.0.1" || candidate.toLowerCase() === "::1";
|
||||||
}
|
}
|
||||||
|
|
||||||
const [rawIp, rawPrefix] = candidate.split("/", 2);
|
const [rawIp, rawPrefix] = candidate.split("/", 2);
|
||||||
@@ -527,11 +528,7 @@ function isLoopbackOnlyTrustedProxyEntry(entry: string): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (ipVersion === 4) {
|
if (ipVersion === 4) {
|
||||||
if (prefix < 8 || prefix > 32) {
|
return rawIp.trim() === "127.0.0.1" && prefix === 32;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const firstOctet = Number.parseInt(rawIp.trim().split(".")[0] ?? "", 10);
|
|
||||||
return firstOctet === 127;
|
|
||||||
}
|
}
|
||||||
if (ipVersion === 6) {
|
if (ipVersion === 6) {
|
||||||
return prefix === 128 && rawIp.trim().toLowerCase() === "::1";
|
return prefix === 128 && rawIp.trim().toLowerCase() === "::1";
|
||||||
|
|||||||
Reference in New Issue
Block a user