vercel/turborepo · warning

Blocked symlink: ${entry.path}

Error message

Blocked symlink: ${entry.path}

What it means

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.

Source

Thrown at packages/turbo-utils/src/examples.ts:385

        const pathParts = entry.path.split("/");
        const strippedPath = pathParts.slice(strip).join("/");

        if (!strippedPath) {
          entry.resume();
          return;
        }

        // Validate the path stays within the target directory (Zip Slip protection)
        // Pass pre-resolved root for performance
        if (!isPathSafe(root, strippedPath, resolvedRoot)) {
          error(`Blocked path traversal attempt: ${entry.path}`);
          entry.resume();
          return;
        }

        // Block symlinks and hard links to prevent symlink attacks
        if (entry.type && isLinkEntry(entry.type)) {
          warn(`Blocked symlink: ${entry.path}`);
          entry.resume();
          return;
        }

        const destPath = resolve(resolvedRoot, strippedPath);

        if (entry.type === "Directory") {
          if (!createdDirs.has(destPath)) {
            mkdirSync(destPath, { recursive: true });
            createdDirs.add(destPath);
          }
          entry.resume();
        } else if (entry.type === "File") {
          const dirPath = dirname(destPath);
          if (!createdDirs.has(dirPath)) {
            mkdirSync(dirPath, { recursive: true });
            createdDirs.add(dirPath);
          }

View on GitHub (pinned to f9245100cf)

Solutions

  1. 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).
  2. 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>`.
  3. 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.
  4. 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.

Example fix

# before: example repo has a symlink
examples/my-app/shared-config -> ../../shared/config

# after: commit the file so tarball extraction includes it
examples/my-app/shared-config   # regular file with the config contents
Defensive patterns

Strategy: validation

Validate before calling

// Before scaffolding, ask GitHub's tree API whether the example contains symlinks (mode 120000)
async function exampleHasSymlinks(example: string): Promise<boolean> {
  const res = await fetch(
    `https://api.github.com/repos/vercel/turborepo/git/trees/main?recursive=1`
  );
  const { tree } = (await res.json()) as { tree: Array<{ path: string; mode: string }> };
  return tree.some((e) => e.path.startsWith(`examples/${example}/`) && e.mode === "120000");
}

Type guard

// Mirror of the library's own guard, usable on tar entries you process yourself
import type { ReadEntry } from "tar";
function isLinkEntry(entryType: string): boolean {
  return entryType === "SymbolicLink" || entryType === "Link";
}
function isSafeEntry(entry: ReadEntry): boolean {
  return !isLinkEntry(entry.type);
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of vercel/turborepo@f9245100cf (2026-08-17). Data as JSON: /api/errors/6cab0b7d0bdd5abb. Report an issue: GitHub.