refactor(#4653): make validatePath module-internal
Phase 3 of epic #4636, stage 3a. ADR-4650 decision 2: the engine stops being a public shape. The only exported containment surface is now assertWithinRoot / tryWithinRoot / requireSafePath, none of which can hand a caller a usable path when the answer is unsafe. The src/security.cts diff is one keyword. The engine body is byte-identical — the dangling-symlink existence-oracle closure, the ancestor canonicalization and the separator-aware boundary test are untouched, which is the whole constraint this phase operates under. WHAT THE TRANSLATION COST, AND THE RULE THAT KEPT IT AT ZERO. Roughly thirty test call sites consumed validatePath directly, including the two BLOCKER regressions that are this refactor's safety net. Translating them all to `tryWithinRoot(...) === null` would have looked correct and silently destroyed one of them: BLOCKER-1 asserts the rejection reason contains "unresolvable symbolic link", which is what distinguishes a DANGLING symlink from an ordinary escape. tryWithinRoot returns a bare null and cannot tell those apart, so that assertion would have degenerated into "it failed somehow" — and the existence-oracle closure could regress with the test still green. So the rule applied throughout is: an assertion on the rejection REASON goes through assertWithinRoot, whose throw carries the engine's message verbatim; only assertions on the boolean go through tryWithinRoot. Under that rule no coverage is lost. BLOCKER-1 still pins "unresolvable symbolic link" and BLOCKER-2 still pins the exact canonicalized resolved value. Three success-path tests came out BETTER than they went in. They previously carried `expected safe:true, got error: ${result.error}` as an assertion message; routing them through assertWithinRoot means an engine regression now surfaces the real reason in the failure itself rather than as a hand-built string. The two describe blocks named after validatePath are renamed — a block named for a symbol the module no longer exports is a false signpost. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -28,7 +28,7 @@ import path from 'node:path';
|
||||
* Validate that a file path resolves within an allowed base directory.
|
||||
* Prevents path traversal attacks via ../ sequences, symlinks, or absolute paths.
|
||||
*/
|
||||
export function validatePath(filePath: unknown, baseDir: unknown, opts: { allowAbsolute?: boolean } = {}): { safe: boolean; resolved: string; error?: string } {
|
||||
function validatePath(filePath: unknown, baseDir: unknown, opts: { allowAbsolute?: boolean } = {}): { safe: boolean; resolved: string; error?: string } {
|
||||
if (!filePath || typeof filePath !== 'string') {
|
||||
return { safe: false, resolved: '', error: 'Empty or invalid file path' };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user