( filePath: string, )
| 226 | * directory via .. segments, or escapes via a symlink (PSR M22186). |
| 227 | */ |
| 228 | export async function validateTeamMemWritePath( |
| 229 | filePath: string, |
| 230 | ): Promise<string> { |
| 231 | if (filePath.includes('\0')) { |
| 232 | throw new PathTraversalError(`Null byte in path: "${filePath}"`) |
| 233 | } |
| 234 | // First pass: normalize .. segments and check string-level containment. |
| 235 | // This is a fast rejection for obvious traversal attempts before we touch |
| 236 | // the filesystem. |
| 237 | const resolvedPath = resolve(filePath) |
| 238 | const teamDir = getTeamMemPath() |
| 239 | // Prefix attack protection: teamDir already ends with sep (from getTeamMemPath), |
| 240 | // so "team-evil/" won't match "team/" |
| 241 | if (!resolvedPath.startsWith(teamDir)) { |
| 242 | throw new PathTraversalError( |
| 243 | `Path escapes team memory directory: "${filePath}"`, |
| 244 | ) |
| 245 | } |
| 246 | // Second pass: resolve symlinks on the deepest existing ancestor and verify |
| 247 | // the real path is still within the real team dir. This catches symlink-based |
| 248 | // escapes that path.resolve() alone cannot detect. |
| 249 | const realPath = await realpathDeepestExisting(resolvedPath) |
| 250 | if (!(await isRealPathWithinTeamDir(realPath))) { |
| 251 | throw new PathTraversalError( |
| 252 | `Path escapes team memory directory via symlink: "${filePath}"`, |
| 253 | ) |
| 254 | } |
| 255 | return resolvedPath |
| 256 | } |
| 257 | |
| 258 | /** |
| 259 | * Validate a relative path key from the server against the team memory directory. |
nothing calls this directly
no test coverage detected