An insecure temp file is a temporary file your code creates under a name another local user can predict, in a directory they can write to, such as /tmp. They plant a symlink or a file at that name first, or swap one in between your check and your open, and your process writes through it with its own privileges or leaves its data in a file they can read. Fix it at creation: let the system pick a random name and create the file with O_CREAT|O_EXCL and mode 0600, which is what mkstemp, mktemp(1), Python's tempfile, Java's Files.createTempFile, Go's os.CreateTemp and Rust's tempfile crate do. Keep related files in a private 0700 directory, keep using the handle instead of reopening by name, and treat the Linux fs.protected_* sysctls and systemd PrivateTmp= as a backstop, not the fix.
- A temp file race needs a name an attacker can predict (a PID, UID, date or constant) and a directory they can write to; take away either and a symlink attack has nothing to aim at.
- Checking first doesn't help:
access()oros.path.exists()runs before the open, and a dangling symlink passes the check, so in a test the program created the attacker's chosen file. O_CREAT|O_EXCLmakes creation atomic and fails on any existing name, including a dangling symlink;mkstempadds an unpredictable name and mode 0600.- Unsafe APIs still ship:
tmpnam,tempnamandmktemp(3)in C,tempfile.mktempin Python (no runtime warning in 3.14),File.createTempFilein Java (0644 under umask 022) and the Rusttempfilecrate'stempdir()(0755 under umask 022). - Give each job one private 0700 directory and use fixed names inside it; that also covers tools that insist on creating their own output files.
- The kernel default for all four
fs.protected_*sysctls is 0. systemd sets them to 1, but a Docker Desktop VM hadfs.protected_regular=0, so check every host.
| Setting | Value | Note | Source |
|---|---|---|---|
| C file creation | mkstemp: random suffix, O_RDWR|O_CREAT|O_EXCL, mode 0600 | POSIX.1-2008 requires 0600; glibc 2.06 and earlier used 0666. mkdtemp creates directories with 0700. | Linux man-pages mkstemp(3) and mkdtemp(3); POSIX.1-2024 |
| Shell mktemp | Files 0600; mktemp -d directories 0700 | mktemp -u only prints a name and creates nothing. | Tested: GNU coreutils 9.7, uutils coreutils 0.10.0 |
Python tempfile | mkstemp and NamedTemporaryFile 0600; mkdtemp and TemporaryDirectory 0700 | tempfile.mktemp is deprecated since 2.3 but emits no warning in 3.14.7. | Python 3.14 tempfile docs; tested |
| Java | Files.createTempFile rw-------; Files.createTempDirectory rwx------ | File.createTempFile produced rw-r--r-- under umask 022. | Tested on JDK 21.0.12 |
| Node.js | fs.mkdtemp creates a 0700 directory; open files inside it with flag 'wx' | 'wx' is O_CREAT|O_EXCL. Node.js has no API that creates a temporary file. | Node.js fs docs; tested on 26.3.0 |
| Go | os.CreateTemp 0o600 with O_EXCL; os.MkdirTemp 0o700 (before umask) | — | Go os package docs and source; tested on 1.27.2 |
| .NET | Directory.CreateTempSubdirectory 700; FileMode.CreateNew plus UnixCreateMode 0600 for files | Path.GetTempFileName created a 0600 file on macOS. Its 65,535-file limit applied to .NET Framework and is gone in .NET 8 and later. | Microsoft Learn; tested on .NET 10.0.7 |
| Rust tempfile 3.27.0 | Files 0o600; tempdir() 0o777 before umask, so 0o755 under umask 022 | Use Builder::permissions with 0o700 for private directories. | docs.rs tempfile Builder::permissions; tested |
fs.protected_symlinks, fs.protected_hardlinks | Kernel default 0; systemd's 50-default.conf sets 1 | A Docker Desktop VM (LinuxKit 6.12 kernel) had both at 1. | Linux fs/namei.c; systemd sysctl.d/50-default.conf; tested |
fs.protected_regular, fs.protected_fifos | Kernel default 0; systemd sets 1; 2 also covers group-writable sticky directories | The same Docker Desktop VM had both at 0, so an O_CREAT open without O_EXCL wrote into a file another user had planted. | Linux kernel sysctl/fs documentation; tested |
/tmp and /var/tmp | Mode 1777 (world-writable, sticky); systemd-tmpfiles ages /tmp entries after 10 days and /var/tmp after 30 | The sticky bit stops other users deleting or renaming your entries, not creating new names. | systemd tmpfiles.d/tmp.conf |
systemd PrivateTmp= | Off by default; yes gives the unit its own /tmp and /var/tmp | disconnected backs them with a new tmpfs; DynamicUser=yes implies it. | systemd.exec(5), systemd 262 |
- Search for
tmpnam,tempnam,mktemp(,mktemp -u,tempfile.mktemp,File.createTempFile, GuavaFiles.createTempDir,Path.GetTempFileNameand literals starting with/tmp/, and run CodeQLpy/insecure-temporary-file,java/local-temp-file-or-directory-information-disclosureand Bandit B108 and B306 in CI. - Run the job under
strace -f -e trace=openat,mkdir,mkdiratand confirm every file it creates in/tmpor$TMPDIRis opened withO_CREAT|O_EXCLorO_TMPFILEand mode 0600, and every directory with 0700. - Plant a dangling symlink and a symlink to a canary file at each name the old code used, run the job, and confirm the canary is unchanged and nothing appears at the dangling target.
- Send
SIGTERMto the job partway through and confirm its temporary directory is gone afterwards. - On every Linux host and node, run
sysctl fs.protected_symlinks fs.protected_hardlinks fs.protected_regular fs.protected_fifosand expect 1 or 2 for each; containers inherit the host kernel's values. - For each systemd service that writes temporary files, run
systemctl show -p PrivateTmp <unit>and expectPrivateTmp=yes, or a written reason why not.
- Applies to
- C and C++ (POSIX) mkstemp and mkdtemp in POSIX.1-2008 and later; O_TMPFILE since Linux 3.11; tested with clang on Ubuntu 26.04 and Apple clang 21
- Python 3.14.7 tested; delete_on_close and TemporaryDirectory(delete=) since 3.12
- Java JDK 21.0.12 tested; JUnit 4.13.1 and Guava 32.0.1 or later for the CVEs below
- Go 1.27.2 tested; os.CreateTemp and os.MkdirTemp since 1.16
Code that creates temporary or intermediate files on Unix-like systems (C/C++, shell, Python, Java, Node.js, Go, .NET, Rust). macOS and Windows give each user a private temp directory, but hard-coding /tmp on macOS still shares it.
- Part of
- Workflows: Fix a finding, Secure a feature
Tested fixes: the vulnerable code, the fix, a plausible fix that still fails, and the regression tests that tell them apart.
Learning goals and prerequisites
What you will learn
- Explain how a predictable name in a world-writable directory lets another local user redirect, read or pre-empt your writes.
- Replace check-then-open code with atomic creation (
O_EXCL,mkstempand the language equivalents) and owner-only permissions. - Prove the fix with a planted-symlink regression test,
straceand the Linuxprotected_*sysctls, and harden the host.
Before you start
Developers who write files from services, CLIs, build scripts and tests on shared hosts, and reviewers triaging temporary-file findings.
- Know Unix file permissions, umask, and symlinks versus hard links.
- Read C and one of Python, Java, JavaScript or Go.
A symlink attack on a root job's temporary file
- Predict the nameA local user reads the job's source and computes its path,
/tmp/nightly-report-0.csv, from root's UID. - Plant the linkThey create a dangling symlink at that name, pointing at a file that doesn't exist yet in a directory a privileged program reads.
- The check passesThe job calls
access(path, F_OK). The link's target is missing, so the check reports the name as free. - The open follows the link
fopen(path, "w")follows the symlink and creates the target with root's privileges and the job's data.
fs.protected_symlinks=1, step 4 fails with EACCES because root won't follow another user's link in a sticky world-writable directory. With mkstemp there is no guessable name to plant, and O_EXCL fails with EEXIST on any kernel.On Linux and most Unix systems, /tmp and /var/tmp are shared by every local account and have mode 1777: anyone can create entries, and the sticky bit only stops users from deleting or renaming entries they don't own. If your program writes to a name another account can compute, such as /tmp/report-$UID.csv, /tmp/backup.$$ or /tmp/app.lock, that account can create the name first. This is CWE-377, Insecure Temporary File.
What they plant decides the damage. The victim is often a root cron job, a CI agent or a service account, so the write happens with privileges the attacker lacks, or the data lands in a file the attacker can read. CWE-59 covers following the planted link, CWE-367 the gap between a check and the open, and CWE-379 creating files in a directory where other users can read or interfere with them.
| Planted at the predicted name | What a plain open with O_CREAT|O_TRUNC does | Result |
|---|---|---|
| A symlink to an existing file | Follows the link, truncates the target and writes to it | The target is overwritten with the victim's privileges |
| A dangling symlink to a path that doesn't exist yet | Follows the link and creates the target | A new file, owned by the victim, wherever the attacker chose |
| A hard link to a file the attacker can't write | Opens the shared inode | The same overwrite, on systems without fs.protected_hardlinks |
| A regular file the attacker owns, mode 0666 | Opens and truncates it; the attacker keeps ownership | The attacker reads everything written, or edits it before the victim reads it back |
| A directory with the predicted name | mkdir fails, or mkdir -p succeeds silently | Files the victim writes inside land in a directory the attacker controls |
Nothing yet; the attacker watches /tmp with inotify | Creates the file, then reopens it by name later | The attacker swaps the name between the two opens |
This program builds its file name from the UID and checks that nothing exists before it calls fopen(). Both choices fail. access() follows symlinks, so a dangling symlink reports the name as free, and fopen(path, "w") then creates the link's target. Even a correct check can't help, because anything can appear at the name between the check and the open: that window is the classic temp file race (CWE-367, CAPEC-29), and filling it with a symlink is CAPEC-27.
Tested with Apple clang 21 on macOS and clang on Ubuntu 26.04: a dangling symlink planted at the name made the program create the attacker's chosen file, and a symlink planted during the one-second WIDEN_RACE window made it overwrite an existing file. The file it creates is mode 0644 under the usual umask 022, so every local user can read it.
In the Ubuntu container, whose kernel had fs.protected_symlinks=1, root's fopen() of a symlink another user owned in /tmp failed with EACCES. With fs.protected_regular=0, a regular file pre-created by that user still got through: root wrote its data into a mode 0666 file the other user owned and could read.
/* report_vulnerable.c: predictable name in a shared directory (CWE-377) */
#include <stdio.h>
#include <unistd.h>
int main(void) {
char path[4096];
/* Guessable: anyone can compute a UID, PID or date. */
snprintf(path, sizeof path, "/tmp/nightly-report-%d.csv", (int)getuid());
/* Check-then-use (CWE-367): access() follows symlinks, and the name can be
replaced before fopen(), which also follows symlinks and uses 0666 & ~umask. */
if (access(path, F_OK) == 0) return 1;
FILE *f = fopen(path, "w");
if (!f) { perror("fopen"); return 1; }
fputs("customer_id,balance\n1001,250.00\n", f);
fclose(f);
return 0;
}mkstemp() replaces the trailing XXXXXX with random characters and opens the file as if by open(path, O_RDWR|O_CREAT|O_EXCL, S_IRUSR|S_IWUSR), as POSIX.1-2024 specifies. Creation either produces a new 0600 file that only you can open, or fails; there is no window. glibc 2.06 and earlier created the file with 0666, so code that still runs on very old systems should call umask(077) first.
Keep using the descriptor (wrap it with fdopen() or pass it to a child) instead of reopening the path, honor TMPDIR so per-user temp directories work, and unlink the file when you're done. For a set of related files, mkdtemp() creates a directory with mode 0700. Tested on macOS and Ubuntu 26.04: mkstemp() created 0600 files and mkdtemp() 0700 directories inside 1777 directories, and planted symlinks had nothing to hit because the program never opens a guessable name.
When a protocol fixes the name so mkstemp cannot randomize it, open with O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW and mode 0600: O_EXCL fails with EEXIST if anything is already there, including a symlink, and O_NOFOLLOW documents the intent.
/* report_fixed.c: mkstemp creates the file atomically with O_EXCL and mode 0600 */
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(void) {
const char *base = getenv("TMPDIR");
if (base == NULL || *base == '\0') base = "/tmp";
char path[4096];
if (snprintf(path, sizeof path, "%s/report-XXXXXX", base) >= (int)sizeof path) return 1;
int fd = mkstemp(path); /* replaces XXXXXX, opens O_RDWR|O_CREAT|O_EXCL, 0600 */
if (fd == -1) { fprintf(stderr, "mkstemp: %s\n", strerror(errno)); return 1; }
FILE *f = fdopen(fd, "w"); /* keep using the descriptor, never reopen by name */
if (!f) { close(fd); unlink(path); return 1; }
fputs("customer_id,balance\n1001,250.00\n", f);
if (fclose(f) != 0) { unlink(path); return 1; }
unlink(path); /* hand the descriptor onward first; remove when done */
return 0;
}Shell scripts keep predictable names longest (/tmp/backup.$$, /tmp/db-$(date +%F).sql, a constant lock). Use umask 077 and workdir=$(mktemp -d), and trap EXIT plus HUP, INT and TERM to remove it: dash (Debian's /bin/sh) skips the EXIT trap on a signal unless you also trap the signal.
Node.js has no temp-file creator, so path.join(os.tmpdir(), name) is a fixed name in a shared directory. Create a private directory with fsPromises.mkdtemp() (or mkdtempDisposable() under await using, Node.js 24.4+), then open files inside it with flag 'wx' (O_CREAT|O_EXCL) and mode 0o600.
In .NET, avoid Path.GetTempFileName() (it creates and reopens by name). Use Directory.CreateTempSubdirectory() (owner-only, 0700 on Unix, .NET 7+) and a FileStream with FileMode.CreateNew and UnixCreateMode 0600. In Rust, the tempfile crate's NamedTempFile and tempfile::tempdir are safe, but set directory permissions explicitly because tempdir() uses 0o777 before the umask.
In Go, os.MkdirTemp("", "report-*") makes a 0700 directory and os.CreateTemp opens a 0600 file with O_EXCL inside it; defer os.RemoveAll(dir) cleans up on every return.
The vulnerable Python version builds os.path.join(tempfile.gettempdir(), f"report-{os.getuid()}.csv"), deletes it if os.path.exists() finds it and opens it with "w", which has every flaw of the C program. So does tempfile.mktemp(), which returns a name only. It has been deprecated since Python 2.3, yet Python 3.14.7 still runs it without any warning, so rely on linters rather than runtime warnings to find it.
tempfile.TemporaryDirectory() calls mkdtemp() (random name, mode 0700) and removes the tree when the block exits. Inside it, open files with mode "x", which is O_CREAT|O_EXCL. For a single file, mkstemp() and NamedTemporaryFile() create it with O_EXCL and mode 0600.
To hand a named file to another program, use NamedTemporaryFile(delete_on_close=False) (Python 3.12+): you can close it so the other program can open it, including on Windows, and it is still deleted when the with block ends. A process killed with SIGKILL deletes nothing, so keep secrets out of temporary files that live long.
Keep a regression test that plants a dangling symlink and a symlink to a canary file at each name the old code used, points tempfile.tempdir at a world-writable directory, runs the job, and asserts the canary is untouched and nothing is left behind.
import os
import subprocess
import tempfile
def compress_report(rows: list[str]) -> bytes:
# mkdtemp under the hood: random name, mode 0700, removed with its contents on exit.
with tempfile.TemporaryDirectory(prefix="report-") as workdir:
path = os.path.join(workdir, "report.csv")
# "x" is O_CREAT|O_EXCL: fail rather than reuse anything already there.
with open(path, "x", encoding="utf-8") as f:
f.write("\n".join(rows) + "\n")
subprocess.run(["gzip", path], check=True)
with open(path + ".gz", "rb") as f:
return f.read()File.createTempFile() picks a random name and creates the file atomically, but with default permissions: on JDK 21.0.12 under umask 022 it produced rw-r--r--, readable by every local user. Files.createTempFile() produced rw------- and Files.createTempDirectory() produced rwx------. The Javadoc only says they may be more restrictive, so pass a PosixFilePermissions attribute when it matters.
Avoid the old directory recipe of File.createTempFile(), then delete(), then mkdir(). Between delete() and mkdir() another user who saw the name can create it, mkdir() returns false, and code that ignores the result writes into their directory; when the recipe does win, the directory is rwxr-xr-x. java.io.File has no createTempDir() method; the similarly named Guava Files.createTempDir() had the flaw described in the next section.
On Windows, a PosixFilePermissions attribute makes createFile throw UnsupportedOperationException; there %TEMP% is per-user and the file inherits its access control list. Tested on JDK 21.0.12 in a Linux container: the key file was -rw------- inside a rwx------ directory, and the directory was gone after the call. The same CWE-379 pattern hit widely used libraries: JUnit 4's TemporaryFolder (CVE-2020-15250, fixed in 4.13.1), Guava's Files.createTempDir (CVE-2020-8908, fixed in 32.0.0) and Guava's FileBackedOutputStream (CVE-2023-2976, fixed in 32.0.0), each creating world-readable entries in the shared temp directory.
// KeyedTool.java: hand a secret to an external tool through a private temp dir
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.util.*;
public final class KeyedTool {
private static final FileAttribute<Set<PosixFilePermission>> OWNER_ONLY =
PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-------"));
static Path writeKey(byte[] keyPem) throws IOException {
Path work = Files.createTempDirectory("sign-"); // random name, rwx------ on POSIX
// createFile is atomic and throws for any existing entry, symlinks included.
Path key = Files.createFile(work.resolve("key.pem"), OWNER_ONLY); // rw-------
Files.write(key, keyPem, StandardOpenOption.WRITE);
return key; // delete the tree in a finally block once the tool has run
}
}Four sysctls block the classic attacks in sticky world-writable directories such as /tmp. With fs.protected_symlinks=1, a process may follow a symlink there only if the link belongs to it or to the directory's owner. fs.protected_hardlinks=1 stops users from hard-linking files they don't own or can't read and write. fs.protected_regular=1 and fs.protected_fifos=1 refuse an O_CREAT open of a regular file or FIFO that someone else owns there, unless the directory's owner owns it; the value 2 extends that to group-writable sticky directories.
The kernel's built-in default for all four is 0; systemd's 50-default.conf sets all four to 1, so most systemd distributions ship them on. Don't assume: a Docker Desktop VM running a 6.12 LinuxKit kernel had symlinks and hardlinks at 1 but regular and fifos at 0. Containers report and obey the host kernel's values, so set them on the host or node.
They are a backstop. They don't stop a link planted by another process running as the same user, they don't apply outside sticky world-writable directories, and they exist only on Linux; on macOS /private/tmp is also 1777.
On Linux the strongest option is to build the file unnamed with open(dir, O_TMPFILE | O_WRONLY, 0600) and give it a name only once it is complete with linkat(..., AT_SYMLINK_FOLLOW), which fails with EEXIST rather than following or replacing anything at the target name; the file also vanishes if the process crashes before linking.
For services, PrivateTmp=yes in a systemd unit gives the process its own /tmp and /var/tmp, removed when it stops; for files that outlive one call, such as locks and sockets, RuntimeDirectory=<name> with RuntimeDirectoryMode=0700 creates a per-service /run/<name>, and UMask=0077 makes everything the service writes private. Per-user temp directories ($XDG_RUNTIME_DIR on Linux, /var/folders on macOS, %TEMP% on Windows) remove most cross-user attacks, so get the location from your language's temp API instead of hard-coding /tmp.
# /etc/sysctl.d/60-tmp-protections.conf
# Apply with: sudo sysctl --system
fs.protected_symlinks = 1
fs.protected_hardlinks = 1
fs.protected_regular = 1
fs.protected_fifos = 1Creation is the main risk, but the name stays a liability for as long as the file exists. A file in /tmp that your code closes and later reopens by path can be deleted and replaced in between: systemd-tmpfiles ages /tmp entries after 10 days and /var/tmp entries after 30, and once a cleaner removes your file, anyone can create the name again.
- Keep the descriptor or handle open and pass it on, or keep everything in a private 0700 directory where only you can create names.
- Publish finished files with
linkat()from anO_TMPFILEdescriptor, or withrename()from inside a directory you own, never by copying to a guessable path. - Delete with scoped helpers:
TemporaryDirectory,await using,defer os.RemoveAll,finally, drop guards and shell EXIT traps.SIGKILLand power loss skip all of them, so addPrivateTmp=or a startup sweep of your own private directory. - Keep secrets out of temporary files when a pipe or standard input will do; /domains/identity-crypto/secrets/secrets-lifecycle/ covers other ways to deliver them. If a tool insists on a key file, write it 0600 inside a 0700 directory and delete it in
finally. - Don't build temporary file names from user input such as an upload's original file name. Generate the name and keep the original only as metadata, as /domains/appsec/secure-features/secure-file-uploads/ describes.
- Search for the unsafe calls:
tmpnam,tempnamandmktemp(in C,mktemp -uand$$in shell file names,tempfile.mktempin Python,File.createTempFileand GuavaFiles.createTempDirin Java,Path.GetTempFileNamein .NET, and string literals that start with/tmp/or/var/tmp/. - CodeQL
py/insecure-temporary-file(tagged CWE-377) flagstempfile.mktempandos.tempnam;java/local-temp-file-or-directory-information-disclosureflagsFile.createTempFileand GuavaFiles.createTempDirwriting to the shared temp directory. - Bandit B306 (
mktemp_q) flagstempfile.mktemp, and B108 (hardcoded_tmp_directory) flags strings that start with/tmp,/var/tmpor/dev/shm. - At runtime,
strace -f -e trace=openat,mkdir,mkdiratshows each create; look forO_CREATwithoutO_EXCLorO_TMPFILEunder a shared directory, and for modes wider than 0600 and 0700. - In tests, plant a dangling symlink and a symlink to a canary at every name the old code used, as in the Python test above, and keep those tests after the fix so the bug can't come back.
- While a job runs,
find /tmp /var/tmp -user <service> -perm /o=rwxlists its files and directories that other users can reach.
Guided practice
You are reviewing a shared Linux build server (40 developer accounts plus CI and web services). For each snippet below, say why it is unsafe given the host sysctls and the observation, and write the fix.
Supplied scenario material
sysctl: protected_symlinks=1 protected_hardlinks=1 protected_regular=0 protected_fifos=0
T1 db-snapshot (root, dash): out=/tmp/db-snapshot-$(date +%F).sql; pg_dumpall > "$out"; gzip -f "$out"
T3 ArtifactSigner.java (ci): File w=File.createTempFile("sign-",""); w.delete(); w.mkdir(); Files.write(new File(w,"key.pem").toPath(), keyPem)
T4 upload-preview (Node, www-data): const p=path.join(os.tmpdir(), req.file.originalname); await fs.promises.writeFile(p, req.file.buffer)
T5 log-shipper (Go, root): os.OpenFile("/tmp/log-shipper.lock", os.O_CREATE|os.O_RDWR, 0o644); syscall.Flock(fd, LOCK_EX|LOCK_NB)
Observations: O1 /tmp has db-snapshot-<date>.sql -> /etc/ld.so.preload owned by another user. O2 /tmp/sign-* is drwxr-xr-x. O3 originalname "../../var/www/html/p.php" wrote /var/www/html/p.php. O4 log-shipper sometimes exits with "resource temporarily unavailable".1. Backup job (T1/O1)
Explain O1 and rewrite T1.
Expected result and reasoning
Predictable date name.
protected_symlinksblocks the symlink, butprotected_regular=0lets a planted 0666 file be truncated and filled with every database. Fix:umask 077; workdir=$(mktemp -d); trap 'rm -rf -- "$workdir"' EXIT INT TERM; work inside it;mvinto root-only/var/backups.2. Signing directory (T3/O2)
Explain the race and O2, then rewrite T3.
Expected result and reasoning
Between
delete()andmkdir()another user creates the directory first, so the key lands in theirs; when CI wins, O2 shows 0755 with a world-readable 0644 key. UseFiles.createTempDirectoryand createkey.pemwith arw-------attribute (the CVE-2020-15250 mistake).3. Upload preview (T4/O3)
Explain O3 and rewrite T4.
Expected result and reasoning
originalnameis attacker input;path.joinresolves its../out of/tmpinto the web root (CWE-22) and'w'follows symlinks. Write to a per-requestmkdtempdirectory under a generated name with flag'wx'.4. Lock file and host (T5/O4)
Explain O4, move T5's lock, and list host backstops.
Expected result and reasoning
A user pre-creates the lock; with
protected_regular=0root opens it and the attacker'sflockmakesLOCK_NBfail withEAGAIN(O4). UseRuntimeDirectory=log-shipper(0700). Host: setprotected_regular/fifos=1, run as systemd units withPrivateTmpandUMask=0077, add a planted-symlink CI test.
Paper exercise: it doesn't prove the fixes under real concurrency or on your kernel. Reproduce T1 and T5 with two local accounts in a disposable VM where you control the protected_* sysctls; a container reports and obeys its host kernel's values, so it can't show you a different setting.
Save a draft on this device to resume later. Automatic saving begins after you choose Save. Drafts are never sent by this application; use synthetic data.
No draft saved on this device yet.
Sources & further reading
The standards, specifications and documentation behind the guidance above. Each is the evidence for a claim on this page; consult it for detail beyond the defaults and versions listed here.
- CWE-377: Insecure Temporary File. CWE 4.20.
- SEI CERT C Coding Standard, FIO21-C: Do not create temporary files in shared directories
- POSIX.1-2024 (The Open Group Base Specifications Issue 8): mkdtemp, mkostemp, mkstemp
- Linux man-pages open(2): O_EXCL and O_TMPFILE
- Linux kernel documentation, /proc/sys/fs: protected_symlinks, protected_hardlinks, protected_regular, protected_fifos
Reference information
Behavior and permissions were checked on macOS and in Ubuntu 26.04 and Debian trixie containers on a 6.12 Linux kernel. The Linux protections depend on the distribution's sysctl settings, not the kernel's built-in defaults.
- SDLC phase
- DesignDevelopmentTesting
- Role
- Developers
- ASVS 5.0.0 requirements
- v5.0.0-15.4.2 · v5.0.0-5.3.2
- CAPEC attack patterns