* Resolve symlinks for the deepest existing ancestor of a path. * The target file may not exist yet (we may be about to create it), so we * walk up the directory tree until realpath() succeeds, then rejoin the * non-existing tail onto the resolved ancestor. * * SECURITY (PSR M22186): path.resol
(absolutePath: string)
| 107 | * |
| 108 | */ |
| 109 | async function realpathDeepestExisting(absolutePath: string): Promise<string> { |
| 110 | const tail: string[] = [] |
| 111 | let current = absolutePath |
| 112 | // Walk up until realpath succeeds. ENOENT means this segment doesn't exist |
| 113 | // yet; pop it onto the tail and try the parent. ENOTDIR means a non-directory |
| 114 | // component sits in the middle of the path; pop and retry so we can realpath |
| 115 | // the ancestor to detect symlink escapes. |
| 116 | // Loop terminates when we reach the filesystem root (dirname('/') === '/'). |
| 117 | for ( |
| 118 | let parent = dirname(current); |
| 119 | current !== parent; |
| 120 | parent = dirname(current) |
| 121 | ) { |
| 122 | try { |
| 123 | const realCurrent = await realpath(current) |
| 124 | // Rejoin the non-existing tail in reverse order (deepest popped first) |
| 125 | return tail.length === 0 |
| 126 | ? realCurrent |
| 127 | : join(realCurrent, ...tail.reverse()) |
| 128 | } catch (e: unknown) { |
| 129 | const code = getErrnoCode(e) |
| 130 | if (code === 'ENOENT') { |
| 131 | // Could be truly non-existent (safe to walk up) OR a dangling symlink |
| 132 | // whose target doesn't exist. Dangling symlinks are an attack vector: |
| 133 | // writeFile would follow the link and create the target outside teamDir. |
| 134 | // lstat distinguishes: it succeeds for dangling symlinks (the link entry |
| 135 | // itself exists), fails with ENOENT for truly non-existent paths. |
| 136 | try { |
| 137 | const st = await lstat(current) |
| 138 | if (st.isSymbolicLink()) { |
| 139 | throw new PathTraversalError( |
| 140 | `Dangling symlink detected (target does not exist): "${current}"`, |
| 141 | ) |
| 142 | } |
| 143 | // lstat succeeded but isn't a symlink — ENOENT from realpath was |
| 144 | // caused by a dangling symlink in an ancestor. Walk up to find it. |
| 145 | } catch (lstatErr: unknown) { |
| 146 | if (lstatErr instanceof PathTraversalError) { |
| 147 | throw lstatErr |
| 148 | } |
| 149 | // lstat also failed (truly non-existent or inaccessible) — safe to walk up. |
| 150 | } |
| 151 | } else if (code === 'ELOOP') { |
| 152 | // Symlink loop — corrupted or malicious filesystem state. |
| 153 | throw new PathTraversalError( |
| 154 | `Symlink loop detected in path: "${current}"`, |
| 155 | ) |
| 156 | } else if (code !== 'ENOTDIR' && code !== 'ENAMETOOLONG') { |
| 157 | // EACCES, EIO, etc. — cannot verify containment. Fail closed by wrapping |
| 158 | // as PathTraversalError so the caller can skip this entry gracefully |
| 159 | // instead of aborting the entire batch. |
| 160 | throw new PathTraversalError( |
| 161 | `Cannot verify path containment (${code}): "${current}"`, |
| 162 | ) |
| 163 | } |
| 164 | tail.push(current.slice(parent.length + sep.length)) |
| 165 | current = parent |
| 166 | } |
no test coverage detected