abhigyanpatwari/GitNexus · error · BadRequestError

Invalid upload path

Error message

Invalid upload path

What it means

resolveContainedDest is the load-bearing path-traversal control for browser folder uploads in gitnexus's serve upload-ingest pipeline. This first throw rejects a manifest webkitRelativePath that is not a non-empty string: a missing/undefined manifest entry, a number, null, or ''. Every file part's declared path passes through here before any filesystem write, so malformed values never reach disk.

Source

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

export interface IngestResult {
  /** Absolute path to the populated staging directory (realpath-canonical). */
  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) {

View on GitHub (pinned to aac7515d2a)

Solutions

  1. In your uploader, always send file.webkitRelativePath || file.name for every file part so the manifest path is a non-empty string
  2. Validate the manifest client-side before POSTing: every entry must be a string of length > 0
  3. If you hit this as an API consumer, treat HTTP 400 with 'Invalid upload path' as a client bug in manifest construction, not a server fault

Example fix

// before — client manifest omits path for root files
manifest.files.forEach(f => entries.push({ path: f.webkitRelativePath })); // undefined for some picks
// after
manifest.files.forEach(f => entries.push({ path: f.webkitRelativePath || f.name }));
Defensive patterns

Strategy: type-guard

Validate before calling

// Client-side: build a manifest that can never contain empty/non-string paths
const entries = files.map((f) => ({
  path: (f.webkitRelativePath || f.name) as string, // always non-empty string
}));

Type guard

function isNonEmptyRelativePath(rel: unknown): rel is string {
  return (
    typeof rel === 'string' &&
    rel.length > 0 &&
    !rel.startsWith('/') &&
    !rel.includes('\\') &&
    !rel.includes('\u0000')
  );
}

Prevention

When it happens

Trigger: POST multipart upload to the serve ingest endpoint where a file part's path in the JSON manifest is absent, '', null, a number, or any non-string; a hand-rolled uploader client that omits webkitRelativePath for root-level files instead of sending just the filename; manifest deserialization producing undefined for skipped keys.

Common situations: Custom upload clients not using the browser File.webkitRelativePath API (drag-drop scripts, curl-based tests); older browsers/polyfills where webkitRelativePath is undefined; a manifest built from Object.keys with a typo'd key so lookups yield undefined; API consumers testing the endpoint with fabricated manifests.

Related errors


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