From b37346103285b5f4c185cd867d8b2ed0fd324291 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 15 Feb 2026 04:29:12 +0000 Subject: [PATCH] refactor(security): share scan path helpers --- src/plugins/install.ts | 17 +---------------- src/security/audit-extra.async.ts | 17 +---------------- src/security/scan-paths.ts | 17 +++++++++++++++++ 3 files changed, 19 insertions(+), 32 deletions(-) create mode 100644 src/security/scan-paths.ts diff --git a/src/plugins/install.ts b/src/plugins/install.ts index 3ef4e4bb4..97b8f597e 100644 --- a/src/plugins/install.ts +++ b/src/plugins/install.ts @@ -17,6 +17,7 @@ import { } from "../infra/install-safe-path.js"; import { validateRegistryNpmSpec } from "../infra/npm-registry-spec.js"; import { runCommandWithTimeout } from "../process/exec.js"; +import { extensionUsesSkippedScannerPath, isPathInside } from "../security/scan-paths.js"; import * as skillScanner from "../security/skill-scanner.js"; import { CONFIG_DIR, resolveUserPath } from "../utils.js"; @@ -60,22 +61,6 @@ function validatePluginId(pluginId: string): string | null { return null; } -function isPathInside(basePath: string, candidatePath: string): boolean { - const base = path.resolve(basePath); - const candidate = path.resolve(candidatePath); - const rel = path.relative(base, candidate); - return rel === "" || (!rel.startsWith(`..${path.sep}`) && rel !== ".." && !path.isAbsolute(rel)); -} - -function extensionUsesSkippedScannerPath(entry: string): boolean { - const segments = entry.split(/[\\/]+/).filter(Boolean); - return segments.some( - (segment) => - segment === "node_modules" || - (segment.startsWith(".") && segment !== "." && segment !== ".."), - ); -} - async function ensureOpenClawExtensions(manifest: PackageManifest) { const extensions = manifest[MANIFEST_KEY]?.extensions; if (!Array.isArray(extensions)) { diff --git a/src/security/audit-extra.async.ts b/src/security/audit-extra.async.ts index 197a1c982..3c75966f7 100644 --- a/src/security/audit-extra.async.ts +++ b/src/security/audit-extra.async.ts @@ -31,6 +31,7 @@ import { inspectPathPermissions, safeStat, } from "./audit-fs.js"; +import { extensionUsesSkippedScannerPath, isPathInside } from "./scan-paths.js"; import * as skillScanner from "./skill-scanner.js"; export type SecurityAuditFinding = { @@ -62,22 +63,6 @@ function expandTilde(p: string, env: NodeJS.ProcessEnv): string | null { return null; } -function isPathInside(basePath: string, candidatePath: string): boolean { - const base = path.resolve(basePath); - const candidate = path.resolve(candidatePath); - const rel = path.relative(base, candidate); - return rel === "" || (!rel.startsWith(`..${path.sep}`) && rel !== ".." && !path.isAbsolute(rel)); -} - -function extensionUsesSkippedScannerPath(entry: string): boolean { - const segments = entry.split(/[\\/]+/).filter(Boolean); - return segments.some( - (segment) => - segment === "node_modules" || - (segment.startsWith(".") && segment !== "." && segment !== ".."), - ); -} - async function readPluginManifestExtensions(pluginPath: string): Promise { const manifestPath = path.join(pluginPath, "package.json"); const raw = await fs.readFile(manifestPath, "utf-8").catch(() => ""); diff --git a/src/security/scan-paths.ts b/src/security/scan-paths.ts new file mode 100644 index 000000000..246df3fef --- /dev/null +++ b/src/security/scan-paths.ts @@ -0,0 +1,17 @@ +import path from "node:path"; + +export function isPathInside(basePath: string, candidatePath: string): boolean { + const base = path.resolve(basePath); + const candidate = path.resolve(candidatePath); + const rel = path.relative(base, candidate); + return rel === "" || (!rel.startsWith(`..${path.sep}`) && rel !== ".." && !path.isAbsolute(rel)); +} + +export function extensionUsesSkippedScannerPath(entry: string): boolean { + const segments = entry.split(/[\\/]+/).filter(Boolean); + return segments.some( + (segment) => + segment === "node_modules" || + (segment.startsWith(".") && segment !== "." && segment !== ".."), + ); +}