Skip to content

Commit 23a914d

Browse files
committed
fix(cue): reject Windows cross-drive rel + read canonical path
Two follow-up hardenings to the prompt_file containment check: - On Windows, path.relative returns an absolute path (drive-letter or UNC) when realPath has no common base with realRoot — that result does not start with `..`, so the previous check accepted it. Add path.isAbsolute(rel) to the rejection condition. - Read the canonicalized realPath instead of absPath so a TOCTOU symlink swap between the realpath check and the readFileSync call cannot redirect the read to an out-of-root target.
1 parent 7fe4317 commit 23a914d

1 file changed

Lines changed: 13 additions & 4 deletions

File tree

src/main/cue/config/cue-config-normalizer.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,20 +45,29 @@ function readPromptFile(projectRoot: string, promptFile: string): string | undef
4545
// differences (NFC vs NFD), and symlinks that could otherwise escape the
4646
// root without tripping a lowercase `startsWith` guard. `path.relative`
4747
// returns '' when the paths are equal (treated as inside — reading the
48-
// root directory as a file will simply fail downstream) and a path
49-
// starting with `..` when the target escapes the root.
48+
// root directory as a file will simply fail downstream), a `..`-prefixed
49+
// path for POSIX escapes, and an absolute path on Windows when `realPath`
50+
// lives on a different drive or UNC share (no common base) — so we reject
51+
// any absolute rel too.
52+
let canonicalPath: string;
5053
try {
5154
const realRoot = fs.realpathSync.native(normalizedRoot);
5255
const realPath = fs.realpathSync.native(absPath);
5356
const rel = path.relative(realRoot, realPath);
54-
if (rel !== '' && rel.split(path.sep)[0] === '..') {
57+
if (rel !== '' && (path.isAbsolute(rel) || rel.split(path.sep)[0] === '..')) {
5558
return undefined;
5659
}
60+
canonicalPath = realPath;
5761
} catch {
5862
return undefined;
5963
}
6064
try {
61-
return fs.readFileSync(absPath, 'utf-8');
65+
// Read the canonicalized path, not absPath. If `promptFile` was a symlink
66+
// that pointed inside the root at check time, reading `absPath` would
67+
// re-follow the symlink at read time — letting an attacker swap the
68+
// symlink's target between the check and the read. Reading `realPath`
69+
// pins us to the file we actually validated.
70+
return fs.readFileSync(canonicalPath, 'utf-8');
6271
} catch {
6372
return undefined;
6473
}

0 commit comments

Comments
 (0)