mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 09:37:41 +00:00
fix(security): block hook transform symlink escapes
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { CONFIG_PATH, type HookMappingConfig, type HooksConfig } from "../config/config.js";
|
||||
import { importFileModule, resolveFunctionModuleExport } from "../hooks/module-loader.js";
|
||||
@@ -355,6 +356,34 @@ function resolvePath(baseDir: string, target: string): string {
|
||||
return path.isAbsolute(target) ? path.resolve(target) : path.resolve(baseDir, target);
|
||||
}
|
||||
|
||||
function escapesBase(baseDir: string, candidate: string): boolean {
|
||||
const relative = path.relative(baseDir, candidate);
|
||||
return relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative);
|
||||
}
|
||||
|
||||
function safeRealpathSync(candidate: string): string | null {
|
||||
try {
|
||||
const nativeRealpath = fs.realpathSync.native as ((path: string) => string) | undefined;
|
||||
return nativeRealpath ? nativeRealpath(candidate) : fs.realpathSync(candidate);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveExistingAncestor(candidate: string): string | null {
|
||||
let current = path.resolve(candidate);
|
||||
while (true) {
|
||||
if (fs.existsSync(current)) {
|
||||
return current;
|
||||
}
|
||||
const parent = path.dirname(current);
|
||||
if (parent === current) {
|
||||
return null;
|
||||
}
|
||||
current = parent;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveContainedPath(baseDir: string, target: string, label: string): string {
|
||||
const base = path.resolve(baseDir);
|
||||
const trimmed = target?.trim();
|
||||
@@ -362,8 +391,20 @@ function resolveContainedPath(baseDir: string, target: string, label: string): s
|
||||
throw new Error(`${label} module path is required`);
|
||||
}
|
||||
const resolved = resolvePath(base, trimmed);
|
||||
const relative = path.relative(base, resolved);
|
||||
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
||||
if (escapesBase(base, resolved)) {
|
||||
throw new Error(`${label} module path must be within ${base}: ${target}`);
|
||||
}
|
||||
|
||||
// Block symlink escapes for existing path segments while preserving current
|
||||
// behavior for not-yet-created files.
|
||||
const baseRealpath = safeRealpathSync(base);
|
||||
const existingAncestor = resolveExistingAncestor(resolved);
|
||||
const existingAncestorRealpath = existingAncestor ? safeRealpathSync(existingAncestor) : null;
|
||||
if (
|
||||
baseRealpath &&
|
||||
existingAncestorRealpath &&
|
||||
escapesBase(baseRealpath, existingAncestorRealpath)
|
||||
) {
|
||||
throw new Error(`${label} module path must be within ${base}: ${target}`);
|
||||
}
|
||||
return resolved;
|
||||
|
||||
Reference in New Issue
Block a user