mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 19:14:33 +00:00
fix(security): warn on wildcard control-ui origins and feishu owner grants
This commit is contained in:
@@ -1223,6 +1223,29 @@ describe("security audit", () => {
|
|||||||
expectFinding(res, "gateway.control_ui.allowed_origins_required", "critical");
|
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 () => {
|
it("flags dangerous host-header origin fallback and suppresses missing allowed-origins finding", async () => {
|
||||||
const cfg: OpenClawConfig = {
|
const cfg: OpenClawConfig = {
|
||||||
gateway: {
|
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 () => {
|
it("scores X-Real-IP fallback risk by gateway exposure", async () => {
|
||||||
const trustedProxyCfg = (trustedProxies: string[]): OpenClawConfig => ({
|
const trustedProxyCfg = (trustedProxies: string[]): OpenClawConfig => ({
|
||||||
gateway: {
|
gateway: {
|
||||||
|
|||||||
@@ -128,6 +128,57 @@ function normalizeAllowFromList(list: Array<string | number> | undefined | null)
|
|||||||
return list.map((v) => String(v).trim()).filter(Boolean);
|
return list.map((v) => String(v).trim()).filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
||||||
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return value as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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: {
|
async function collectFilesystemFindings(params: {
|
||||||
stateDir: string;
|
stateDir: string;
|
||||||
configPath: 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 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) {
|
if (dangerouslyAllowHostHeaderOriginFallback) {
|
||||||
const exposed = bind !== "loopback";
|
const exposed = bind !== "loopback";
|
||||||
findings.push({
|
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);
|
const enabledDangerousFlags = collectEnabledInsecureOrDangerousFlags(cfg);
|
||||||
if (enabledDangerousFlags.length > 0) {
|
if (enabledDangerousFlags.length > 0) {
|
||||||
findings.push({
|
findings.push({
|
||||||
|
|||||||
Reference in New Issue
Block a user