mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:30:43 +00:00
fix(security): ignore plugin install debris in audits
This commit is contained in:
@@ -33,6 +33,7 @@ Docs: https://docs.openclaw.ai
|
||||
### Fixes
|
||||
|
||||
- Telegram/native commands: pass persisted session files into plugin commands for topic-bound sessions, so `/codex bind` works from Telegram forum topics. Refs #75845 and #76049. Thanks @MatthewSchleder.
|
||||
- Security audit/plugins: ignore plugin install backup, disabled, and dependency debris directories when enumerating installed plugin roots, avoiding false-positive findings for `.openclaw-install-backups` after plugin updates. Fixes #75456.
|
||||
- Telegram: honor runtime conversation bindings for native slash commands in bound top-level groups, so commands like `/status@bot` route to the active non-`main` session instead of falling back to the default route. Fixes #75405; supersedes #75558. Thanks @ziptbm and @yfge.
|
||||
- Gateway/tasks: make task registry maintenance use pass-local backing-session lookups and fresh active child-session indexes, avoiding repeated full task snapshots and session-store clones on large stale registries. Fixes #73517 and #75708; supersedes #74406 and #75709. Thanks @Lightningxxl, @glfruit, and @jared-rebel.
|
||||
- Models CLI: restore `openclaw models list --provider <id>` catalog and registry fallback rows for unconfigured providers, so provider-specific verification commands no longer report "No models found." Fixes #75517; supersedes #75615. Thanks @lotsoftick and @koshaji.
|
||||
|
||||
@@ -153,6 +153,54 @@ description: test skill
|
||||
expect(findings.some((f) => f.checkId === "plugins.code_safety.entry_escape")).toBe(true);
|
||||
});
|
||||
|
||||
it("ignores install backup and debris dirs when scanning installed plugin roots", async () => {
|
||||
const scanSpy = vi
|
||||
.spyOn(skillScanner, "scanDirectoryWithSummary")
|
||||
.mockImplementation(async (dirPath) => ({
|
||||
scannedFiles: 1,
|
||||
critical: dirPath.includes(`${path.sep}demo`) ? 1 : 0,
|
||||
warn: 0,
|
||||
info: 0,
|
||||
findings: dirPath.includes(`${path.sep}demo`)
|
||||
? [
|
||||
{
|
||||
ruleId: "dangerous-exec",
|
||||
severity: "critical",
|
||||
file: path.join(dirPath, "index.js"),
|
||||
line: 1,
|
||||
message: "dangerous exec",
|
||||
evidence: "exec(...)",
|
||||
},
|
||||
]
|
||||
: [],
|
||||
}));
|
||||
|
||||
try {
|
||||
const tmpDir = await makeTmpDir("audit-scanner-install-debris");
|
||||
for (const name of [
|
||||
"demo",
|
||||
".openclaw-install-backups",
|
||||
"node_modules",
|
||||
"old-plugin.backup-20260502",
|
||||
"old-plugin.disabled.20260502",
|
||||
"old-plugin.bak",
|
||||
]) {
|
||||
const pluginDir = path.join(tmpDir, "extensions", name);
|
||||
await fs.mkdir(pluginDir, { recursive: true });
|
||||
await fs.writeFile(path.join(pluginDir, "index.js"), "eval('1+1');");
|
||||
}
|
||||
|
||||
const findings = await collectPluginsCodeSafetyFindings({ stateDir: tmpDir });
|
||||
|
||||
expect(scanSpy.mock.calls.map(([dirPath]) => path.basename(dirPath))).toEqual(["demo"]);
|
||||
const codeSafetyFinding = findings.find((f) => f.checkId === "plugins.code_safety");
|
||||
expect(codeSafetyFinding?.title).toContain('Plugin "demo"');
|
||||
expect(findings.map((f) => f.title).join("\n")).not.toContain(".openclaw-install-backups");
|
||||
} finally {
|
||||
scanSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("surfaces manifest_parse_error finding when plugin package.json is malformed JSON", async () => {
|
||||
const tmpDir = await makeTmpDir("audit-manifest-parse-error");
|
||||
const pluginDir = path.join(tmpDir, "extensions", "broken-plugin");
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
normalizeOptionalLowercaseString,
|
||||
normalizeOptionalString,
|
||||
} from "../shared/string-coerce.js";
|
||||
import { shouldIgnoreInstalledPluginDirName } from "./installed-plugin-dirs.js";
|
||||
import { extensionUsesSkippedScannerPath, isPathInside } from "./scan-paths.js";
|
||||
import type { SkillScanFinding } from "./skill-scanner.js";
|
||||
import type { ExecFn } from "./windows-acl.js";
|
||||
@@ -219,6 +220,7 @@ async function listInstalledPluginDirs(params: {
|
||||
const pluginDirs = entries
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => entry.name)
|
||||
.filter((name) => !shouldIgnoreInstalledPluginDirName(name))
|
||||
.filter(Boolean);
|
||||
return { extensionsDir, pluginDirs };
|
||||
}
|
||||
|
||||
@@ -360,6 +360,37 @@ describe("security audit install metadata findings", () => {
|
||||
expect(phantomFinding?.detail).not.toContain("installed-plugin");
|
||||
});
|
||||
|
||||
it("ignores install backup and debris dirs when auditing installed plugin roots", async () => {
|
||||
const stateDir = await makeTmpDir("installed-plugin-debris");
|
||||
for (const name of [
|
||||
"live-plugin",
|
||||
".openclaw-install-backups",
|
||||
"node_modules",
|
||||
"old-plugin.backup-20260502",
|
||||
"old-plugin.disabled.20260502",
|
||||
"old-plugin.bak",
|
||||
]) {
|
||||
await fs.mkdir(path.join(stateDir, "extensions", name), {
|
||||
recursive: true,
|
||||
});
|
||||
}
|
||||
|
||||
const findings = await runInstallMetadataAudit({}, stateDir);
|
||||
|
||||
const noAllowlist = findings.find(
|
||||
(finding) => finding.checkId === "plugins.extensions_no_allowlist",
|
||||
);
|
||||
expect(noAllowlist?.detail).toContain("Found 1 extension(s)");
|
||||
|
||||
const toolsReachable = findings.find(
|
||||
(finding) => finding.checkId === "plugins.tools_reachable_permissive_policy",
|
||||
);
|
||||
expect(toolsReachable?.detail).toContain("Enabled extension plugins: live-plugin.");
|
||||
expect(findings.map((finding) => finding.detail).join("\n")).not.toContain(
|
||||
".openclaw-install-backups",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not report bundled provider and utility plugins as phantom allowlist entries", async () => {
|
||||
const stateDir = await makeTmpDir("phantom-bundled-providers");
|
||||
await fs.mkdir(path.join(stateDir, "extensions", "installed-plugin"), {
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from "../plugins/plugin-registry.js";
|
||||
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
|
||||
import type { SecurityAuditFinding } from "./audit.types.js";
|
||||
import { shouldIgnoreInstalledPluginDirName } from "./installed-plugin-dirs.js";
|
||||
|
||||
type SandboxToolPolicy = import("../agents/sandbox/types.js").SandboxToolPolicy;
|
||||
|
||||
@@ -143,6 +144,7 @@ async function listInstalledPluginDirs(params: {
|
||||
const pluginDirs = entries
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => entry.name)
|
||||
.filter((name) => !shouldIgnoreInstalledPluginDirName(name))
|
||||
.filter(Boolean);
|
||||
return { extensionsDir, pluginDirs };
|
||||
}
|
||||
|
||||
26
src/security/installed-plugin-dirs.ts
Normal file
26
src/security/installed-plugin-dirs.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
|
||||
|
||||
const IGNORED_INSTALLED_PLUGIN_DIR_NAMES = new Set(["node_modules", ".openclaw-install-backups"]);
|
||||
|
||||
export function shouldIgnoreInstalledPluginDirName(name: string): boolean {
|
||||
const normalized = normalizeOptionalLowercaseString(name);
|
||||
if (!normalized) {
|
||||
return true;
|
||||
}
|
||||
if (IGNORED_INSTALLED_PLUGIN_DIR_NAMES.has(normalized)) {
|
||||
return true;
|
||||
}
|
||||
if (normalized.startsWith(".")) {
|
||||
return true;
|
||||
}
|
||||
if (normalized.endsWith(".bak")) {
|
||||
return true;
|
||||
}
|
||||
if (normalized.includes(".backup-")) {
|
||||
return true;
|
||||
}
|
||||
if (normalized.includes(".disabled")) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
Reference in New Issue
Block a user