{"record":{"id":"d1f8939577def11e","repo":"abhigyanpatwari/GitNexus","slug":"storage-path-escaped-its-parent-directory","errorCode":null,"errorMessage":"Storage path escaped its parent directory.","messagePattern":"Storage path escaped its parent directory\\.","errorType":"validation","errorClass":"InvalidStoragePathError","httpStatus":null,"severity":"critical","filePath":"gitnexus/src/storage/storage-resolver.ts","lineNumber":262,"sourceCode":"};\n\nexport const defaultStoragePath = (repoPath: string): string =>\n  path.join(resolveRepoPath(repoPath), GITNEXUS_DIR);\n\nexport const validateConfiguredStoragePath = (value: string): string => {\n  const resolved = validateAbsolutePath(value, 'Storage path');\n  const parent = path.dirname(resolved);\n  const base = path.basename(resolved);\n  if (base.length === 0) {\n    throw new InvalidStoragePathError('Storage path must not be a filesystem root.');\n  }\n  // Rebuild through parent + basename and apply the path.relative idiom\n  // CodeQL's js/path-injection sanitizer recognizes. The reconstructed path\n  // is what callers pass to filesystem APIs.\n  const inspected = path.resolve(parent, base);\n  const rel = path.relative(parent, inspected);\n  if (rel.startsWith('..') || path.isAbsolute(rel)) {\n    throw new InvalidStoragePathError('Storage path escaped its parent directory.');\n  }\n  return inspected;\n};\n\n/** Resolve one repository's isolated slot under an external storage root. */\nexport const storagePathFromRoot = (rootPath: string, repoPath: string): string => {\n  const root = validateAbsolutePath(rootPath, STORAGE_ROOT_ENV);\n  const storagePath = path.resolve(root, storageSlotName(repoPath));\n  const rel = path.relative(root, storagePath);\n  if (\n    rel === '' ||\n    rel.startsWith('..') ||\n    path.isAbsolute(rel) ||\n    !samePath(path.dirname(storagePath), root)\n  ) {\n    throw new InvalidStoragePathError(\n      `Resolved storage path must remain directly inside ${STORAGE_ROOT_ENV}.`,\n    );","sourceCodeStart":244,"sourceCodeEnd":280,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/ac9a4e9abd8fd3058c070b72c23402a4f887929a/gitnexus/src/storage/storage-resolver.ts#L244-L280","documentation":"validateConfiguredStoragePath computes path.relative(parent, inspected) and throws if the result starts with '..' or is absolute — the storage path resolved outside its parent directory. This is a path-traversal guard ensuring the configured path cannot climb out (via '..' segments or symlink-style tricks) of the directory it is supposed to be contained in.","triggerScenarios":"Passing a storage path containing '..' segments that escape the parent (e.g. /data/repos/../elsewhere) to configuredStoragePath, registeredStoragePath, resolved/resolvedStoragePath, or readOwnershipMetadata.","commonSituations":"Untrusted storage paths from HTTP/CLI concatenated without normalization; config values assembled from user input; overly clever templating inserting '..' segments.","solutions":["Normalize/verify before the call: ensure path.resolve(value) stays within the intended base via path.relative(base, path.resolve(value)).startsWith('..') === false.","Reject user-supplied '..' segments at the boundary instead of resolving them.","Use storagePathFromRoot(root, repoPath) to derive slots from a fixed root rather than accepting free-form paths."],"exampleFix":"// before\nconfiguredStoragePath(path.join(base, userInput)); // userInput = '../other'\n// after\nconst candidate = path.resolve(base, userInput);\nif (path.relative(base, candidate).startsWith('..') || path.isAbsolute(path.relative(base, candidate))) {\n  throw new Error('storage path escapes base directory');\n}\nconfiguredStoragePath(candidate);","handlingStrategy":"validation","validationCode":"const candidate = path.resolve(value);\nconst rel = path.relative(intendedBase, candidate);\nif (rel.startsWith('..') || path.isAbsolute(rel)) {\n  throw new Error(`storage path escapes base directory: ${value}`);\n}","typeGuard":"const staysWithin = (base: string, target: string): boolean => {\n  const rel = path.relative(path.resolve(base), path.resolve(target));\n  return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);\n};","tryCatchPattern":"try {\n  const p = registeredStoragePath(repoPath);\n} catch (e) {\n  if (e instanceof InvalidStoragePathError && e.message.includes('escaped')) {\n    throw new Error(`traversal attempt in registered storage path for ${repoPath}; audit registry/config input`);\n  }\n  throw e;\n}","preventionTips":["Normalize user input with path.resolve before joining into storage paths.","Reject '..' segments from untrusted sources instead of resolving them.","Prefer storagePathFromRoot over free-form path construction for slots."],"tags":["path-traversal","security","storage"],"backgroundTag":"path-traversal-blocked","analyzedSha":"ac9a4e9abd8fd3058c070b72c23402a4f887929a","analyzedAt":"2026-09-15T23:29:44.066Z","contentChangedAt":"2026-09-15T23:29:44.066Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}