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
- Inspect the archive for symlink entries and remove them (e.g. `zip -d file.zip 'link*'` or repack without symlinks).
- Only extract archives from trusted sources; verify checksums/signatures before download.
- 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
- Only extract archives from trusted, checksum-verified sources.
- Scan archives for symlink entries before extraction.
- Extract untrusted archives into an isolated throwaway directory first.
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
- Invalid extension name: ${name}
- process.${name}() is not allowed in extension sandbox
- Cannot use process.umask() to change mask (read-only)
- Invalid extraction directory: ${current}
AI-assisted analysis of neoclide/coc.nvim@50e974d969 (2026-08-31).
Data as JSON: /api/errors/aaa0b348f6a7c93d.
Report an issue: GitHub.