Secure Coding in C · intermediate · ~15 min
Refuse to traverse symlinks in untrusted paths.
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.
Setuid programs and root daemons that touch user-writable paths get owned by symlinks. O_NOFOLLOW, mkstemp, and realpath are the standard defences.
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.
open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
int mkstemp(char *template); /* template must end "XXXXXX" */
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.
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. */
}
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 */
tmpnam or hand-rolled temp file paths. Always mkstemp.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.
mkstemp modifies the template in place; the template must be writable (NOT a string literal).
Every Linux package manager, every CI/CD agent, every cron-spawned process that writes to /tmp.
/var/data/.O_NOFOLLOW + mkstemp + realpath + prefix-check. Standard symlink defence in four moves.