abhigyanpatwari/GitNexus · error · BadRequestError

Upload path must not contain traversal segments

Error message

Upload path must not contain traversal segments

What it means

After splitting and NFC-normalizing each segment, resolveContainedDest rejects any segment equal to '.' or '..' with 'Upload path must not contain traversal segments'. This is the direct anti-traversal rule: even though a later resolve-then-contain check would also catch escapes, '.'/'..' segments are unambiguous traversal syntax and are refused explicitly, including post-normalization forms.

Source

Thrown at gitnexus/src/server/upload-ingest.ts:91

  // webkitRelativePath is always relative; a leading slash is absolute/hostile.
  if (rel.startsWith('/')) {
    throw new BadRequestError('Invalid upload path');
  }
  // Browsers emit forward slashes only; a NUL byte or backslash is hostile.
  if (rel.includes('\u0000') || rel.includes('\\')) {
    throw new BadRequestError('Invalid upload path');
  }
  const rawSegments = rel.split('/').filter((s) => s.length > 0);
  if (rawSegments.length === 0 || rawSegments.length > MAX_PATH_DEPTH) {
    throw new BadRequestError('Invalid upload path');
  }
  const segments: string[] = [];
  for (const seg of rawSegments) {
    // Normalize so NFC/NFD variants don't collide silently on case/unicode
    // -folding filesystems (macOS/Windows).
    const s = seg.normalize('NFC');
    if (s === '.' || s === '..') {
      throw new BadRequestError('Upload path must not contain traversal segments');
    }
    segments.push(s);
  }
  const dest = path.resolve(stageRoot, segments.join(path.sep));
  // Suffix path.sep so a sibling prefix (/sandbox-evil vs /sandbox) can't pass.
  const safePrefix = stageRoot.endsWith(path.sep) ? stageRoot : stageRoot + path.sep;
  if (dest !== stageRoot && !dest.startsWith(safePrefix)) {
    throw new BadRequestError('Upload path escapes the sandbox');
  }
  return dest;
}

interface DirState {
  dirCount: number;
  limits: IngestLimits;
}

/**

View on GitHub (pinned to aac7515d2a)

Solutions

  1. Send only paths as the browser reports them — webkitRelativePath never contains '..' because the picker roots at the chosen folder
  2. Strip traversal client-side if you compute paths: segments.filter(s => s && s !== '.' && s !== '..').join('/')
  3. In security tests, keep '../..' and 'a/./b' cases asserting HTTP 400 'Upload path must not contain traversal segments'

Example fix

// before — client computes paths relative to CWD
entry.path = path.relative(projectRoot, file.path); // may yield '../../downloads/x'
// after
entry.path = file.webkitRelativePath || file.name; // rooted at the picked folder
Defensive patterns

Strategy: type-guard

Validate before calling

// Strip traversal segments before sending
entry.path = entry.path
  .split('/')
  .filter((s) => s && s !== '.' && s !== '..')
  .join('/');

Type guard

function isTraversalFreeUploadPath(rel: string): boolean {
  return rel
    .split('/')
    .every((s) => s.normalize('NFC') !== '.' && s.normalize('NFC') !== '..');
}

Prevention

When it happens

Trigger: POST multipart ingest with manifest paths like '../../etc/cron.d/x', 'src/../../../secrets', or 'a/./b' — any entry that yields a '.' or '..' segment after splitting on '/'. NFC normalization runs first so unicode tricks cannot smuggle a dot-dot equivalent.

Common situations: Classic path-traversal attack on the upload endpoint; a client that preserves '../' prefixes from paths outside the picked folder; test suites asserting OWASP File Upload coverage; accidentally built manifests that include parent references from relative-path math.

Related errors


AI-assisted analysis of abhigyanpatwari/GitNexus@aac7515d2a (2026-08-20). Data as JSON: /api/errors/9a1ef3079470ab34. Report an issue: GitHub.