Today I Learned - Protected Symlinks

Table of Contents

Introduction

A long-standing class of security issues is the symlink-based time-of-check-time-of-use race, most commonly seen in world-writable directories like /tmp. The common method of exploitation of this flaw is to cross privilege boundaries when following a given symlink (i.e. a root process follows a symlink belonging to another user). For a likely incomplete list of hundreds of examples across the years, please see: http://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=/tmp. Source: Sysctl Explorer

The protected_symlinks setting within the Linux Kernel helps prevent TOCTOU (time-of-check-time-of-use) vulnerabilities in privileged processes. Without this protection, a malicious user could trick a privileged process (such as a root-owned service) into following a symlink and overwriting or reading sensitive files. 0xdf showcases the “problem” with protected symlinks in his writeup on the HackTheBox machine Bookworm.

Here is another example, showcasing how protected_symlinks literally breaks a vulnerability in Sudo:

A race condition exists whereby the invoking user could replace the temporary file with a symbolic link after the file has been edited but before sudoedit changes its owner back to the target user. Winning the race allows the user to set the owner of an arbitrary file to the target user. However, if the protected symlinks feature is supported by the kernel and /proc/sys/fs/protected_symlinks is set to 1 (the default on many systems), the attack will fail.

Case Study

As mentioned in the opening quote of this blog post, finding a relevant vulnerability is as simple as visiting the MITRE page and searching for CVEs with the keyword /tmp. During my (re-)search, I discovered CVE-2023-33865, where details were published on the Full Disclosure Mailing List:

CVE-2023-33865, a symlink vulnerability in /tmp/RenderDoc

As soon as librenderdoc.so is LD_PRELOADed into the application to be debugged, its library_loaded() function:

  • creates the directory /tmp/RenderDoc, or reuses it if it already exists, even if it does not belong to the user who runs RenderDoc (Alice, in this advisory);
  • opens (and possibly creates) a log file of the form /tmp/RenderDoc/RenderDoc_app_YYYY.MM.DD_hh.mm.ss.log, and writes to it in append mode.

Consequently, a local attacker can create /tmp/RenderDoc before Alice runs RenderDoc and can populate this directory with numerous symlinks (of the form /tmp/RenderDoc/RenderDoc_app_YYYY.MM.DD_hh.mm.ss.log) that point to an arbitrary file in the filesystem; when Alice runs RenderDoc, this file will be created (if it does not exist already) and written to, with Alice’s privileges.

The original post includes exploit code, but let’s walk through the process by recreating and refining the steps.

Proof of concept

Verify whether protected symlinks are enabled:

root@symlink:~# cat /proc/sys/fs/protected_symlinks
1

Acting as Thomas, we first create a new directory named RenderDoc under /tmp. Inside this directory, we then create a symbolic link pointing to a file in Sarah’s home directory (/home/sarah/dfir.ch):

thomas@symlink:/tmp$ mkdir RenderDoc
thomas@symlink:/tmp$ cd RenderDoc/
thomas@symlink:/tmp/RenderDoc$ ln -s /home/sarah/dfir.ch RenderDoc_app_2025.02.18_08.00.00.log
thomas@symlink:/tmp/RenderDoc$ ls -l RenderDoc_app_2025.02.18_08.00.00.log 
lrwxrwxrwx 1 thomas thomas 19 Feb 18 06:43 RenderDoc_app_2025.02.18_08.00.00.log -> /home/sarah/dfir.ch

If Sarah now writes in the document RenderDoc_app_2025.02.18_08.00.00.log, the symbolic link is followed, and a file is created in her home directory:

sarah@symlink:~$ echo "DFIR rocks!" > /tmp/RenderDoc/RenderDoc_app_2025.02.18_08.00.00.log
sarah@symlink:~$ cat /home/sarah/dfir.ch 
DFIR rocks!
sarah@symlink:~$ 

Wait.. Haven’t you said it should prevent exactly this scenario?

Well.. yes:

sarah@symlink:~$ echo "DFIR rocks!" > /tmp/RenderDoc_app_2025.02.18_08.00.00.log
bash: /tmp/RenderDoc_app_2025.02.18_08.00.00.log: Permission denied

When set to “1” symlinks are permitted to be followed only when outside a sticky world-writable directory.. - and because the sub-directory RenderDoc will not inherit the stick-bit from the /tmp directory, the protection will not work here.

The sudoedit exploit discussed above will not work because the (malicious) symlink must be created inside /var/tmp/, and we will receive permission denied when we try to follow this symlink (same as in /tmp). So, in this scenario, the symlink protection is an effective protection.

Relevant Source Code

To determine if this behavior is by design, we look at the relevant code from the Linux repository on GitHub. First things first, if the system-wide symlink protection setting (protected_symlinks) is disabled, following the link is allowed immediately:

if (!sysctl_protected_symlinks)
    return 0;

If the owner of the symlink and the follower match, i.e. the same user, the action is allowed:

if (vfsuid_eq_kuid(vfsuid, current_fsuid()))
    return 0;

If the owner of the symlink and the parent directories matches, i.e. the same user, the action is allowed:

if (vfsuid_valid(nd->dir_vfsuid) && vfsuid_eq(nd->dir_vfsuid, vfsuid))
    return 0;

If the parent directory is not sticky and world-writable, the action is allowed. And here lies the problem. Because our created RenderDoc directory does not inherit the sticky-bit from the /tmp folder, the code does not see a (security) problem and follows the symlink, resulting in the created file in Sarah’s home directory.

if ((nd->dir_mode & (S_ISVTX|S_IWOTH)) != (S_ISVTX|S_IWOTH))
    return 0;

Conclusion

The protected_symlinks feature in the Linux kernel is an essential safeguard against TOCTOU race conditions and symlink-based privilege escalation.

However, as demonstrated, it is not foolproof—specific scenarios, such as symlinks within subdirectories that do not inherit the sticky bit, can still bypass this protection. Understanding how protected_symlinks works, including its limitations, is crucial for both system administrators and security researchers.

For enhanced system security, always ensure this feature is enabled and be aware of edge cases that may still pose risks. This blog post doesn’t cover all the protections available, but others follow a similar approach:

  • /proc/sys/fs/protected_hardlinks
  • /proc/sys/fs/protected_fifos
  • /proc/sys/fs/protected_regular