garrytan/gstack · error · Error
Path traversal sequences (..) are not allowed
Error message
Path traversal sequences (..) are not allowed
What it means
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.
Source
Thrown at browse/src/write-commands.ts:609
return `User agent set: ${ua}`;
}
case 'upload': {
const [selector, ...filePaths] = args;
if (!selector || filePaths.length === 0) throw new Error('Usage: browse upload <selector> <file1> [file2...]');
// Validate paths are within safe directories (same check as cookie-import)
for (const fp of filePaths) {
if (!fs.existsSync(fp)) throw new Error(`File not found: ${fp}`);
if (path.isAbsolute(fp)) {
let resolvedFp: string;
try { resolvedFp = fs.realpathSync(path.resolve(fp)); } catch (err: any) { if (err?.code !== 'ENOENT') throw err; resolvedFp = path.resolve(fp); }
if (!SAFE_DIRECTORIES.some(dir => isPathWithin(resolvedFp, dir))) {
throw new Error(`Path must be within: ${SAFE_DIRECTORIES.join(', ')}`);
}
}
if (path.normalize(fp).includes('..')) {
throw new Error('Path traversal sequences (..) are not allowed');
}
}
const resolved = await session.resolveRef(selector);
if ('locator' in resolved) {
await resolved.locator.setInputFiles(filePaths);
} else {
await target.locator(resolved.selector).setInputFiles(filePaths);
}
const fileInfo = filePaths.map(fp => {
const stat = fs.statSync(fp);
return `${path.basename(fp)} (${stat.size}B)`;
}).join(', ');
return `Uploaded: ${fileInfo}`;
}
case 'dialog-accept': {View on GitHub (pinned to 94993f7401)
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.
Example fix
// before
const fp = path.join(baseDir, userInput); // userInput = '../secret.png'
await runBrowseCommand(['upload', 'input[type=file]', fp]);
// after
const safe = path.resolve(baseDir, path.basename(userInput));
if (path.normalize(safe).includes('..')) throw new Error('rejected');
await runBrowseCommand(['upload', 'input[type=file]', safe]); Defensive patterns
Strategy: validation
Validate before calling
import path from 'path';
function ensureNoTraversal(fp: string): void {
if (path.normalize(fp).includes('..')) {
throw new Error(`Path traversal rejected: ${fp}`);
}
}
function safeJoin(base: string, userInput: string): string {
const cleaned = path.basename(userInput);
const joined = path.join(base, cleaned);
ensureNoTraversal(joined);
return joined;
} Type guard
function hasNoTraversal(fp: string): boolean {
return !path.normalize(fp).includes('..');
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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 `../`.
Related errors
- Path must be within: ${SAFE_DIRECTORIES.join(', ')}
- Path must be within: ${SAFE_DIRECTORIES.join(', ')}
- Invalid file path in stageSkill: "${relPath}".
- commitSkill: staged dir "${opts.stagedDir}" is a symlink — r
- commitSkill: destination "${dest}" escapes tier root.
AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12).
Data as JSON: /api/errors/6b6ffe43944ffbe6.
Report an issue: GitHub.