abhigyanpatwari/GitNexus · error · BadRequestError

Upload path too long

Error message

Upload path too long

What it means

The length cap in resolveContainedDest: an upload manifest path longer than MAX_PATH_LENGTH (4096 chars) throws 'Upload path too long' before any write. The guard exists because some filesystems silently truncate or error on very long paths, and unbounded lengths are a cheap DoS/vector for the traversal checker; it runs after the type/emptiness check and before the leading-slash check.

Source

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

  stageRoot: string;
  fileCount: number;
  totalBytes: number;
  /** First path segment shared by the uploaded tree (the picked folder). */
  topLevelName: string;
}

/**
 * Resolve a client-provided relative path to an absolute destination PROVABLY
 * contained within `stageRoot`. Throws BadRequestError on any unsafe input.
 * This is the load-bearing path-traversal-on-write control; keep it pure and
 * unit-tested.
 */
export function resolveContainedDest(stageRoot: string, rel: unknown): string {
  if (typeof rel !== 'string' || rel.length === 0) {
    throw new BadRequestError('Invalid upload path');
  }
  if (rel.length > MAX_PATH_LENGTH) {
    throw new BadRequestError('Upload path too long');
  }
  // 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');

View on GitHub (pinned to aac7515d2a)

Solutions

  1. Fix the client to send the file's genuine webkitRelativePath (it is bounded by OS limits far below 4096)
  2. Look for cumulative string concatenation bugs in your manifest builder (path growing per level instead of being taken from the File object)
  3. Treat a 400 'Upload path too long' as evidence of a hostile or broken client — log the offending part's field name, not the path itself

Example fix

// before — cumulative join grows the path each level
rel = rel + '/' + part; // may exceed 4096 across deep trees
// after — take the browser-provided relative path once
rel = file.webkitRelativePath || file.name;
Defensive patterns

Strategy: validation

Validate before calling

const MAX_PATH_LENGTH = 4096; // mirror of the server cap
if (entry.path.length > MAX_PATH_LENGTH) {
  throw new Error(`manifest path too long (${entry.path.length} > ${MAX_PATH_LENGTH})`);
}

Type guard

function isBoundedUploadPath(rel: string): boolean {
  return rel.length > 0 && rel.length <= 4096;
}

Prevention

When it happens

Trigger: POST multipart ingest where a manifest webkitRelativePath exceeds 4096 characters — e.g. a fabricated manifest with a padded segment, a client bug concatenating paths cumulatively, or a hostile fuzzing payload probing the limits.

Common situations: Automated/fuzz clients generating pathological manifests; a client that joins folder names repeatedly (path += '/' + dir on each level); deeply nested picks on Windows where paths already approach limits; essentially never from a real browser folder picker, since OS path limits sit well below 4096.

Related errors


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