mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 19:41:23 +00:00
feat(gateway)!: require explicit non-loopback control-ui origins
This commit is contained in:
@@ -2,14 +2,23 @@ import { describe, expect, it } from "vitest";
|
||||
import { checkBrowserOrigin } from "./origin-check.js";
|
||||
|
||||
describe("checkBrowserOrigin", () => {
|
||||
it("accepts same-origin host matches", () => {
|
||||
it("accepts same-origin host matches only with legacy host-header fallback", () => {
|
||||
const result = checkBrowserOrigin({
|
||||
requestHost: "127.0.0.1:18789",
|
||||
origin: "http://127.0.0.1:18789",
|
||||
allowHostHeaderOriginFallback: true,
|
||||
});
|
||||
expect(result.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects same-origin host matches when legacy host-header fallback is disabled", () => {
|
||||
const result = checkBrowserOrigin({
|
||||
requestHost: "gateway.example.com:18789",
|
||||
origin: "https://gateway.example.com:18789",
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts loopback host mismatches for dev", () => {
|
||||
const result = checkBrowserOrigin({
|
||||
requestHost: "127.0.0.1:18789",
|
||||
|
||||
@@ -25,6 +25,7 @@ export function checkBrowserOrigin(params: {
|
||||
requestHost?: string;
|
||||
origin?: string;
|
||||
allowedOrigins?: string[];
|
||||
allowHostHeaderOriginFallback?: boolean;
|
||||
}): OriginCheckResult {
|
||||
const parsedOrigin = parseOrigin(params.origin);
|
||||
if (!parsedOrigin) {
|
||||
@@ -39,7 +40,11 @@ export function checkBrowserOrigin(params: {
|
||||
}
|
||||
|
||||
const requestHost = normalizeHostHeader(params.requestHost);
|
||||
if (requestHost && parsedOrigin.host === requestHost) {
|
||||
if (
|
||||
params.allowHostHeaderOriginFallback === true &&
|
||||
requestHost &&
|
||||
parsedOrigin.host === requestHost
|
||||
) {
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ describe("resolveGatewayRuntimeConfig", () => {
|
||||
bind: "lan" as const,
|
||||
auth: TRUSTED_PROXY_AUTH,
|
||||
trustedProxies: ["192.168.1.1"],
|
||||
controlUi: { allowedOrigins: ["https://control.example.com"] },
|
||||
},
|
||||
},
|
||||
expectedBindHost: "0.0.0.0",
|
||||
@@ -90,7 +91,12 @@ describe("resolveGatewayRuntimeConfig", () => {
|
||||
{
|
||||
name: "lan binding without trusted proxies",
|
||||
cfg: {
|
||||
gateway: { bind: "lan" as const, auth: TRUSTED_PROXY_AUTH, trustedProxies: [] },
|
||||
gateway: {
|
||||
bind: "lan" as const,
|
||||
auth: TRUSTED_PROXY_AUTH,
|
||||
trustedProxies: [],
|
||||
controlUi: { allowedOrigins: ["https://control.example.com"] },
|
||||
},
|
||||
},
|
||||
expectedMessage:
|
||||
"gateway auth mode=trusted-proxy requires gateway.trustedProxies to be configured",
|
||||
@@ -121,7 +127,13 @@ describe("resolveGatewayRuntimeConfig", () => {
|
||||
it.each([
|
||||
{
|
||||
name: "lan binding with token",
|
||||
cfg: { gateway: { bind: "lan" as const, auth: TOKEN_AUTH } },
|
||||
cfg: {
|
||||
gateway: {
|
||||
bind: "lan" as const,
|
||||
auth: TOKEN_AUTH,
|
||||
controlUi: { allowedOrigins: ["https://control.example.com"] },
|
||||
},
|
||||
},
|
||||
expectedAuthMode: "token",
|
||||
expectedBindHost: "0.0.0.0",
|
||||
},
|
||||
@@ -188,6 +200,36 @@ describe("resolveGatewayRuntimeConfig", () => {
|
||||
expectedMessage,
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects non-loopback control UI when allowed origins are missing", async () => {
|
||||
await expect(
|
||||
resolveGatewayRuntimeConfig({
|
||||
cfg: {
|
||||
gateway: {
|
||||
bind: "lan",
|
||||
auth: TOKEN_AUTH,
|
||||
},
|
||||
},
|
||||
port: 18789,
|
||||
}),
|
||||
).rejects.toThrow("non-loopback Control UI requires gateway.controlUi.allowedOrigins");
|
||||
});
|
||||
|
||||
it("allows non-loopback control UI without allowed origins when dangerous fallback is enabled", async () => {
|
||||
const result = await resolveGatewayRuntimeConfig({
|
||||
cfg: {
|
||||
gateway: {
|
||||
bind: "lan",
|
||||
auth: TOKEN_AUTH,
|
||||
controlUi: {
|
||||
dangerouslyAllowHostHeaderOriginFallback: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
port: 18789,
|
||||
});
|
||||
expect(result.bindHost).toBe("0.0.0.0");
|
||||
});
|
||||
});
|
||||
|
||||
describe("HTTP security headers", () => {
|
||||
|
||||
@@ -115,6 +115,11 @@ export async function resolveGatewayRuntimeConfig(params: {
|
||||
process.env.OPENCLAW_SKIP_CANVAS_HOST !== "1" && params.cfg.canvasHost?.enabled !== false;
|
||||
|
||||
const trustedProxies = params.cfg.gateway?.trustedProxies ?? [];
|
||||
const controlUiAllowedOrigins = (params.cfg.gateway?.controlUi?.allowedOrigins ?? [])
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean);
|
||||
const dangerouslyAllowHostHeaderOriginFallback =
|
||||
params.cfg.gateway?.controlUi?.dangerouslyAllowHostHeaderOriginFallback === true;
|
||||
|
||||
assertGatewayAuthConfigured(resolvedAuth);
|
||||
if (tailscaleMode === "funnel" && authMode !== "password") {
|
||||
@@ -130,6 +135,16 @@ export async function resolveGatewayRuntimeConfig(params: {
|
||||
`refusing to bind gateway to ${bindHost}:${params.port} without auth (set gateway.auth.token/password, or set OPENCLAW_GATEWAY_TOKEN/OPENCLAW_GATEWAY_PASSWORD)`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
controlUiEnabled &&
|
||||
!isLoopbackHost(bindHost) &&
|
||||
controlUiAllowedOrigins.length === 0 &&
|
||||
!dangerouslyAllowHostHeaderOriginFallback
|
||||
) {
|
||||
throw new Error(
|
||||
"non-loopback Control UI requires gateway.controlUi.allowedOrigins (set explicit origins), or set gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true to use Host-header origin fallback mode",
|
||||
);
|
||||
}
|
||||
|
||||
if (authMode === "trusted-proxy") {
|
||||
if (trustedProxies.length === 0) {
|
||||
|
||||
@@ -334,6 +334,8 @@ export function attachGatewayWsMessageHandler(params: {
|
||||
requestHost,
|
||||
origin: requestOrigin,
|
||||
allowedOrigins: configSnapshot.gateway?.controlUi?.allowedOrigins,
|
||||
allowHostHeaderOriginFallback:
|
||||
configSnapshot.gateway?.controlUi?.dangerouslyAllowHostHeaderOriginFallback === true,
|
||||
});
|
||||
if (!originCheck.ok) {
|
||||
const errorMessage =
|
||||
|
||||
Reference in New Issue
Block a user