feat(gateway)!: require explicit non-loopback control-ui origins

This commit is contained in:
Peter Steinberger
2026-02-24 01:52:15 +00:00
parent edfefdff7d
commit 223d7dc23d
19 changed files with 187 additions and 10 deletions

View File

@@ -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: {

View File

@@ -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(

View File

@@ -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");
}