mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 05:21:23 +00:00
refactor(archive): share archive path safety helpers
This commit is contained in:
63
src/infra/archive-path.ts
Normal file
63
src/infra/archive-path.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import path from "node:path";
|
||||
import { resolveSafeBaseDir } from "./path-safety.js";
|
||||
|
||||
export function isWindowsDrivePath(value: string): boolean {
|
||||
return /^[a-zA-Z]:[\\/]/.test(value);
|
||||
}
|
||||
|
||||
export function normalizeArchiveEntryPath(raw: string): string {
|
||||
return raw.replaceAll("\\", "/");
|
||||
}
|
||||
|
||||
export function validateArchiveEntryPath(
|
||||
entryPath: string,
|
||||
params?: { escapeLabel?: string },
|
||||
): void {
|
||||
if (!entryPath || entryPath === "." || entryPath === "./") {
|
||||
return;
|
||||
}
|
||||
if (isWindowsDrivePath(entryPath)) {
|
||||
throw new Error(`archive entry uses a drive path: ${entryPath}`);
|
||||
}
|
||||
const normalized = path.posix.normalize(normalizeArchiveEntryPath(entryPath));
|
||||
const escapeLabel = params?.escapeLabel ?? "destination";
|
||||
if (normalized === ".." || normalized.startsWith("../")) {
|
||||
throw new Error(`archive entry escapes ${escapeLabel}: ${entryPath}`);
|
||||
}
|
||||
if (path.posix.isAbsolute(normalized) || normalized.startsWith("//")) {
|
||||
throw new Error(`archive entry is absolute: ${entryPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function stripArchivePath(entryPath: string, stripComponents: number): string | null {
|
||||
const raw = normalizeArchiveEntryPath(entryPath);
|
||||
if (!raw || raw === "." || raw === "./") {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Mimic tar --strip-components semantics (raw segments before normalization)
|
||||
// so strip-induced escapes like "a/../b" are visible to validators.
|
||||
const parts = raw.split("/").filter((part) => part.length > 0 && part !== ".");
|
||||
const strip = Math.max(0, Math.floor(stripComponents));
|
||||
const stripped = strip === 0 ? parts.join("/") : parts.slice(strip).join("/");
|
||||
const result = path.posix.normalize(stripped);
|
||||
if (!result || result === "." || result === "./") {
|
||||
return null;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function resolveArchiveOutputPath(params: {
|
||||
rootDir: string;
|
||||
relPath: string;
|
||||
originalPath: string;
|
||||
escapeLabel?: string;
|
||||
}): string {
|
||||
const safeBase = resolveSafeBaseDir(params.rootDir);
|
||||
const outPath = path.resolve(params.rootDir, params.relPath);
|
||||
const escapeLabel = params.escapeLabel ?? "destination";
|
||||
if (!outPath.startsWith(safeBase)) {
|
||||
throw new Error(`archive entry escapes ${escapeLabel}: ${params.originalPath}`);
|
||||
}
|
||||
return outPath;
|
||||
}
|
||||
Reference in New Issue
Block a user