diff --git a/src/gateway/server-startup-log.test.ts b/src/gateway/server-startup-log.test.ts new file mode 100644 index 00000000000..04648ddebb2 --- /dev/null +++ b/src/gateway/server-startup-log.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it, vi } from "vitest"; +import { logGatewayStartup } from "./server-startup-log.js"; + +describe("gateway startup log", () => { + it("warns when dangerous config flags are enabled", () => { + const info = vi.fn(); + const warn = vi.fn(); + + logGatewayStartup({ + cfg: { + gateway: { + controlUi: { + dangerouslyDisableDeviceAuth: true, + }, + }, + }, + bindHost: "127.0.0.1", + port: 18789, + log: { info, warn }, + isNixMode: false, + }); + + expect(warn).toHaveBeenCalledTimes(1); + expect(warn).toHaveBeenCalledWith(expect.stringContaining("dangerous config flags enabled")); + expect(warn).toHaveBeenCalledWith( + expect.stringContaining("gateway.controlUi.dangerouslyDisableDeviceAuth=true"), + ); + expect(warn).toHaveBeenCalledWith(expect.stringContaining("openclaw security audit")); + }); + + it("does not warn when dangerous config flags are disabled", () => { + const info = vi.fn(); + const warn = vi.fn(); + + logGatewayStartup({ + cfg: {}, + bindHost: "127.0.0.1", + port: 18789, + log: { info, warn }, + isNixMode: false, + }); + + expect(warn).not.toHaveBeenCalled(); + }); +}); diff --git a/src/gateway/server-startup-log.ts b/src/gateway/server-startup-log.ts index cf6d2575c7c..0a95bc68ea7 100644 --- a/src/gateway/server-startup-log.ts +++ b/src/gateway/server-startup-log.ts @@ -3,6 +3,7 @@ import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js"; import { resolveConfiguredModelRef } from "../agents/model-selection.js"; import type { loadConfig } from "../config/config.js"; import { getResolvedLoggerSettings } from "../logging.js"; +import { collectEnabledInsecureOrDangerousFlags } from "../security/dangerous-config-flags.js"; export function logGatewayStartup(params: { cfg: ReturnType; @@ -10,7 +11,7 @@ export function logGatewayStartup(params: { bindHosts?: string[]; port: number; tlsEnabled?: boolean; - log: { info: (msg: string, meta?: Record) => void }; + log: { info: (msg: string, meta?: Record) => void; warn: (msg: string) => void }; isNixMode: boolean; }) { const { provider: agentProvider, model: agentModel } = resolveConfiguredModelRef({ @@ -37,4 +38,12 @@ export function logGatewayStartup(params: { if (params.isNixMode) { params.log.info("gateway: running in Nix mode (config managed externally)"); } + + const enabledDangerousFlags = collectEnabledInsecureOrDangerousFlags(params.cfg); + if (enabledDangerousFlags.length > 0) { + const warning = + `security warning: dangerous config flags enabled: ${enabledDangerousFlags.join(", ")}. ` + + "Run `openclaw security audit`."; + params.log.warn(warning); + } } diff --git a/src/security/audit.ts b/src/security/audit.ts index dc6d14a14cb..a1a95df601d 100644 --- a/src/security/audit.ts +++ b/src/security/audit.ts @@ -39,6 +39,7 @@ import { formatPermissionRemediation, inspectPathPermissions, } from "./audit-fs.js"; +import { collectEnabledInsecureOrDangerousFlags } from "./dangerous-config-flags.js"; import { DEFAULT_GATEWAY_HTTP_TOOL_DENY } from "./dangerous-tools.js"; import type { ExecFn } from "./windows-acl.js"; @@ -119,30 +120,6 @@ function normalizeAllowFromList(list: Array | undefined | null) return list.map((v) => String(v).trim()).filter(Boolean); } -function collectEnabledInsecureOrDangerousFlags(cfg: OpenClawConfig): string[] { - const enabledFlags: string[] = []; - if (cfg.gateway?.controlUi?.allowInsecureAuth === true) { - enabledFlags.push("gateway.controlUi.allowInsecureAuth=true"); - } - if (cfg.gateway?.controlUi?.dangerouslyDisableDeviceAuth === true) { - enabledFlags.push("gateway.controlUi.dangerouslyDisableDeviceAuth=true"); - } - if (cfg.hooks?.gmail?.allowUnsafeExternalContent === true) { - enabledFlags.push("hooks.gmail.allowUnsafeExternalContent=true"); - } - if (Array.isArray(cfg.hooks?.mappings)) { - for (const [index, mapping] of cfg.hooks.mappings.entries()) { - if (mapping?.allowUnsafeExternalContent === true) { - enabledFlags.push(`hooks.mappings[${index}].allowUnsafeExternalContent=true`); - } - } - } - if (cfg.tools?.exec?.applyPatch?.workspaceOnly === false) { - enabledFlags.push("tools.exec.applyPatch.workspaceOnly=false"); - } - return enabledFlags; -} - async function collectFilesystemFindings(params: { stateDir: string; configPath: string; diff --git a/src/security/dangerous-config-flags.ts b/src/security/dangerous-config-flags.ts new file mode 100644 index 00000000000..a272d5a069a --- /dev/null +++ b/src/security/dangerous-config-flags.ts @@ -0,0 +1,25 @@ +import type { OpenClawConfig } from "../config/config.js"; + +export function collectEnabledInsecureOrDangerousFlags(cfg: OpenClawConfig): string[] { + const enabledFlags: string[] = []; + if (cfg.gateway?.controlUi?.allowInsecureAuth === true) { + enabledFlags.push("gateway.controlUi.allowInsecureAuth=true"); + } + if (cfg.gateway?.controlUi?.dangerouslyDisableDeviceAuth === true) { + enabledFlags.push("gateway.controlUi.dangerouslyDisableDeviceAuth=true"); + } + if (cfg.hooks?.gmail?.allowUnsafeExternalContent === true) { + enabledFlags.push("hooks.gmail.allowUnsafeExternalContent=true"); + } + if (Array.isArray(cfg.hooks?.mappings)) { + for (const [index, mapping] of cfg.hooks.mappings.entries()) { + if (mapping?.allowUnsafeExternalContent === true) { + enabledFlags.push(`hooks.mappings[${index}].allowUnsafeExternalContent=true`); + } + } + } + if (cfg.tools?.exec?.applyPatch?.workspaceOnly === false) { + enabledFlags.push("tools.exec.applyPatch.workspaceOnly=false"); + } + return enabledFlags; +}