From f858b5de22083a4459f29d70e3dd2265ff45d113 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Fri, 1 May 2026 08:22:06 -0700 Subject: [PATCH] fix(security): keep plain audit off plugin runtimes Keep routine security audit on config/filesystem checks by default, reserving plugin runtime collectors for deep audit paths.\n\nThanks @vincentkoc --- CHANGELOG.md | 1 + docs/cli/security.md | 2 ++ src/cli/security-cli.ts | 7 ++++-- .../audit-plugin-readonly-scope.test.ts | 24 ++++++++++++++++++- src/security/audit.ts | 2 +- 5 files changed, 32 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ec67f30165..c54ac1d38b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ Docs: https://docs.openclaw.ai - Discord/voice: lengthen the default voice join Ready wait, add configurable `voice.connectTimeoutMs`/`voice.reconnectGraceMs`, and warn before destroying unrecovered disconnected sessions so slow Discord voice handshakes and reconnects no longer fail silently. Fixes #63098; refs #39825 and #65039. Thanks @darealgege, @kzicherman, and @ayochim. - Gateway/health: refresh cached health RPC snapshots when channel runtime state diverges, so Discord and other channel status reads no longer report stale running or connected values until the cache TTL expires. (#75423) Thanks @clawsweeper. - Gateway/sessions: keep session-store reads from running stale prune and entry-count cap maintenance during startup, so oversized stores no longer block chat history readiness after updates while writes and `sessions cleanup --enforce` still preserve the cleanup safeguards. Fixes #70050. Thanks @tangda18. +- Security/audit: keep plain `security audit` on the cold config/filesystem path and reserve plugin runtime security collectors for `--deep`, so large plugin installs cannot execute every plugin runtime during routine audits. Thanks @vincentkoc. - Discord/voice: merge configured media-understanding providers such as Deepgram into partial active provider registries, so follow-up voice turns keep transcribing after another media plugin is already active. Fixes #65687. Thanks @OneMintJulep. - WhatsApp: stage `qrcode` through root mirrored runtime dependencies so packaged QR pairing can render from staged plugin-runtime-deps installs. Fixes #75394. Thanks @FelipeX2001. - Discord/voice: apply per-channel Discord `systemPrompt` overrides to voice transcript turns by forwarding the trusted channel prompt through the voice agent run. Fixes #47095. Thanks @qearlyao. diff --git a/docs/cli/security.md b/docs/cli/security.md index d40c91ee4d9..93160d1ec57 100644 --- a/docs/cli/security.md +++ b/docs/cli/security.md @@ -25,6 +25,8 @@ openclaw security audit --fix openclaw security audit --json ``` +Plain `security audit` stays on the cold config/filesystem/read-only path. It does not discover plugin runtime security collectors by default, so routine audits do not load every installed plugin runtime. Use `--deep` to include best-effort live Gateway probes and plugin-owned security audit collectors; explicit internal callers may also opt into those plugin-owned collectors when they already have an appropriate runtime scope. + The audit warns when multiple DM senders share the main session and recommends **secure DM mode**: `session.dmScope="per-channel-peer"` (or `per-account-channel-peer` for multi-account channels) for shared inboxes. This is for cooperative/shared inbox hardening. A single Gateway shared by mutually untrusted/adversarial operators is not a recommended setup; split trust boundaries with separate gateways (or separate OS users/hosts). It also emits `security.trust_model.multi_user_heuristic` when config suggests likely shared-user ingress (for example open DM/group policy, configured group targets, or wildcard sender rules), and reminds you that OpenClaw is a personal-assistant trust model by default. diff --git a/src/cli/security-cli.ts b/src/cli/security-cli.ts index 3a9d7b9eeee..49ff2b96db3 100644 --- a/src/cli/security-cli.ts +++ b/src/cli/security-cli.ts @@ -41,7 +41,10 @@ export function registerSecurityCli(program: Command) { () => `\n${theme.heading("Examples:")}\n${formatHelpExamples([ ["openclaw security audit", "Run a local security audit."], - ["openclaw security audit --deep", "Include best-effort live Gateway probe checks."], + [ + "openclaw security audit --deep", + "Include best-effort live Gateway probes and plugin-owned security audit collectors.", + ], ["openclaw security audit --deep --token ", "Use explicit token for deep probe."], [ "openclaw security audit --deep --password ", @@ -55,7 +58,7 @@ export function registerSecurityCli(program: Command) { security .command("audit") .description("Audit config + local state for common security foot-guns") - .option("--deep", "Attempt live Gateway probe (best-effort)", false) + .option("--deep", "Attempt live Gateway probes and plugin-owned collector checks", false) .option("--token ", "Use explicit gateway token for deep probe auth") .option("--password ", "Use explicit gateway password for deep probe auth") .option("--fix", "Apply safe fixes (tighten defaults + chmod state/config)", false) diff --git a/src/security/audit-plugin-readonly-scope.test.ts b/src/security/audit-plugin-readonly-scope.test.ts index 10dde131653..200b30999a4 100644 --- a/src/security/audit-plugin-readonly-scope.test.ts +++ b/src/security/audit-plugin-readonly-scope.test.ts @@ -23,7 +23,7 @@ vi.mock("../plugins/runtime/metadata-registry-loader.js", () => ({ loadPluginMetadataRegistrySnapshotMock(...args), })); -const { collectPluginSecurityAuditFindings } = await import("./audit.js"); +const { collectPluginSecurityAuditFindings, runSecurityAudit } = await import("./audit.js"); function createAuditContext(params: { sourceConfig: Parameters[0]["sourceConfig"]; @@ -152,4 +152,26 @@ describe("security audit read-only plugin scope", () => { expect(applyPluginAutoEnableMock).not.toHaveBeenCalled(); expect(loadPluginMetadataRegistrySnapshotMock).not.toHaveBeenCalled(); }); + + it("keeps plain security audit off plugin collector runtime discovery by default", async () => { + const sourceConfig = { + plugins: { + allow: ["audit-plugin"], + }, + }; + + await runSecurityAudit({ + config: sourceConfig, + sourceConfig, + env: {}, + includeFilesystem: false, + includeChannelSecurity: false, + stateDir: "/tmp/openclaw-test-state", + configPath: "/tmp/openclaw-test-config.json", + }); + + expect(getActivePluginRegistryMock).not.toHaveBeenCalled(); + expect(applyPluginAutoEnableMock).not.toHaveBeenCalled(); + expect(loadPluginMetadataRegistrySnapshotMock).not.toHaveBeenCalled(); + }); }); diff --git a/src/security/audit.ts b/src/security/audit.ts index e2f3db7440f..b569a36bee2 100644 --- a/src/security/audit.ts +++ b/src/security/audit.ts @@ -946,7 +946,7 @@ async function createAuditExecutionContext( execDockerRawFn: opts.execDockerRawFn, probeGatewayFn: opts.probeGatewayFn, plugins: opts.plugins, - loadPluginSecurityCollectors: opts.loadPluginSecurityCollectors !== false, + loadPluginSecurityCollectors: opts.loadPluginSecurityCollectors ?? deep, workspaceDir, configSnapshot, codeSafetySummaryCache: opts.codeSafetySummaryCache ?? new Map>(),