mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 12:21:24 +00:00
fix(security): restrict skill download target paths
This commit is contained in:
@@ -5,6 +5,7 @@ import path from "node:path";
|
||||
import { Readable, Transform } from "node:stream";
|
||||
import { pipeline } from "node:stream/promises";
|
||||
import * as tar from "tar";
|
||||
import { resolveSafeBaseDir } from "./path-safety.js";
|
||||
|
||||
export type ArchiveKind = "tar" | "zip";
|
||||
|
||||
@@ -101,11 +102,6 @@ export async function withTimeout<T>(
|
||||
}
|
||||
}
|
||||
|
||||
function resolveSafeBaseDir(destDir: string): string {
|
||||
const resolved = path.resolve(destDir);
|
||||
return resolved.endsWith(path.sep) ? resolved : `${resolved}${path.sep}`;
|
||||
}
|
||||
|
||||
// Path hygiene.
|
||||
function normalizeArchivePath(raw: string): string {
|
||||
// Archives may contain Windows separators; treat them as separators.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import path from "node:path";
|
||||
|
||||
export function unscopedPackageName(name: string): string {
|
||||
@@ -16,6 +17,30 @@ export function safeDirName(input: string): string {
|
||||
return trimmed.replaceAll("/", "__").replaceAll("\\", "__");
|
||||
}
|
||||
|
||||
export function safePathSegmentHashed(input: string): string {
|
||||
const trimmed = input.trim();
|
||||
const base = trimmed
|
||||
.replaceAll(/[\\/]/g, "-")
|
||||
.replaceAll(/[^a-zA-Z0-9._-]/g, "-")
|
||||
.replaceAll(/-+/g, "-")
|
||||
.replaceAll(/^-+/g, "")
|
||||
.replaceAll(/-+$/g, "");
|
||||
|
||||
const normalized = base.length > 0 ? base : "skill";
|
||||
const safe = normalized === "." || normalized === ".." ? "skill" : normalized;
|
||||
|
||||
const hash = createHash("sha256").update(trimmed).digest("hex").slice(0, 10);
|
||||
|
||||
if (safe !== trimmed) {
|
||||
const prefix = safe.length > 50 ? safe.slice(0, 50) : safe;
|
||||
return `${prefix}-${hash}`;
|
||||
}
|
||||
if (safe.length > 60) {
|
||||
return `${safe.slice(0, 50)}-${hash}`;
|
||||
}
|
||||
return safe;
|
||||
}
|
||||
|
||||
export function resolveSafeInstallDir(params: {
|
||||
baseDir: string;
|
||||
id: string;
|
||||
|
||||
20
src/infra/path-safety.ts
Normal file
20
src/infra/path-safety.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import path from "node:path";
|
||||
|
||||
export function resolveSafeBaseDir(rootDir: string): string {
|
||||
const resolved = path.resolve(rootDir);
|
||||
return resolved.endsWith(path.sep) ? resolved : `${resolved}${path.sep}`;
|
||||
}
|
||||
|
||||
export function isWithinDir(rootDir: string, targetPath: string): boolean {
|
||||
const resolvedRoot = path.resolve(rootDir);
|
||||
const resolvedTarget = path.resolve(targetPath);
|
||||
|
||||
// Windows paths are effectively case-insensitive; normalize to avoid false negatives.
|
||||
if (process.platform === "win32") {
|
||||
const relative = path.win32.relative(resolvedRoot.toLowerCase(), resolvedTarget.toLowerCase());
|
||||
return relative === "" || (!relative.startsWith("..") && !path.win32.isAbsolute(relative));
|
||||
}
|
||||
|
||||
const relative = path.relative(resolvedRoot, resolvedTarget);
|
||||
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
DEFAULT_MAIN_KEY,
|
||||
normalizeAgentId,
|
||||
} from "../routing/session-key.js";
|
||||
import { isWithinDir } from "./path-safety.js";
|
||||
import {
|
||||
ensureDir,
|
||||
existsDir,
|
||||
@@ -360,11 +361,6 @@ function isDirPath(filePath: string): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
function isWithinDir(targetPath: string, rootDir: string): boolean {
|
||||
const relative = path.relative(path.resolve(rootDir), path.resolve(targetPath));
|
||||
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
||||
}
|
||||
|
||||
function isLegacyTreeSymlinkMirror(currentDir: string, realTargetDir: string): boolean {
|
||||
let entries: fs.Dirent[];
|
||||
try {
|
||||
@@ -395,7 +391,7 @@ function isLegacyTreeSymlinkMirror(currentDir: string, realTargetDir: string): b
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
if (!isWithinDir(resolvedRealTarget, realTargetDir)) {
|
||||
if (!isWithinDir(realTargetDir, resolvedRealTarget)) {
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
|
||||
Reference in New Issue
Block a user