Safeguard
Application Security•

CWE-59 Link Following: How Symlink and Shortcut Attacks Work

CWE-59 is what happens when code opens a file by name and the name turns out to be a link to something else. Causes, checks and fixes.

Safeguard Security Team
7 min read

You ask the file system for /var/log/app.log. It hands you the file. But what if app.log was swapped for a link that points at /etc/shadow, and your program is running with enough privilege to write there? CWE-59, Improper Link Resolution Before File Access, is that problem. MITRE describes it as a product that accesses a file by name without properly preventing the name from identifying a link or shortcut that resolves to an unintended resource.

It is also known as link following. It affects programs that run with more privilege than the people who can write near the files they touch: services, installers, updaters, antivirus tools, editors that write recovery files, and anything that uses shared directories such as temporary folders. The record lists Windows and Unix as platforms, and the weakness is not language specific.

CWE-59 is the general class. The Unix specific case, CWE-61, covers symbolic links, and the Windows case covers shortcut files. MITRE notes that simple Windows shortcuts, sometimes called soft links, can be exploited remotely since a .LNK file can be placed where a program will follow it.

How it happens

The pattern is a check followed by a use, or just a use without a check.

  1. A privileged program is about to create, append to, or read a file in a directory the attacker can also write to.
  2. The attacker plants a link at the expected name, pointing at a file they care about.
  3. The program opens the name. The operating system follows the link. The program writes to, or reads from, the target, using its own privileges.

The record gives examples of the shape:

  • Some versions of Perl followed symbolic links when running with the -e option, allowing local users to overwrite arbitrary files via a symlink attack (CVE-1999-1386).
  • A text editor followed symbolic links when creating a rescue copy during an abnormal exit, letting local users overwrite files of other users (CVE-2000-1178).
  • An antivirus update allowed local users to create or append to arbitrary files via a symlink attack on a logfile (CVE-2004-0217).
  • A symlink attack allowing local users to overwrite files (CVE-2003-0517).

The antivirus case is a good reminder. Security software often runs as an administrator and writes logs to predictable paths. A predictable path in a writable place is the invitation.

Here is a vulnerable pattern in Python:

# Vulnerable: predictable name in a shared directory, followed blindly
import os
def write_report(text):
    path = "/tmp/report.txt"
    with open(path, "w") as f:      # follows a link planted at /tmp/report.txt
        f.write(text)

If an attacker creates /tmp/report.txt as a link to a file the service can write, the service overwrites that file.

What an attacker gains

MITRE lists several impacts. The attacker may traverse the file system to unintended locations and read or modify files and directories, and may bypass a protection mechanism, since the check was done on the name, not on what it resolves to. On Windows, MITRE notes that remote exploitation of shortcut files can lead to executing unauthorized code or commands.

In plain terms: overwriting a file the attacker could not write directly, reading a file they could not read directly, and sometimes turning that into higher privilege or code execution.

How to find it

MITRE cites a SOAR source and lists several detection techniques: automated and manual static analysis of binary or bytecode, and dynamic analysis with automated or manual interpretation of results.

For a code review, a short checklist works.

  1. Find every place that opens, creates, appends to, deletes or changes permissions on a file by path.
  2. For each, ask whether any directory in that path is writable by a less privileged user.
  3. Look for predictable names in shared locations, such as fixed temp file names, logs and caches in world writable folders.
  4. Look for a check on a path followed later by a use of the same path. The gap between them is a race window.
  5. Search for APIs that follow links by default and check whether a no follow option exists for them.

Testing helps too. In a staging environment, plant a link at each expected file name that points at a harmless canary file, run the code path, and see whether the canary changes.

Click through it at your own pace, or open it full screen.

How to fix it

The one mitigation MITRE lists is least privilege and compartmentalization: follow the principle of least privilege when assigning access rights, because denying access to a file can prevent an attacker from replacing that file with a link to a sensitive file. Put protected areas under good compartmentalization so that they can be trusted.

Concretely:

  1. Do not write to shared, writable directories if you can avoid it. Keep logs, caches and state in a directory owned by the service account, with permissions that stop other users from creating entries.
  2. Use secure temporary file functions that create a file with an unpredictable name and fail if the name already exists.
  3. Open files with flags that refuse to follow links, where the platform offers them, and create new files in exclusive mode.
  4. Operate on the open file handle, not on the path. Check the handle with fstat after opening, not the path before.
  5. Drop privileges when touching user controlled locations. A process that acts as the user cannot do more than the user could.
  6. On Windows, treat shortcut files from untrusted sources as input, and do not resolve them in privileged code.
# Fixed: unpredictable name, exclusive create, no link following
import os, tempfile

def write_report(text):
    fd, path = tempfile.mkstemp(prefix="report-", suffix=".txt", dir="/var/lib/myapp")
    with os.fdopen(fd, "w") as f:
        f.write(text)
    return path
# Fixed: open a known path without following a final link (Unix)
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW
fd = os.open("/var/lib/myapp/state.json", flags, 0o600)

O_EXCL makes creation fail if the name exists, and O_NOFOLLOW makes the open fail if the last component is a link. Together they close the planted link trick for that file.

If the bug sits in a package you depend on, such as an updater or logging library, updating is the fix. You can check your dependency file for known issues with the free in-browser scan at gold.safeguard.sh/scan. No login is needed, the file stays on your machine and it covers up to 100 dependencies. Reachability analysis then tells you whether your code actually calls the affected function.

Quick check

Why does link following matter more for privileged programs?

The program uses its own privileges when it follows the link. An attacker who cannot write a file directly can get a privileged program to do it for them.

What is the mitigation MITRE lists?

Least privilege and compartmentalization. Denying access to a file or directory can stop an attacker replacing it with a link to a sensitive file.

Why prefer a file handle over a path?

A path can change between a check and a use. A handle refers to the file you actually opened, so checks on it stay valid.

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.