From 17bae93680f07496913eb5cae0d3d74fde61b15d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 2 Mar 2026 02:23:03 +0000 Subject: [PATCH] fix(security): warn on wildcard control-ui origins and feishu owner grants --- src/security/audit.test.ts | 52 ++++++++++++++++++++++++++ src/security/audit.ts | 75 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) diff --git a/src/security/audit.test.ts b/src/security/audit.test.ts index 93e9d113174..c92aa40520e 100644 --- a/src/security/audit.test.ts +++ b/src/security/audit.test.ts @@ -1223,6 +1223,29 @@ describe("security audit", () => { expectFinding(res, "gateway.control_ui.allowed_origins_required", "critical"); }); + it("flags wildcard Control UI origins by exposure level", async () => { + const loopbackCfg: OpenClawConfig = { + gateway: { + bind: "loopback", + controlUi: { allowedOrigins: ["*"] }, + }, + }; + const exposedCfg: OpenClawConfig = { + gateway: { + bind: "lan", + auth: { mode: "token", token: "very-long-browser-token-0123456789" }, + controlUi: { allowedOrigins: ["*"] }, + }, + }; + + const loopback = await audit(loopbackCfg); + const exposed = await audit(exposedCfg); + + expectFinding(loopback, "gateway.control_ui.allowed_origins_wildcard", "warn"); + expectFinding(exposed, "gateway.control_ui.allowed_origins_wildcard", "critical"); + expectNoFinding(exposed, "gateway.control_ui.allowed_origins_required"); + }); + it("flags dangerous host-header origin fallback and suppresses missing allowed-origins finding", async () => { const cfg: OpenClawConfig = { gateway: { @@ -1243,6 +1266,35 @@ describe("security audit", () => { ); }); + it("warns when Feishu doc tool is enabled because create supports owner_open_id", async () => { + const cfg: OpenClawConfig = { + channels: { + feishu: { + appId: "cli_test", + appSecret: "secret_test", + }, + }, + }; + + const res = await audit(cfg); + expectFinding(res, "channels.feishu.doc_owner_open_id", "warn"); + }); + + it("does not warn for Feishu owner_open_id when doc tools are disabled", async () => { + const cfg: OpenClawConfig = { + channels: { + feishu: { + appId: "cli_test", + appSecret: "secret_test", + tools: { doc: false }, + }, + }, + }; + + const res = await audit(cfg); + expectNoFinding(res, "channels.feishu.doc_owner_open_id"); + }); + it("scores X-Real-IP fallback risk by gateway exposure", async () => { const trustedProxyCfg = (trustedProxies: string[]): OpenClawConfig => ({ gateway: { diff --git a/src/security/audit.ts b/src/security/audit.ts index e6254b5cc80..1febc036f52 100644 --- a/src/security/audit.ts +++ b/src/security/audit.ts @@ -128,6 +128,57 @@ function normalizeAllowFromList(list: Array | undefined | null) return list.map((v) => String(v).trim()).filter(Boolean); } +function asRecord(value: unknown): Record | undefined { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return undefined; + } + return value as Record; +} + +function hasNonEmptyString(value: unknown): boolean { + return typeof value === "string" && value.trim().length > 0; +} + +function isFeishuDocToolEnabled(cfg: OpenClawConfig): boolean { + const channels = asRecord(cfg.channels); + const feishu = asRecord(channels?.feishu); + if (!feishu || feishu.enabled === false) { + return false; + } + + const baseTools = asRecord(feishu.tools); + const baseDocEnabled = baseTools?.doc !== false; + const baseAppId = hasNonEmptyString(feishu.appId); + const baseAppSecret = hasNonEmptyString(feishu.appSecret); + const baseConfigured = baseAppId && baseAppSecret; + + const accounts = asRecord(feishu.accounts); + if (!accounts || Object.keys(accounts).length === 0) { + return baseDocEnabled && baseConfigured; + } + + for (const accountValue of Object.values(accounts)) { + const account = asRecord(accountValue) ?? {}; + if (account.enabled === false) { + continue; + } + const accountTools = asRecord(account.tools); + const effectiveTools = accountTools ?? baseTools; + const docEnabled = effectiveTools?.doc !== false; + if (!docEnabled) { + continue; + } + const accountConfigured = + (hasNonEmptyString(account.appId) || baseAppId) && + (hasNonEmptyString(account.appSecret) || baseAppSecret); + if (accountConfigured) { + return true; + } + } + + return false; +} + async function collectFilesystemFindings(params: { stateDir: string; configPath: string; @@ -366,6 +417,18 @@ function collectGatewayConfigFindings( "If your deployment intentionally relies on Host-header origin fallback, set gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true.", }); } + if (controlUiAllowedOrigins.includes("*")) { + const exposed = bind !== "loopback"; + findings.push({ + checkId: "gateway.control_ui.allowed_origins_wildcard", + severity: exposed ? "critical" : "warn", + title: "Control UI allowed origins contains wildcard", + detail: + 'gateway.controlUi.allowedOrigins includes "*" which effectively disables origin allowlisting for Control UI/WebChat requests.', + remediation: + "Replace wildcard origins with explicit trusted origins (for example https://control.example.com).", + }); + } if (dangerouslyAllowHostHeaderOriginFallback) { const exposed = bind !== "loopback"; findings.push({ @@ -452,6 +515,18 @@ function collectGatewayConfigFindings( }); } + if (isFeishuDocToolEnabled(cfg)) { + findings.push({ + checkId: "channels.feishu.doc_owner_open_id", + severity: "warn", + title: "Feishu doc create can grant owner permissions", + detail: + 'channels.feishu tools include "doc"; feishu_doc action "create" accepts owner_open_id and can grant document access to that user.', + remediation: + "Disable channels.feishu.tools.doc when not needed, and restrict tool access so untrusted prompts cannot set owner_open_id.", + }); + } + const enabledDangerousFlags = collectEnabledInsecureOrDangerousFlags(cfg); if (enabledDangerousFlags.length > 0) { findings.push({