import path from 'node:path'; import fs from 'node:fs'; /** * Resolves a user-provided path and verifies it is inside the allowed sandbox directory. * Throws SandboxEscapeError if the resolved path is outside the sandbox. * * Uses realpathSync to resolve symlinks in the sandbox root. The user-supplied path * is checked for containment AFTER lexical resolution but BEFORE resolving any symlinks * within the user path — so symlink escape attempts are caught too. * * @param userPath - The path provided by the agent (may be relative or absolute) * @param sandboxDir - The allowed root directory (already validated on session creation) * @returns The resolved absolute path, guaranteed to be within sandboxDir */ export function guardPath(userPath: string, sandboxDir: string): string { const resolved = path.resolve(sandboxDir, userPath); const sandboxResolved = fs.realpathSync.native(sandboxDir); // Normalize both paths to resolve any symlinks in the sandbox root itself. // For the user path, we check containment BEFORE resolving symlinks in the path // (so we catch symlink escape attempts too — the resolved path must still be under sandbox) if (!resolved.startsWith(sandboxResolved + path.sep) && resolved !== sandboxResolved) { throw new SandboxEscapeError(userPath, sandboxDir, resolved); } return resolved; } /** * Validates a path without resolving symlinks in the user-provided portion. * Use for paths that may not exist yet (creates, writes). * * Performs a lexical containment check only using path.resolve. */ export function guardPathUnsafe(userPath: string, sandboxDir: string): string { const resolved = path.resolve(sandboxDir, userPath); const sandboxAbs = path.resolve(sandboxDir); if (!resolved.startsWith(sandboxAbs + path.sep) && resolved !== sandboxAbs) { throw new SandboxEscapeError(userPath, sandboxDir, resolved); } return resolved; } export class SandboxEscapeError extends Error { constructor( public readonly userPath: string, public readonly sandboxDir: string, public readonly resolvedPath: string, ) { super( `Path escape attempt blocked: "${userPath}" resolves to "${resolvedPath}" which is outside sandbox "${sandboxDir}"`, ); this.name = 'SandboxEscapeError'; } }