* Check if the resolved realpath stays within the worktree boundary. * Prevents symlink escape attacks where a symlink points outside the worktree. * * @throws PathValidationError if realpath escapes worktree
( worktreePath: string, fullPath: string, )
| 176 | * @throws PathValidationError if realpath escapes worktree |
| 177 | */ |
| 178 | async function assertRealpathInWorktree( |
| 179 | worktreePath: string, |
| 180 | fullPath: string, |
| 181 | ): Promise<void> { |
| 182 | try { |
| 183 | const real = await realpath(fullPath); |
| 184 | const worktreeReal = await realpath(worktreePath); |
| 185 | |
| 186 | // Use path.relative for safer boundary checking |
| 187 | if (!isPathWithinWorktree(worktreeReal, real)) { |
| 188 | throw new PathValidationError( |
| 189 | "File is a symlink pointing outside the worktree", |
| 190 | "SYMLINK_ESCAPE", |
| 191 | ); |
| 192 | } |
| 193 | } catch (error) { |
| 194 | // If realpath fails with ENOENT, the target doesn't exist |
| 195 | // But the path itself might be a dangling symlink - check that first! |
| 196 | if (error instanceof Error && "code" in error && error.code === "ENOENT") { |
| 197 | await assertDanglingSymlinkSafe(worktreePath, fullPath); |
| 198 | return; |
| 199 | } |
| 200 | // Re-throw PathValidationError |
| 201 | if (error instanceof PathValidationError) { |
| 202 | throw error; |
| 203 | } |
| 204 | // Other errors (permission denied, etc.) - fail closed for security |
| 205 | throw new PathValidationError( |
| 206 | "Cannot validate file path", |
| 207 | "SYMLINK_ESCAPE", |
| 208 | ); |
| 209 | } |
| 210 | } |
| 211 | |
| 212 | /** |
| 213 | * Handle the ENOENT case: check if fullPath is a dangling symlink pointing outside |