{"record":{"id":"3c9980d0e5503fcd","repo":"affaan-m/ECC","slug":"invalid-ecc-repo-root-missing-package-json-at-p","errorCode":null,"errorMessage":"Invalid ECC repo root: missing package.json at ${packageJsonPath}","messagePattern":"Invalid ECC repo root: missing package\\.json at (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"critical","filePath":"scripts/auto-update.js","lineNumber":144,"sourceCode":"    return path.dirname(record.state.target.root);\n  }\n\n  return repoRoot;\n}\n\n// Recognized ECC package names. A repo root is only trusted to run its\n// install-apply.js if its package.json identifies it as ECC — otherwise a\n// cloned project that ships a nested `evil/{package.json,scripts/install-apply.js}`\n// could drive auto-update into executing attacker code (GHSA-hfpv-w6mp-5g95).\nconst ECC_PACKAGE_NAMES = new Set(['ecc-universal', 'everything-claude-code']);\n\nfunction validateRepoRoot(repoRoot) {\n  const normalized = path.resolve(repoRoot);\n  const packageJsonPath = path.join(normalized, 'package.json');\n  const installApplyPath = path.join(normalized, 'scripts', 'install-apply.js');\n\n  if (!fs.existsSync(packageJsonPath)) {\n    throw new Error(`Invalid ECC repo root: missing package.json at ${packageJsonPath}`);\n  }\n\n  if (!fs.existsSync(installApplyPath)) {\n    throw new Error(`Invalid ECC repo root: missing install script at ${installApplyPath}`);\n  }\n\n  let pkgName = null;\n  try {\n    pkgName = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')).name;\n  } catch {\n    throw new Error(`Invalid ECC repo root: unreadable package.json at ${packageJsonPath}`);\n  }\n  if (!ECC_PACKAGE_NAMES.has(pkgName)) {\n    throw new Error(`Refusing to run install from untrusted repo root ${normalized}: package.json name '${pkgName}' is not an official ECC package.`);\n  }\n\n  return normalized;\n}","sourceCodeStart":126,"sourceCodeEnd":162,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/scripts/auto-update.js#L126-L162","documentation":"assertMemoryDirectorySafe applies the same symlink check that the root gets, but to a subdirectory of the vault (asserting it stays within the trusted root). If the directory exists and lstat reports a symbolic link, the library refuses to traverse it. This blocks symlink-based escapes from inside the vault (e.g. an attacker planting 'project -> /etc' inside the memory directory).","triggerScenarios":"Calling saveMemory or directory-listing paths when a scope subdirectory (e.g. the per-scope memory directory under the root) is a symlink. Triggered by directories/saveMemory callers whenever the checked directory path exists as a link.","commonSituations":"A user symlinked a subfolder of the vault to shared storage; a compromised process planted a symlink inside the vault; restoring from an archive that preserved symlinks; a package manager or tool replaced a vault subdirectory with a link during an update.","solutions":["Replace the symlinked directory with a real directory and copy/move its contents in place.","Audit the vault tree for unexpected links (`find ~/.ecc-memory -type l`) and remove any you did not create.","Point the whole scope root config at the real location if you intentionally keep the data elsewhere.","Re-check directory contents after any restore/sync operation that may have reintroduced symlinks."],"exampleFix":"// before (shell)\nln -s /shared/notes ~/.ecc-memory/project/notes\n\n// after (shell)\nrm ~/.ecc-memory/project/notes && mkdir ~/.ecc-memory/project/notes && cp -rL /shared/notes/. ~/.ecc-memory/project/notes/","handlingStrategy":"validation","validationCode":"const fs = require('fs');\nfunction assertNoSymlinkedSubdirs(vaultRoot) {\n  for (const entry of fs.readdirSync(vaultRoot)) {\n    const p = `${vaultRoot}/${entry}`;\n    if (fs.lstatSync(p).isSymbolicLink()) {\n      throw new Error(`Symlinked directory inside vault: ${p}. Replace with a real directory.`);\n    }\n  }\n}","typeGuard":"const isRealDirectory = (p) => { try { const st = fs.lstatSync(p); return st.isDirectory() && !st.isSymbolicLink(); } catch { return false; } };","tryCatchPattern":"try {\n  saveMemory(scope, doc);\n} catch (err) {\n  if (err.message.includes('symlink directory')) {\n    console.error(`Security: replace the symlinked vault subdirectory with a real one: ${err.message}`);\n  } else throw err;\n}","preventionTips":["Audit vault subdirectories for symlinks after restores, archive extractions, or sync operations.","Extract vault archives with symlink preservation disabled.","Restrict write access to the vault directory so untrusted processes cannot plant links.","Use `find <vault> -type l` in a scheduled check and alert on any result."],"tags":["security","symlink","memory-vault","path-traversal"],"backgroundTag":"path-traversal-blocked","analyzedSha":"8321021c54d670126ce3b2969d5deb880b4b0c2a","analyzedAt":"2026-09-16T10:08:13.343Z","contentChangedAt":"2026-09-16T10:08:13.343Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}