Spaces:
Running
Running
| import { posix as path } from "node:path"; | |
| export class InvalidPathError extends Error { | |
| constructor(message: string) { | |
| super(message); | |
| this.name = "InvalidPathError"; | |
| } | |
| } | |
| const ALLOWED_PREFIXES = ["/home/user", "/tmp"]; | |
| export function validateAbsolutePath(input: string): string { | |
| if (typeof input !== "string" || input.length === 0) { | |
| throw new InvalidPathError("Path must be a non-empty string"); | |
| } | |
| if (input.includes("\0")) { | |
| throw new InvalidPathError("Path contains null byte"); | |
| } | |
| if (!input.startsWith("/")) { | |
| throw new InvalidPathError("Path must be absolute"); | |
| } | |
| const normalized = path.normalize(input); | |
| if (normalized.includes("\0")) { | |
| throw new InvalidPathError("Path contains null byte after normalization"); | |
| } | |
| if (normalized === "/" || normalized === "" || normalized.startsWith("..")) { | |
| throw new InvalidPathError("Path traversal not allowed"); | |
| } | |
| // Forbid traversal after normalization (defense in depth) | |
| const parts = normalized.split("/"); | |
| if (parts.some((p) => p === "..")) { | |
| throw new InvalidPathError("Path traversal not allowed"); | |
| } | |
| if (!isUnderAllowedRoot(normalized)) { | |
| throw new InvalidPathError("Path outside allowed namespace"); | |
| } | |
| return normalized; | |
| } | |
| function isUnderAllowedRoot(p: string): boolean { | |
| return ALLOWED_PREFIXES.some( | |
| (root) => p === root || p.startsWith(`${root}/`), | |
| ); | |
| } | |
| export function encodePathForUri(p: string): string { | |
| return encodeURIComponent(p); | |
| } | |
| export function decodePathFromUri(p: string): string { | |
| return decodeURIComponent(p); | |
| } | |