{"record":{"id":"770c4166f1231143","repo":"abhigyanpatwari/GitNexus","slug":"trusted-cache-directory-env-must-not-traverse-s","errorCode":null,"errorMessage":"${TRUSTED_CACHE_DIRECTORY_ENV} must not traverse symbolic links or junctions","messagePattern":"(.+?) must not traverse symbolic links or junctions","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"gitnexus/src/core/analyzer-identity.ts","lineNumber":2087,"sourceCode":"  }\n  const normalized = path.normalize(configured);\n  let resolved: string;\n  try {\n    const link = lstatSync(normalized);\n    if (!link.isDirectory() || link.isSymbolicLink()) {\n      throw new Error('not a real directory');\n    }\n    resolved = realpathSync.native(normalized);\n  } catch {\n    throw new Error(\n      `${TRUSTED_CACHE_DIRECTORY_ENV} must name a pre-existing protected non-symlink directory`,\n    );\n  }\n  // Reject junctions/symlinked ancestors as well as a symlink final component.\n  // The environment variable is an explicit trust assertion, but its spelling\n  // must still bind exactly to the directory the cache will use.\n  if (!pathsEqual(path.resolve(normalized), resolved)) {\n    throw new Error(`${TRUSTED_CACHE_DIRECTORY_ENV} must not traverse symbolic links or junctions`);\n  }\n  return resolved;\n}\n\nfunction cacheDirectory(\n  options: AnalyzerIdentityResolveOptions,\n  packageRoot: string,\n  buildRoot: string,\n): string | null {\n  // An explicit location is a trusted operator/test override and therefore\n  // remains authoritative, including when the secure default is unavailable.\n  if (options.cacheDirectory) {\n    const explicit = path.resolve(options.cacheDirectory);\n    try {\n      // Create it before any build/dependency directory guards are captured.\n      // A cache nested immediately under a package root then changes that\n      // parent's directory state once, not after we persist the first entry.\n      mkdirSync(explicit, { recursive: true, mode: 0o700 });","sourceCodeStart":2069,"sourceCodeEnd":2105,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/52924ef12c2290ceee4612526a828ec4cdf2047f/gitnexus/src/core/analyzer-identity.ts#L2069-L2105","documentation":"The final trust check for GITNEXUS_ANALYZER_IDENTITY_CACHE_DIR compares the lexically resolved path (path.resolve of the normalized spelling) against the kernel-resolved realpath. If they differ, the configured spelling traverses a symlink or junction somewhere along the path (an ancestor component, even if the final component is real). Because the variable is an explicit trust assertion, its exact spelling must bind to the real directory the cache will use — otherwise a later symlink swap could redirect the cache undetected.","triggerScenarios":"GITNEXUS_ANALYZER_IDENTITY_CACHE_DIR is absolute and names an existing real directory, but some ancestor is a symlink: e.g. /tmp/gnx on macOS (/tmp -> /private/tmp), /mnt/data where /mnt/data is a bind/symlink, a Windows junction, or a Docker volume symlink. path.resolve(normalized) != realpathSync.native(normalized) throws.","commonSituations":"macOS /tmp and /var aliases, Linux symlinked home directories (/home/user -> /mnt/data/user), Windows junctions in CI agent layouts, and any deployment where convenience symlinks sit in the path prefix.","solutions":["Point the variable at the fully real path: run realpath <dir> and export the printed value.","On macOS, prefer $HOME-based paths or /private/tmp instead of /tmp.","Replace the symlinked ancestor usage with the underlying mount path in CI scripts.","Alternatively unset the variable and let GitNexus use its default secure cache location, which resolves this itself."],"exampleFix":"# before\nexport GITNEXUS_ANALYZER_IDENTITY_CACHE_DIR=/tmp/gnx-id-cache   # /tmp -> /private/tmp\n# ...must not traverse symbolic links or junctions\n\n# after\nmkdir -p \"$HOME/.cache/gnx-id-cache\"\nexport GITNEXUS_ANALYZER_IDENTITY_CACHE_DIR=\"$(realpath \"$HOME/.cache/gnx-id-cache\")\"","handlingStrategy":"validation","validationCode":"// Reject spellings that traverse symlinks before launching:\nimport { resolve, isAbsolute } from 'node:path';\nimport { realpathSync } from 'node:fs';\nfunction spellingIsRealPath(dir: string): boolean {\n  if (!isAbsolute(dir)) return false;\n  try {\n    return resolve(dir) === realpathSync(dir);\n  } catch {\n    return false;\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  spawnSync('gitnexus', ['analyze']);\n} catch (err) {\n  if (String((err as Error).message).includes('must not traverse symbolic links or junctions')) {\n    process.env.GITNEXUS_ANALYZER_IDENTITY_CACHE_DIR = realpathSync(process.env.GITNEXUS_ANALYZER_IDENTITY_CACHE_DIR!);\n    return spawnSync('gitnexus', ['analyze']);\n  }\n  throw err;\n}","preventionTips":["Always export the output of realpath for cache dir variables in scripts.","Avoid /tmp, /var, and symlinked home prefixes on macOS for trusted paths.","Document the real mount path in CI templates instead of convenient aliases.","If unsure, unset the env var and rely on the default cache location."],"tags":["analyzer-identity","cache","symlink","security","path-validation"],"backgroundTag":"symlink-path-traversal","analyzedSha":"52924ef12c2290ceee4612526a828ec4cdf2047f","analyzedAt":"2026-08-20T23:29:22.980Z","contentChangedAt":"2026-08-20T23:29:22.980Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}