{"record":{"id":"6cab0b7d0bdd5abb","repo":"vercel/turborepo","slug":"blocked-symlink-entry-path","errorCode":null,"errorMessage":"Blocked symlink: ${entry.path}","messagePattern":"Blocked symlink: (.+?)","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"packages/turbo-utils/src/examples.ts","lineNumber":385,"sourceCode":"        const pathParts = entry.path.split(\"/\");\n        const strippedPath = pathParts.slice(strip).join(\"/\");\n\n        if (!strippedPath) {\n          entry.resume();\n          return;\n        }\n\n        // Validate the path stays within the target directory (Zip Slip protection)\n        // Pass pre-resolved root for performance\n        if (!isPathSafe(root, strippedPath, resolvedRoot)) {\n          error(`Blocked path traversal attempt: ${entry.path}`);\n          entry.resume();\n          return;\n        }\n\n        // Block symlinks and hard links to prevent symlink attacks\n        if (entry.type && isLinkEntry(entry.type)) {\n          warn(`Blocked symlink: ${entry.path}`);\n          entry.resume();\n          return;\n        }\n\n        const destPath = resolve(resolvedRoot, strippedPath);\n\n        if (entry.type === \"Directory\") {\n          if (!createdDirs.has(destPath)) {\n            mkdirSync(destPath, { recursive: true });\n            createdDirs.add(destPath);\n          }\n          entry.resume();\n        } else if (entry.type === \"File\") {\n          const dirPath = dirname(destPath);\n          if (!createdDirs.has(dirPath)) {\n            mkdirSync(dirPath, { recursive: true });\n            createdDirs.add(dirPath);\n          }","sourceCodeStart":367,"sourceCodeEnd":403,"githubUrl":"https://github.com/vercel/turborepo/blob/f9245100cf0d31d96628804ead485f6bf226e55a/packages/turbo-utils/src/examples.ts#L367-L403","documentation":"During streaming tarball extraction (streamingExtract for `create-turbo --example`), every tar entry is checked: after Zip-Slip path validation, entries whose type is a symlink or hard link (isLinkEntry) are refused. This blocks the classic tar-symlink attack where a link entry points outside the extraction root and a later entry writes through it. The entry is skipped (entry.resume()) and extraction continues; the cost is that any legitimately symlinked file in the example will be missing from the generated project.","triggerScenarios":"The codeload.github.com tarball for vercel/turborepo (main) contains an entry of type 'SymbolicLink' or 'Link' inside examples/<name>/ that passes the path-safety and filter checks — i.e. the example's directory in the repo actually contains a git symlink — so the guard fires and the file is not materialized.","commonSituations":"A Turborepo example adds a symlink for shared config or a yarn/npm workspace alias; after scaffolding, the created project fails or behaves oddly because the symlinked file is absent; malicious or proxy-substituted tarballs attempting symlink-based extraction attacks.","solutions":["Check whether the example really contains symlinks: browse github.com/vercel/turborepo/tree/main/examples/<name> or `git clone` the repo and run `git ls-tree -r examples/<name>` (mode 120000 = symlink).","If the symlink is essential to the example, scaffold manually via git and re-add the link yourself: `git clone --depth 1 --filter=blob:none --sparse https://github.com/vercel/turborepo.git && git sparse-checkout set examples/<name>`.","Commit the previously-symlinked content as a real file (or a postinstall script that recreates the link) in the example so the tarball path works for everyone.","If you did not expect any symlink, treat it as a supply-chain red flag: verify the codeload URL/SHA you are fetching and report it to the Turborepo maintainers."],"exampleFix":"# before: example repo has a symlink\nexamples/my-app/shared-config -> ../../shared/config\n\n# after: commit the file so tarball extraction includes it\nexamples/my-app/shared-config   # regular file with the config contents","handlingStrategy":"validation","validationCode":"// Before scaffolding, ask GitHub's tree API whether the example contains symlinks (mode 120000)\nasync function exampleHasSymlinks(example: string): Promise<boolean> {\n  const res = await fetch(\n    `https://api.github.com/repos/vercel/turborepo/git/trees/main?recursive=1`\n  );\n  const { tree } = (await res.json()) as { tree: Array<{ path: string; mode: string }> };\n  return tree.some((e) => e.path.startsWith(`examples/${example}/`) && e.mode === \"120000\");\n}","typeGuard":"// Mirror of the library's own guard, usable on tar entries you process yourself\nimport type { ReadEntry } from \"tar\";\nfunction isLinkEntry(entryType: string): boolean {\n  return entryType === \"SymbolicLink\" || entryType === \"Link\";\n}\nfunction isSafeEntry(entry: ReadEntry): boolean {\n  return !isLinkEntry(entry.type);\n}","tryCatchPattern":null,"preventionTips":["Don't commit symlinks inside examples/ of the upstream repo — ship real files or a postinstall script that recreates links.","After scaffolding with --example, diff the generated tree against github.com/vercel/turborepo/tree/main/examples/<name> to spot silently skipped link entries.","If you maintain your own tar serving, strip or materialize link entries so consumers of streamingExtract never hit the guard."],"tags":["security","tarball","extraction","symlink","create-turbo"],"backgroundTag":"tar-extraction-symlink-attack","analyzedSha":"f9245100cf0d31d96628804ead485f6bf226e55a","analyzedAt":"2026-08-17T10:46:15.696Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}