garrytan/gstack · error · Error
Path must be within: ${SAFE_DIRECTORIES.join(', ')}
Error message
Path must be within: ${SAFE_DIRECTORIES.join(', ')} What it means
Thrown by `browse upload` when a file path is absolute AND its realpath-resolved location is not inside one of `SAFE_DIRECTORIES` (which is `[TEMP_DIR, process.cwd()]`, each resolved through `realpathSync` to defeat symlink tricks). This is a security guard: the upload command reads local files and pushes them into the browser, so it confines reads to the temp directory and the project working directory to prevent arbitrary file exfiltration from the host.
Source
Thrown at browse/src/write-commands.ts:605
const error = await bm.recreateContext();
if (error) {
return `User agent set to "${ua}" but: ${error}`;
}
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(', ');View on GitHub (pinned to 94993f7401)
Solutions
- Copy or stage the file under TEMP_DIR (`os.tmpdir()`) or the project cwd before uploading.
- If the file legitimately lives elsewhere on disk, start the browse server with cwd set to a parent directory that contains it (if that is acceptable for your threat model).
- Avoid symlinks that escape the safe directory — resolve them first and confirm the realpath target is inside a safe dir.
- Note relative paths skip this branch (only `path.isAbsolute(fp)` enters it) but still hit the `..` traversal check.
Example fix
// before
await runBrowseCommand(['upload', 'input[type=file]', '/Users/me/Secrets/key.pem']);
// after
import fs from 'fs';
import os from 'os';
import path from 'path';
const staged = path.join(os.tmpdir(), 'key.pem');
fs.copyFileSync('/Users/me/Secrets/key.pem', staged);
await runBrowseCommand(['upload', 'input[type=file]', staged]); Defensive patterns
Strategy: validation
Validate before calling
import fs from 'fs';
import path from 'path';
import os from 'os';
const SAFE = [os.tmpdir(), process.cwd()].map(d => { try { return fs.realpathSync(d); } catch { return d; } });
function isPathWithin(p: string, dir: string): boolean {
const rel = path.relative(dir, p);
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
}
function ensureWithinSafeDirs(fp: string): void {
const resolved = path.isAbsolute(fp) ? fs.realpathSync(path.resolve(fp)) : fp;
if (path.isAbsolute(fp) && !SAFE.some(d => isPathWithin(resolved, d))) {
throw new Error(`Path outside safe dirs: ${fp}`);
}
} Type guard
function isWithinSafeDirs(fp: string): boolean {
if (!path.isAbsolute(fp)) return true;
let resolved: string;
try { resolved = fs.realpathSync(path.resolve(fp)); } catch { resolved = path.resolve(fp); }
return SAFE.some(d => isPathWithin(resolved, d));
} Prevention
- Stage files under TEMP_DIR or the project cwd before upload.
- Resolve symlinks before checking — realpathSync follows them.
- If the file lives elsewhere, copy it into a safe directory first.
When it happens
Trigger: Passing `/etc/passwd`, `/Users/me/Secrets/key.pem`, or any path outside TEMP_DIR/cwd; passing a path inside a safe dir that is itself a symlink pointing outside (realpathSync follows it and the target fails the check); running the browse server with a cwd different from where the file lives.
Common situations: CI runs the browse server with cwd set to the repo root, but the file was staged in `/tmp/staging/` which is neither TEMP_DIR nor cwd; an agent downloaded a file to a custom cache dir outside the two allowed roots; the user expects `~` paths to work and they live under `/home` which is not allowed.
Related errors
- Path traversal sequences (..) are not allowed
- Invalid file path in stageSkill: "${relPath}".
- commitSkill: staged dir "${opts.stagedDir}" is a symlink — r
- commitSkill: destination "${dest}" escapes tier root.
- pdf: --from-file ${payloadPath} must be under ${SAFE_DIRECTO
AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12).
Data as JSON: /api/errors/c3863fa89028b6d8.
Report an issue: GitHub.