Safeguard
Application Security•

ASVS 5.0 V5.2.5: Block Symlinks in Uploaded Archives

A symlink inside a zip or tar can point at any file on your server. Reject archives with symlinks unless you truly need them, and allowlist targets if you do.

Safeguard Security Team
3 min read

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 extractall on 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

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.