Secure Coding in C · intermediate · ~15 min

Symlink attacks and how to refuse them

Refuse to traverse symlinks in untrusted paths.

Overview

Symlink attacks redirect a privileged process to operate on a file the attacker controls. The kernel will follow the symlink unless you specifically tell it not to.

Why it matters

Setuid programs and root daemons that touch user-writable paths get owned by symlinks. O_NOFOLLOW, mkstemp, and realpath are the standard defences.

Core concepts

O_NOFOLLOW. Open fails with ELOOP if the LAST path component is a symlink. The intermediate dirs can still be symlinks though.

mkstemp. Creates a temp file with O_CREAT | O_EXCL | O_RDWR, randomising the suffix until it lands on a name no-one else owns. The atomic create prevents symlink races.

realpath + prefix. Resolve to canonical absolute path, then verify the result is under the allowed root directory.

O_DIRECTORY ensures the opened path is actually a directory; prevents 'opened a regular file we thought was a dir'.

Pentester mindset. Any privileged write to a user-writable path (/tmp, /var/spool/..., /dev/shm/...) is a symlink-attack candidate. Audit checklist: every open(... O_CREAT ...) on such a path must use O_EXCL or mkstemp.

Defensive coding habit. O_NOFOLLOW | O_CLOEXEC is the safe default for opening user-supplied paths.

Syntax notes

open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
int mkstemp(char *template);   /* template must end "XXXXXX" */

Lesson

A symlink (ln -s) is a file that names another path. Attackers point symlinks at files they want you to read or write — a classic privilege-escalation primitive when your code runs with elevated permissions. The defence is O_NOFOLLOW, careful temp-file creation, and realpath+prefix-check.

Code examples

int fd = open("/var/lib/svc/data", O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
if (fd < 0 && errno == ELOOP) {
    /* The last component is a symlink — refuse. */
}

Line by line

char template[] = "/tmp/cplat-XXXXXX";
int fd = mkstemp(template);   /* atomically creates + opens; symlink-safe */
if (fd < 0) { perror("mkstemp"); return -1; }
/* template now contains the actual filename used */

Common mistakes

  • Using tmpnam or hand-rolled temp file paths. Always mkstemp.

Debugging tips

ln -sf /etc/passwd /tmp/target then run your code against /tmp/target and verify ELOOP. If your code happily reads /etc/passwd, your defence isn't on.

Memory safety

mkstemp modifies the template in place; the template must be writable (NOT a string literal).

Real-world uses

Every Linux package manager, every CI/CD agent, every cron-spawned process that writes to /tmp.

Practice tasks

  1. Use mkstemp to create a temp file safely. 2. Open with O_NOFOLLOW and verify ELOOP on a symlink. 3. Combine realpath with a prefix check against /var/data/.

Summary

O_NOFOLLOW + mkstemp + realpath + prefix-check. Standard symlink defence in four moves.

Practice with these exercises