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

  1. 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.
  2. Strip `..` from user-supplied path segments before interpolation, or reject any segment equal to `..`.
  3. Use `path.basename(userFilename)` to take only the final component when you only needed a filename.
  4. 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

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


AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12). Data as JSON: /api/errors/6b6ffe43944ffbe6. Report an issue: GitHub.