abhigyanpatwari/GitNexus · error · InvalidStoragePathError

Repository path must not contain a NUL character.

Error message

Repository path must not contain a NUL character.

What it means

resolveRepoPath rejects repository paths containing a NUL byte ('\0'), which POSIX/Windows filesystem APIs treat as a string terminator and which often signals path injection. The check runs before path.resolve so no filesystem access happens with the tainted value.

Solutions

  1. Strip or reject the offending input at the boundary: if (value.includes('\0')) fail with your own clear error.
  2. If the value came from a Buffer, decode with the correct encoding (buffer.toString('utf8')) and trim trailing NULs: value.replace(/\0+$/, '').
  3. Treat NUL-bearing input as hostile — log and reject rather than sanitize silently.

Example fix

// before
const repoPath = rawBuffer.toString('utf16le'); // leaves NUL padding
const canonical = await canonicalRepoPath(repoPath);
// after
const repoPath = rawBuffer.toString('utf16le').replace(/\0+$/g, '');
if (repoPath.includes('\0')) throw new Error('repoPath contains NUL bytes');
const canonical = await canonicalRepoPath(repoPath);
Defensive patterns

Strategy: validation

Validate before calling

if (typeof repoPath !== 'string' || repoPath.includes('\0')) {
  throw new Error('repoPath must be a string without NUL bytes');
}

Type guard

const isSafePathString = (v: unknown): v is string => typeof v === 'string' && !v.includes('\0');

Try / catch

try {
  return await resolvedRepoPath(repoPath);
} catch (e) {
  if (e instanceof InvalidStoragePathError && e.message.includes('NUL')) {
    throw new Error('repo path contained a NUL byte — check input encoding/source');
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling canonicalRepoPath/defaultStoragePath/resolvedRepoPath/requireDeletableStoragePath with a repo path string that embeds '\0' — typically from untrusted input, binary-corrupted config, or buffer-to-string conversions of padded bytes.

Common situations: Reading a path from an untrusted file or request parameter that was never sanitized; decoding a UTF-16/UTF-32 buffer with wrong encoding leaving NUL padding; fuzzed or malicious CLI input.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of abhigyanpatwari/GitNexus@ac9a4e9abd (2026-09-15). Data as JSON: /api/errors/8d2611d5ad508bd9. Report an issue: GitHub.

Appendix: source

Thrown at gitnexus/src/storage/storage-resolver.ts:178

const isRepositoryLocalStoragePath = (repoPath: string, storagePath: string): boolean =>
  samePath(comparablePath(defaultStoragePath(repoPath)), comparablePath(storagePath));

const isMissingFilesystemError = (error: unknown): boolean => {
  const code = (error as NodeJS.ErrnoException)?.code;
  return code === 'ENOENT' || code === 'ENOTDIR';
};

const filesystemErrorDetail = (error: unknown): string => {
  const code = (error as NodeJS.ErrnoException)?.code;
  return code ? `${code}: ${(error as Error)?.message ?? String(error)}` : String(error);
};

const resolveRepoPath = (value: string): string => {
  if (typeof value !== 'string' || value.length === 0) {
    throw new InvalidStoragePathError('Repository path must be non-empty.');
  }
  if (value.includes('\0')) {
    throw new InvalidStoragePathError('Repository path must not contain a NUL character.');
  }
  return path.resolve(value);
};

const validateAbsolutePath = (value: string, label: string): string => {
  if (typeof value !== 'string' || value.length === 0) {
    throw new InvalidStoragePathError(`${label} must be an absolute, non-empty path.`);
  }
  if (value.includes('\0')) {
    throw new InvalidStoragePathError(`${label} must not contain a NUL character.`);
  }
  if (!path.isAbsolute(value)) {
    throw new InvalidStoragePathError(`${label} must be an absolute path.`);
  }
  return path.resolve(value);
};

// Mirror registry lookup semantics without importing repo-manager and creating a cycle.

View on GitHub (pinned to ac9a4e9abd)