{"record":{"id":"6b6ffe43944ffbe6","repo":"garrytan/gstack","slug":"path-traversal-sequences-are-not-allowed","errorCode":null,"errorMessage":"Path traversal sequences (..) are not allowed","messagePattern":"Path traversal sequences \\(\\.\\.\\) are not allowed","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"browse/src/write-commands.ts","lineNumber":609,"sourceCode":"      return `User agent set: ${ua}`;\n    }\n\n    case 'upload': {\n      const [selector, ...filePaths] = args;\n      if (!selector || filePaths.length === 0) throw new Error('Usage: browse upload <selector> <file1> [file2...]');\n\n      // Validate paths are within safe directories (same check as cookie-import)\n      for (const fp of filePaths) {\n        if (!fs.existsSync(fp)) throw new Error(`File not found: ${fp}`);\n        if (path.isAbsolute(fp)) {\n          let resolvedFp: string;\n          try { resolvedFp = fs.realpathSync(path.resolve(fp)); } catch (err: any) { if (err?.code !== 'ENOENT') throw err; resolvedFp = path.resolve(fp); }\n          if (!SAFE_DIRECTORIES.some(dir => isPathWithin(resolvedFp, dir))) {\n            throw new Error(`Path must be within: ${SAFE_DIRECTORIES.join(', ')}`);\n          }\n        }\n        if (path.normalize(fp).includes('..')) {\n          throw new Error('Path traversal sequences (..) are not allowed');\n        }\n      }\n\n      const resolved = await session.resolveRef(selector);\n      if ('locator' in resolved) {\n        await resolved.locator.setInputFiles(filePaths);\n      } else {\n        await target.locator(resolved.selector).setInputFiles(filePaths);\n      }\n\n      const fileInfo = filePaths.map(fp => {\n        const stat = fs.statSync(fp);\n        return `${path.basename(fp)} (${stat.size}B)`;\n      }).join(', ');\n      return `Uploaded: ${fileInfo}`;\n    }\n\n    case 'dialog-accept': {","sourceCodeStart":591,"sourceCodeEnd":627,"githubUrl":"https://github.com/garrytan/gstack/blob/94993f74012782fd94416dd44b8314f6363a13a4/browse/src/write-commands.ts#L591-L627","documentation":"Thrown by `browse upload` when `path.normalize(fp).includes('..')` is true for any supplied file path. This catches relative paths that would escape their starting directory after normalization (e.g. `safe/../../../etc/passwd`). It is the second layer of path defense, applying to BOTH relative and absolute paths, complementing the safe-directory check which only runs for absolute paths.","triggerScenarios":"Passing `../../etc/passwd`; passing `/tmp/errlookup-0o5UJv/subdir/../escape.png` where the normalized form still resolves inside a safe dir but the raw token contains `..` (the check is on the normalized string, so `subdir/..` normalizes away and passes — only SURVIVING `..` segments trigger); a user-supplied filename that was not sanitized before being interpolated into a path.","commonSituations":"An agent concatenates a user-controlled path segment with a base dir without sanitizing; a filename field from a form contains `..`; the path was constructed via template string and a variable was empty, producing a leading `../`.","solutions":["Resolve the path to absolute FIRST and pass the resolved form: `path.resolve(baseDir, userInput)` — but ensure the resolved result still passes the safe-directory check.","Strip `..` from user-supplied path segments before interpolation, or reject any segment equal to `..`.","Use `path.basename(userFilename)` to take only the final component when you only needed a filename.","Prefer staging files under TEMP_DIR with generated names rather than forwarding user paths."],"exampleFix":"// before\nconst fp = path.join(baseDir, userInput); // userInput = '../secret.png'\nawait runBrowseCommand(['upload', 'input[type=file]', fp]);\n\n// after\nconst safe = path.resolve(baseDir, path.basename(userInput));\nif (path.normalize(safe).includes('..')) throw new Error('rejected');\nawait runBrowseCommand(['upload', 'input[type=file]', safe]);","handlingStrategy":"validation","validationCode":"import path from 'path';\nfunction ensureNoTraversal(fp: string): void {\n  if (path.normalize(fp).includes('..')) {\n    throw new Error(`Path traversal rejected: ${fp}`);\n  }\n}\nfunction safeJoin(base: string, userInput: string): string {\n  const cleaned = path.basename(userInput);\n  const joined = path.join(base, cleaned);\n  ensureNoTraversal(joined);\n  return joined;\n}","typeGuard":"function hasNoTraversal(fp: string): boolean {\n  return !path.normalize(fp).includes('..');\n}","tryCatchPattern":null,"preventionTips":["Sanitize user-supplied path segments with path.basename before joining.","Reject any segment equal to '..'.","Prefer resolving to absolute and confirming the result is inside a known base dir."],"tags":["security","path-traversal","filesystem","upload","safe-directories"],"backgroundTag":null,"analyzedSha":"94993f74012782fd94416dd44b8314f6363a13a4","analyzedAt":"2026-08-12T04:06:23.140Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}