feat(gateway)!: require explicit non-loopback control-ui origins
This commit is contained in:
@@ -1136,6 +1136,38 @@ describe("security audit", () => {
|
||||
expect(finding?.detail).toContain("tools.exec.applyPatch.workspaceOnly=false");
|
||||
});
|
||||
|
||||
it("flags non-loopback Control UI without allowed origins", async () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
gateway: {
|
||||
bind: "lan",
|
||||
auth: { mode: "token", token: "very-long-browser-token-0123456789" },
|
||||
},
|
||||
};
|
||||
|
||||
const res = await audit(cfg);
|
||||
expectFinding(res, "gateway.control_ui.allowed_origins_required", "critical");
|
||||
});
|
||||
|
||||
it("flags dangerous host-header origin fallback and suppresses missing allowed-origins finding", async () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
gateway: {
|
||||
bind: "lan",
|
||||
auth: { mode: "token", token: "very-long-browser-token-0123456789" },
|
||||
controlUi: {
|
||||
dangerouslyAllowHostHeaderOriginFallback: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const res = await audit(cfg);
|
||||
expectFinding(res, "gateway.control_ui.host_header_origin_fallback", "critical");
|
||||
expectNoFinding(res, "gateway.control_ui.allowed_origins_required");
|
||||
const flags = res.findings.find((f) => f.checkId === "config.insecure_or_dangerous_flags");
|
||||
expect(flags?.detail ?? "").toContain(
|
||||
"gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true",
|
||||
);
|
||||
});
|
||||
|
||||
it("scores X-Real-IP fallback risk by gateway exposure", async () => {
|
||||
const trustedProxyCfg = (trustedProxies: string[]): OpenClawConfig => ({
|
||||
gateway: {
|
||||
|
||||
@@ -266,6 +266,11 @@ function collectGatewayConfigFindings(
|
||||
const tailscaleMode = cfg.gateway?.tailscale?.mode ?? "off";
|
||||
const auth = resolveGatewayAuth({ authConfig: cfg.gateway?.auth, tailscaleMode, env });
|
||||
const controlUiEnabled = cfg.gateway?.controlUi?.enabled !== false;
|
||||
const controlUiAllowedOrigins = (cfg.gateway?.controlUi?.allowedOrigins ?? [])
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean);
|
||||
const dangerouslyAllowHostHeaderOriginFallback =
|
||||
cfg.gateway?.controlUi?.dangerouslyAllowHostHeaderOriginFallback === true;
|
||||
const trustedProxies = Array.isArray(cfg.gateway?.trustedProxies)
|
||||
? cfg.gateway.trustedProxies
|
||||
: [];
|
||||
@@ -340,6 +345,37 @@ function collectGatewayConfigFindings(
|
||||
remediation: "Set gateway.auth (token recommended) or keep the Control UI local-only.",
|
||||
});
|
||||
}
|
||||
if (
|
||||
bind !== "loopback" &&
|
||||
controlUiEnabled &&
|
||||
controlUiAllowedOrigins.length === 0 &&
|
||||
!dangerouslyAllowHostHeaderOriginFallback
|
||||
) {
|
||||
findings.push({
|
||||
checkId: "gateway.control_ui.allowed_origins_required",
|
||||
severity: "critical",
|
||||
title: "Non-loopback Control UI missing explicit allowed origins",
|
||||
detail:
|
||||
"Control UI is enabled on a non-loopback bind but gateway.controlUi.allowedOrigins is empty. " +
|
||||
"Strict origin policy requires explicit allowed origins for non-loopback deployments.",
|
||||
remediation:
|
||||
"Set gateway.controlUi.allowedOrigins to full trusted origins (for example https://control.example.com). " +
|
||||
"If your deployment intentionally relies on Host-header origin fallback, set gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true.",
|
||||
});
|
||||
}
|
||||
if (dangerouslyAllowHostHeaderOriginFallback) {
|
||||
const exposed = bind !== "loopback";
|
||||
findings.push({
|
||||
checkId: "gateway.control_ui.host_header_origin_fallback",
|
||||
severity: exposed ? "critical" : "warn",
|
||||
title: "DANGEROUS: Host-header origin fallback enabled",
|
||||
detail:
|
||||
"gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true enables Host-header origin fallback " +
|
||||
"for Control UI/WebChat websocket checks and weakens DNS rebinding protections.",
|
||||
remediation:
|
||||
"Disable gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback and configure explicit gateway.controlUi.allowedOrigins.",
|
||||
});
|
||||
}
|
||||
|
||||
if (allowRealIpFallback) {
|
||||
const hasNonLoopbackTrustedProxy = trustedProxies.some(
|
||||
|
||||
@@ -5,6 +5,9 @@ export function collectEnabledInsecureOrDangerousFlags(cfg: OpenClawConfig): str
|
||||
if (cfg.gateway?.controlUi?.allowInsecureAuth === true) {
|
||||
enabledFlags.push("gateway.controlUi.allowInsecureAuth=true");
|
||||
}
|
||||
if (cfg.gateway?.controlUi?.dangerouslyAllowHostHeaderOriginFallback === true) {
|
||||
enabledFlags.push("gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true");
|
||||
}
|
||||
if (cfg.gateway?.controlUi?.dangerouslyDisableDeviceAuth === true) {
|
||||
enabledFlags.push("gateway.controlUi.dangerouslyDisableDeviceAuth=true");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user