ASVS 5.0 V5.2.5 (level 3) says the application must not allow uploading compressed files that contain symbolic links, unless that is specifically required. If it is required, you must enforce an allowlist of the files that can be linked to.
Tar archives and some zip tools can store a symlink entry: a small record that says "this name points to that path". If your extraction code recreates it, the link now lives on your server.
Why it matters
A hostile archive can contain a link called report.txt that points to /etc/passwd or to your application's config file. Extraction creates the link. Later, some part of your system reads or serves report.txt and returns the content of the target. A common variant uses two steps: first a symlink to a directory, then a regular file written "inside" that directory, which actually lands outside the extraction folder.
This is related to zip slip (V5.3.3), but different. Zip slip abuses the names of entries. This one abuses the entry type.
How to implement it
Best option: refuse archives that contain links, and say so in your upload documentation.
Python tarfile example, checking every member before extracting anything:
import tarfile
def check_tar(path):
with tarfile.open(path) as tar:
for m in tar.getmembers():
if m.issym() or m.islnk():
raise ValueError(f'links not allowed: {m.name}')
if m.isdev() or m.isfifo():
raise ValueError(f'special files not allowed: {m.name}')
return tar.getmembers()
On newer Python versions you can also pass filter='data' to extractall, which blocks links that leave the destination, absolute paths and special files. Use it as an extra layer, not as the only check.
For zip files, symlinks are marked in the external attributes of an entry. Check the Unix mode bits:
import stat, zipfile
def zip_has_symlink(path):
with zipfile.ZipFile(path) as z:
return any(stat.S_ISLNK(i.external_attr >> 16) for i in z.infolist())
If you must support links, resolve each target against a fixed base directory, and accept it only if the resolved path stays inside and is on your allowlist. Extract as a low privilege user into a scratch directory, and never serve the extraction directory directly.
How to test it
Create archives with links and confirm each is refused.
mkdir t && cd t
ln -s /etc/passwd leak.txt
tar -cf ../link.tar leak.txt
zip -y ../link.zip leak.txt # -y stores the link itself
Upload both. The response must be a rejection and nothing should appear in the extraction directory. Then build the two step variant: a link to ../ followed by a file written through it. It must also be rejected. Check for hard links as well in tar. After a rejected upload, verify no temp files remain, and review that extraction runs without privileges to read sensitive files.
Common mistakes
- Using
extractallon tar files with no member checks. - Checking file names for
..and forgetting entry types. - Handling tar but not zip, or the other way around.
- Checking links before extraction but then following existing links in the destination directory.
- Running extraction as a user that can read secrets.
Quick check
Why is a symlink in an uploaded archive dangerous?
Extraction recreates it, and later reads or writes through it reach files outside the intended folder, such as config files or system files.
Sources
- OWASP Application Security Verification Standard, requirement V5.2.5 (level 3)
- OWASP File Upload Cheat Sheet
- Python tarfile extraction filters