mirror of
https://github.com/farcasclaudiu/openclaw.git
synced 2026-06-28 17:01:53 +03:00
refactor(install): share safe install path helpers
This commit is contained in:
+16
-38
@@ -10,6 +10,7 @@ import {
|
|||||||
resolvePackedRootDir,
|
resolvePackedRootDir,
|
||||||
} from "../infra/archive.js";
|
} from "../infra/archive.js";
|
||||||
import { installPackageDir } from "../infra/install-package-dir.js";
|
import { installPackageDir } from "../infra/install-package-dir.js";
|
||||||
|
import { resolveSafeInstallDir, unscopedPackageName } from "../infra/install-safe-path.js";
|
||||||
import { validateRegistryNpmSpec } from "../infra/npm-registry-spec.js";
|
import { validateRegistryNpmSpec } from "../infra/npm-registry-spec.js";
|
||||||
import { runCommandWithTimeout } from "../process/exec.js";
|
import { runCommandWithTimeout } from "../process/exec.js";
|
||||||
import { CONFIG_DIR, resolveUserPath } from "../utils.js";
|
import { CONFIG_DIR, resolveUserPath } from "../utils.js";
|
||||||
@@ -38,22 +39,6 @@ export type InstallHooksResult =
|
|||||||
|
|
||||||
const defaultLogger: HookInstallLogger = {};
|
const defaultLogger: HookInstallLogger = {};
|
||||||
|
|
||||||
function unscopedPackageName(name: string): string {
|
|
||||||
const trimmed = name.trim();
|
|
||||||
if (!trimmed) {
|
|
||||||
return trimmed;
|
|
||||||
}
|
|
||||||
return trimmed.includes("/") ? (trimmed.split("/").pop() ?? trimmed) : trimmed;
|
|
||||||
}
|
|
||||||
|
|
||||||
function safeDirName(input: string): string {
|
|
||||||
const trimmed = input.trim();
|
|
||||||
if (!trimmed) {
|
|
||||||
return trimmed;
|
|
||||||
}
|
|
||||||
return trimmed.replaceAll("/", "__").replaceAll("\\", "__");
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateHookId(hookId: string): string | null {
|
function validateHookId(hookId: string): string | null {
|
||||||
if (!hookId) {
|
if (!hookId) {
|
||||||
return "invalid hook name: missing";
|
return "invalid hook name: missing";
|
||||||
@@ -73,32 +58,17 @@ export function resolveHookInstallDir(hookId: string, hooksDir?: string): string
|
|||||||
if (hookIdError) {
|
if (hookIdError) {
|
||||||
throw new Error(hookIdError);
|
throw new Error(hookIdError);
|
||||||
}
|
}
|
||||||
const targetDirResult = resolveSafeInstallDir(hooksBase, hookId);
|
const targetDirResult = resolveSafeInstallDir({
|
||||||
|
baseDir: hooksBase,
|
||||||
|
id: hookId,
|
||||||
|
invalidNameMessage: "invalid hook name: path traversal detected",
|
||||||
|
});
|
||||||
if (!targetDirResult.ok) {
|
if (!targetDirResult.ok) {
|
||||||
throw new Error(targetDirResult.error);
|
throw new Error(targetDirResult.error);
|
||||||
}
|
}
|
||||||
return targetDirResult.path;
|
return targetDirResult.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveSafeInstallDir(
|
|
||||||
hooksDir: string,
|
|
||||||
hookId: string,
|
|
||||||
): { ok: true; path: string } | { ok: false; error: string } {
|
|
||||||
const targetDir = path.join(hooksDir, safeDirName(hookId));
|
|
||||||
const resolvedBase = path.resolve(hooksDir);
|
|
||||||
const resolvedTarget = path.resolve(targetDir);
|
|
||||||
const relative = path.relative(resolvedBase, resolvedTarget);
|
|
||||||
if (
|
|
||||||
!relative ||
|
|
||||||
relative === ".." ||
|
|
||||||
relative.startsWith(`..${path.sep}`) ||
|
|
||||||
path.isAbsolute(relative)
|
|
||||||
) {
|
|
||||||
return { ok: false, error: "invalid hook name: path traversal detected" };
|
|
||||||
}
|
|
||||||
return { ok: true, path: targetDir };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function ensureOpenClawHooks(manifest: HookPackageManifest) {
|
async function ensureOpenClawHooks(manifest: HookPackageManifest) {
|
||||||
const hooks = manifest[MANIFEST_KEY]?.hooks;
|
const hooks = manifest[MANIFEST_KEY]?.hooks;
|
||||||
if (!Array.isArray(hooks)) {
|
if (!Array.isArray(hooks)) {
|
||||||
@@ -188,7 +158,11 @@ async function installHookPackageFromDir(params: {
|
|||||||
: path.join(CONFIG_DIR, "hooks");
|
: path.join(CONFIG_DIR, "hooks");
|
||||||
await fs.mkdir(hooksDir, { recursive: true });
|
await fs.mkdir(hooksDir, { recursive: true });
|
||||||
|
|
||||||
const targetDirResult = resolveSafeInstallDir(hooksDir, hookPackId);
|
const targetDirResult = resolveSafeInstallDir({
|
||||||
|
baseDir: hooksDir,
|
||||||
|
id: hookPackId,
|
||||||
|
invalidNameMessage: "invalid hook name: path traversal detected",
|
||||||
|
});
|
||||||
if (!targetDirResult.ok) {
|
if (!targetDirResult.ok) {
|
||||||
return { ok: false, error: targetDirResult.error };
|
return { ok: false, error: targetDirResult.error };
|
||||||
}
|
}
|
||||||
@@ -271,7 +245,11 @@ async function installHookFromDir(params: {
|
|||||||
: path.join(CONFIG_DIR, "hooks");
|
: path.join(CONFIG_DIR, "hooks");
|
||||||
await fs.mkdir(hooksDir, { recursive: true });
|
await fs.mkdir(hooksDir, { recursive: true });
|
||||||
|
|
||||||
const targetDirResult = resolveSafeInstallDir(hooksDir, hookName);
|
const targetDirResult = resolveSafeInstallDir({
|
||||||
|
baseDir: hooksDir,
|
||||||
|
id: hookName,
|
||||||
|
invalidNameMessage: "invalid hook name: path traversal detected",
|
||||||
|
});
|
||||||
if (!targetDirResult.ok) {
|
if (!targetDirResult.ok) {
|
||||||
return { ok: false, error: targetDirResult.error };
|
return { ok: false, error: targetDirResult.error };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
export function unscopedPackageName(name: string): string {
|
||||||
|
const trimmed = name.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
return trimmed.includes("/") ? (trimmed.split("/").pop() ?? trimmed) : trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function safeDirName(input: string): string {
|
||||||
|
const trimmed = input.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
return trimmed.replaceAll("/", "__").replaceAll("\\", "__");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveSafeInstallDir(params: {
|
||||||
|
baseDir: string;
|
||||||
|
id: string;
|
||||||
|
invalidNameMessage: string;
|
||||||
|
}): { ok: true; path: string } | { ok: false; error: string } {
|
||||||
|
const targetDir = path.join(params.baseDir, safeDirName(params.id));
|
||||||
|
const resolvedBase = path.resolve(params.baseDir);
|
||||||
|
const resolvedTarget = path.resolve(targetDir);
|
||||||
|
const relative = path.relative(resolvedBase, resolvedTarget);
|
||||||
|
if (
|
||||||
|
!relative ||
|
||||||
|
relative === ".." ||
|
||||||
|
relative.startsWith(`..${path.sep}`) ||
|
||||||
|
path.isAbsolute(relative)
|
||||||
|
) {
|
||||||
|
return { ok: false, error: params.invalidNameMessage };
|
||||||
|
}
|
||||||
|
return { ok: true, path: targetDir };
|
||||||
|
}
|
||||||
+15
-38
@@ -10,6 +10,11 @@ import {
|
|||||||
resolvePackedRootDir,
|
resolvePackedRootDir,
|
||||||
} from "../infra/archive.js";
|
} from "../infra/archive.js";
|
||||||
import { installPackageDir } from "../infra/install-package-dir.js";
|
import { installPackageDir } from "../infra/install-package-dir.js";
|
||||||
|
import {
|
||||||
|
resolveSafeInstallDir,
|
||||||
|
safeDirName,
|
||||||
|
unscopedPackageName,
|
||||||
|
} from "../infra/install-safe-path.js";
|
||||||
import { validateRegistryNpmSpec } from "../infra/npm-registry-spec.js";
|
import { validateRegistryNpmSpec } from "../infra/npm-registry-spec.js";
|
||||||
import { runCommandWithTimeout } from "../process/exec.js";
|
import { runCommandWithTimeout } from "../process/exec.js";
|
||||||
import * as skillScanner from "../security/skill-scanner.js";
|
import * as skillScanner from "../security/skill-scanner.js";
|
||||||
@@ -38,23 +43,6 @@ export type InstallPluginResult =
|
|||||||
| { ok: false; error: string };
|
| { ok: false; error: string };
|
||||||
|
|
||||||
const defaultLogger: PluginInstallLogger = {};
|
const defaultLogger: PluginInstallLogger = {};
|
||||||
|
|
||||||
function unscopedPackageName(name: string): string {
|
|
||||||
const trimmed = name.trim();
|
|
||||||
if (!trimmed) {
|
|
||||||
return trimmed;
|
|
||||||
}
|
|
||||||
return trimmed.includes("/") ? (trimmed.split("/").pop() ?? trimmed) : trimmed;
|
|
||||||
}
|
|
||||||
|
|
||||||
function safeDirName(input: string): string {
|
|
||||||
const trimmed = input.trim();
|
|
||||||
if (!trimmed) {
|
|
||||||
return trimmed;
|
|
||||||
}
|
|
||||||
return trimmed.replaceAll("/", "__").replaceAll("\\", "__");
|
|
||||||
}
|
|
||||||
|
|
||||||
function safeFileName(input: string): string {
|
function safeFileName(input: string): string {
|
||||||
return safeDirName(input);
|
return safeDirName(input);
|
||||||
}
|
}
|
||||||
@@ -108,32 +96,17 @@ export function resolvePluginInstallDir(pluginId: string, extensionsDir?: string
|
|||||||
if (pluginIdError) {
|
if (pluginIdError) {
|
||||||
throw new Error(pluginIdError);
|
throw new Error(pluginIdError);
|
||||||
}
|
}
|
||||||
const targetDirResult = resolveSafeInstallDir(extensionsBase, pluginId);
|
const targetDirResult = resolveSafeInstallDir({
|
||||||
|
baseDir: extensionsBase,
|
||||||
|
id: pluginId,
|
||||||
|
invalidNameMessage: "invalid plugin name: path traversal detected",
|
||||||
|
});
|
||||||
if (!targetDirResult.ok) {
|
if (!targetDirResult.ok) {
|
||||||
throw new Error(targetDirResult.error);
|
throw new Error(targetDirResult.error);
|
||||||
}
|
}
|
||||||
return targetDirResult.path;
|
return targetDirResult.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveSafeInstallDir(
|
|
||||||
extensionsDir: string,
|
|
||||||
pluginId: string,
|
|
||||||
): { ok: true; path: string } | { ok: false; error: string } {
|
|
||||||
const targetDir = path.join(extensionsDir, safeDirName(pluginId));
|
|
||||||
const resolvedBase = path.resolve(extensionsDir);
|
|
||||||
const resolvedTarget = path.resolve(targetDir);
|
|
||||||
const relative = path.relative(resolvedBase, resolvedTarget);
|
|
||||||
if (
|
|
||||||
!relative ||
|
|
||||||
relative === ".." ||
|
|
||||||
relative.startsWith(`..${path.sep}`) ||
|
|
||||||
path.isAbsolute(relative)
|
|
||||||
) {
|
|
||||||
return { ok: false, error: "invalid plugin name: path traversal detected" };
|
|
||||||
}
|
|
||||||
return { ok: true, path: targetDir };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function installPluginFromPackageDir(params: {
|
async function installPluginFromPackageDir(params: {
|
||||||
packageDir: string;
|
packageDir: string;
|
||||||
extensionsDir?: string;
|
extensionsDir?: string;
|
||||||
@@ -225,7 +198,11 @@ async function installPluginFromPackageDir(params: {
|
|||||||
: path.join(CONFIG_DIR, "extensions");
|
: path.join(CONFIG_DIR, "extensions");
|
||||||
await fs.mkdir(extensionsDir, { recursive: true });
|
await fs.mkdir(extensionsDir, { recursive: true });
|
||||||
|
|
||||||
const targetDirResult = resolveSafeInstallDir(extensionsDir, pluginId);
|
const targetDirResult = resolveSafeInstallDir({
|
||||||
|
baseDir: extensionsDir,
|
||||||
|
id: pluginId,
|
||||||
|
invalidNameMessage: "invalid plugin name: path traversal detected",
|
||||||
|
});
|
||||||
if (!targetDirResult.ok) {
|
if (!targetDirResult.ok) {
|
||||||
return { ok: false, error: targetDirResult.error };
|
return { ok: false, error: targetDirResult.error };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user