@@ -2,13 +2,24 @@ import * as vscode from "vscode"
22import * as path from "path"
33import * as fs from "fs"
44
5+ /** Narrow an unknown error to a Node errno exception with the given `code`. */
6+ function isErrnoException ( err : unknown , code : string ) : boolean {
7+ return err instanceof Error && ( err as NodeJS . ErrnoException ) . code === code
8+ }
9+
510/**
611 * Resolves a path to its canonical form, following symlinks.
712 *
813 * If the path does not exist yet (e.g. a file that is about to be created), the
914 * realpath of the nearest existing ancestor is resolved and the remaining
1015 * segments are re-appended. This ensures a symlink anywhere along the path is
1116 * still followed, while paths that don't exist yet can still be evaluated.
17+ *
18+ * Only `ENOENT` (a not-yet-existing segment) triggers the walk-up. Any other
19+ * error — e.g. `EACCES` on a symlink whose target has restricted permissions —
20+ * is re-thrown rather than swallowed: silently walking up would mask the symlink
21+ * and could let an out-of-workspace target look "inside". Callers performing a
22+ * security check are expected to fail closed on a thrown error. See issue #169.
1223 */
1324function realPathOrNearest ( target : string ) : string {
1425 let current = path . resolve ( target )
@@ -19,7 +30,13 @@ function realPathOrNearest(target: string): string {
1930 try {
2031 const resolved = fs . realpathSync . native ( current )
2132 return trailing . length > 0 ? path . join ( resolved , ...trailing . reverse ( ) ) : resolved
22- } catch {
33+ } catch ( err ) {
34+ if ( ! isErrnoException ( err , "ENOENT" ) ) {
35+ // Non-ENOENT (e.g. EACCES): don't mask it with a walk-up — propagate so the
36+ // caller's security check can fail closed instead of falling through to the
37+ // lexical path.
38+ throw err
39+ }
2340 const parent = path . dirname ( current )
2441 if ( parent === current ) {
2542 // Reached the root without finding an existing path; fall back to the
@@ -47,12 +64,25 @@ export function isPathOutsideWorkspace(filePath: string): boolean {
4764 // workspace but points outside it is correctly treated as outside. Without
4865 // this, the out-of-workspace read protection was trivially bypassed by
4966 // symlinking to a file outside the workspace. See issue #169.
50- const absolutePath = realPathOrNearest ( filePath )
67+ let absolutePath : string
68+ try {
69+ absolutePath = realPathOrNearest ( filePath )
70+ } catch {
71+ // Could not safely resolve the target (e.g. EACCES on a symlink). Fail closed:
72+ // treat it as outside the workspace rather than risk a false "inside".
73+ return true
74+ }
5175
5276 // Check if the path is within any workspace folder
5377 return ! vscode . workspace . workspaceFolders . some ( ( folder ) => {
5478 // Resolve the workspace folder too, in case it is itself reached via a symlink.
55- const folderPath = realPathOrNearest ( folder . uri . fsPath )
79+ let folderPath : string
80+ try {
81+ folderPath = realPathOrNearest ( folder . uri . fsPath )
82+ } catch {
83+ // Can't resolve this folder safely; it can't be used to prove containment.
84+ return false
85+ }
5686 // Path is inside a workspace if it equals the workspace path or is a subfolder
5787 return absolutePath === folderPath || absolutePath . startsWith ( folderPath + path . sep )
5888 } )
0 commit comments