neoclide/coc.nvim · error

Refusing to extract through symbolic link: ${current}

Error message

Refusing to extract through symbolic link: ${current}

What it means

During archive extraction, ensureNoSymlink in src/model/download.ts walks each path component between the destination root and the target file. If any intermediate component is a symbolic link, extraction is aborted with this error. This is a zip-slip / symlink-traversal security guard preventing archives from writing files outside the destination via planted symlinks.

Source

Thrown at src/model/download.ts:149

        if (!isDirectory) {
          let input = await openZipEntry(zipfile, entry)
          await writeZipEntry(input, target)
        }
        zipfile.readEntry()
      }, fail).catch(fail)
    })
    zipfile.readEntry()
  })
}

async function ensureNoSymlink(dest: string, target: string): Promise<void> {
  let relative = path.relative(dest, target)
  let current = dest
  for (let part of relative.split(path.sep).filter(Boolean)) {
    current = path.join(current, part)
    try {
      let stat = await fs.promises.lstat(current)
      if (stat.isSymbolicLink()) throw new Error(`Refusing to extract through symbolic link: ${current}`)
      if (!stat.isDirectory()) throw new Error(`Invalid extraction directory: ${current}`)
    } catch (e) {
      if ((e as NodeJS.ErrnoException).code !== 'ENOENT') throw e
    }
  }
}

async function writeZipEntry(input: NodeJS.ReadableStream, target: string): Promise<void> {
  let flags = fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC
  if (typeof fs.constants.O_NOFOLLOW === 'number') flags |= fs.constants.O_NOFOLLOW
  let handle = await fs.promises.open(target, flags, 0o666)
  try {
    await pipeline(input, handle.createWriteStream())
  } finally {
    await handle.close().catch(() => undefined)
  }
}

View on GitHub (pinned to 50e974d969)

Solutions

  1. Inspect the archive for symlink entries and remove them (e.g. `zip -d file.zip 'link*'` or repack without symlinks).
  2. Only extract archives from trusted sources; verify checksums/signatures before download.
  3. If you legitimately need symlinks, extract to a location where the link targets are inside dest, or extract manually with symlinks resolved to real copies.

Example fix

// before
download({ url, dest: '/opt/app', extract: true }) // archive contains symlink entries
// after
// repack archive without symlinks, then
download({ url: sanitizedUrl, dest: '/opt/app', extract: true })
Defensive patterns

Strategy: try-catch

Validate before calling

import { execSync } from 'child_process'
// pre-check archive for symlink entries (zip example):
const out = execSync(`unzip -Z1 -v ${archive}`).toString()
if (/symlink/i.test(out)) throw new Error('archive contains symlinks; refusing to extract')

Try / catch

try {
  await download({ url, dest, extract: true })
} catch (e) {
  if (String(e.message).startsWith('Refusing to extract through symbolic link')) {
    console.error('Archive contains symlinks; extract to a throwaway dir or reject the source')
  } else throw e
}

Prevention

When it happens

Trigger: Extracting an archive (unzipFile) that contains a symlink entry (e.g. 'link' -> '/etc') followed by a file under that symlink ('link/evil.txt'); the lstat of an intermediate component detects a symlink.

Common situations: Malicious or tampered archive downloads; archives produced on systems using symlinks (macOS frameworks, node_modules packed with symlinks); downloading untrusted binaries via the download API.

Related errors


AI-assisted analysis of neoclide/coc.nvim@50e974d969 (2026-08-31). Data as JSON: /api/errors/aaa0b348f6a7c93d. Report an issue: GitHub.