abhigyanpatwari/GitNexus · error · InvalidStoragePathError

Resolved storage path must remain directly inside

Error message

Resolved storage path must remain directly inside ${STORAGE_ROOT_ENV}.

What it means

storagePathFromRoot derives one repository's isolated slot under an external storage root and verifies the result is a single-level child: the relative path must be non-empty, not climb with '..', not absolute, and the slot's dirname must equal the root. Any violation throws InvalidStoragePathError 'Resolved storage path must remain directly inside GITNEXUS_STORAGE_ROOT'. This prevents a hostile repoPath from constructing slot paths deeper than or outside the configured root.

Solutions

  1. Ensure rootPath is itself validated (run validateConfiguredStoragePath/root on it) before deriving slots.
  2. Check the repoPath input: canonicalize it (path.resolve + realpath) so the slot name derivation is stable and single-level.
  3. Log the computed storagePath and root on failure; if the encoding scheme produces separators, pre-hash the repo path (e.g. slug + hash) before deriving the slot.

Example fix

// before
const slot = storagePathFromRoot(rawEnvRoot, remoteRepoUrl);
// after
const slot = storagePathFromRoot(validateConfiguredStoragePath(rawEnvRoot), path.resolve(realpathSync(repoDir)));
Defensive patterns

Strategy: validation

Validate before calling

const root = validateConfiguredStoragePath(rootPath);
const repoCanon = path.resolve(fs.realpathSync(repoPath));
const slot = storagePathFromRoot(root, repoCanon);
if (path.dirname(slot) !== root) throw new Error('slot must be a direct child of root');

Type guard

const isDirectChild = (root: string, slot: string): boolean =>
  path.dirname(path.resolve(slot)) === path.resolve(root);

Try / catch

try {
  return storagePathFromRoot(root, repoPath);
} catch (e) {
  if (e instanceof InvalidStoragePathError && e.message.includes('directly inside')) {
    throw new Error(`slot derivation left storage root — canonicalize repoPath and re-validate root`);
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling storagePathFromRoot(rootPath, repoPath) where the derived slot name (from the repo path) yields '' , '..' segments, or a nested path so that path.dirname(slot) !== root — e.g. a repoPath that encodes separators/slashes into the slot name.

Common situations: Unusual repository paths (URLs, remote refs, names containing slashes) hashed/encoded into slot names incorrectly; passing a root with trailing separator or a non-resolved root; upgrading between versions that changed slot-name encoding.

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/97770f43d5500f2c. Report an issue: GitHub.

Appendix: source

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

  const rel = path.relative(parent, inspected);
  if (rel.startsWith('..') || path.isAbsolute(rel)) {
    throw new InvalidStoragePathError('Storage path escaped its parent directory.');
  }
  return inspected;
};

/** Resolve one repository's isolated slot under an external storage root. */
export const storagePathFromRoot = (rootPath: string, repoPath: string): string => {
  const root = validateAbsolutePath(rootPath, STORAGE_ROOT_ENV);
  const storagePath = path.resolve(root, storageSlotName(repoPath));
  const rel = path.relative(root, storagePath);
  if (
    rel === '' ||
    rel.startsWith('..') ||
    path.isAbsolute(rel) ||
    !samePath(path.dirname(storagePath), root)
  ) {
    throw new InvalidStoragePathError(
      `Resolved storage path must remain directly inside ${STORAGE_ROOT_ENV}.`,
    );
  }
  return storagePath;
};

const configuredStoragePath = (): string | undefined => {
  const value = process.env[STORAGE_PATH_ENV];
  return value === undefined ? undefined : validateConfiguredStoragePath(value);
};

const configuredStorageRoot = (repoPath: string): string | undefined => {
  const value = process.env[STORAGE_ROOT_ENV];
  return value === undefined ? undefined : storagePathFromRoot(value, repoPath);
};

const registeredStoragePath = (repoPath: string): string | undefined => {
  let entries: unknown[];

View on GitHub (pinned to ac9a4e9abd)