---
key: "insecure-temp-files"
title: "Create temporary files safely: mkstemp, O_EXCL and private directories"
url: "https://softwaresecurity.com/domains/appsec/secure-coding/insecure-temp-files/"
api: "https://softwaresecurity.com/api/v1/articles/insecure-temp-files.json"
domain: "appsec"
topic: "Application security"
type: "Guide"
identifiers: ["CWE-377","CWE-379","CWE-59","CWE-367","A01:2025","V15.4","V15.4.2","V5.3.2","CAPEC-27","CAPEC-29"]
tags: ["File uploads and archives","Business logic and race conditions"]
stages: ["design","build","test"]
roles: ["developer"]
license: "CC-BY-4.0"
license_url: "https://creativecommons.org/licenses/by/4.0/"
code_license: "MIT"
---

# Create temporary files safely: mkstemp, O_EXCL and private directories

Stop symlink, hardlink and pre-created-file attacks on the temporary files your code writes in shared directories such as /tmp: create each file atomically with O_EXCL and owner-only permissions through mkstemp or its equivalent in shell, Python, Java, Node.js, Go, .NET and Rust, keep related files in a private directory, and treat the Linux protected\_\* sysctls and systemd PrivateTmp as a backstop.

Canonical URL: https://softwaresecurity.com/domains/appsec/secure-coding/insecure-temp-files/

## In short

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()` or `os.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_EXCL` makes creation atomic and fails on any existing name, including a dangling symlink; `mkstemp` adds an unpredictable name and mode 0600.
- Unsafe APIs still ship: `tmpnam`, `tempnam` and `mktemp(3)` in C, `tempfile.mktemp` in Python (no runtime warning in 3.14), `File.createTempFile` in Java (0644 under umask 022) and the Rust `tempfile` crate's `tempdir()` (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 had `fs.protected_regular=0`, so check every host.

## Recommended defaults

| 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 |

## Verify it

1. Search for `tmpnam`, `tempnam`, `mktemp(`, `mktemp -u`, `tempfile.mktemp`, `File.createTempFile`, Guava `Files.createTempDir`, `Path.GetTempFileName` and literals starting with `/tmp/`, and run CodeQL `py/insecure-temporary-file`, `java/local-temp-file-or-directory-information-disclosure` and Bandit B108 and B306 in CI.
2. Run the job under `strace -f -e trace=openat,mkdir,mkdirat` and confirm every file it creates in `/tmp` or `$TMPDIR` is opened with `O_CREAT|O_EXCL` or `O_TMPFILE` and mode 0600, and every directory with 0700.
3. 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.
4. Send `SIGTERM` to the job partway through and confirm its temporary directory is gone afterwards.
5. On every Linux host and node, run `sysctl fs.protected_symlinks fs.protected_hardlinks fs.protected_regular fs.protected_fifos` and expect 1 or 2 for each; containers inherit the host kernel's values.
6. For each systemd service that writes temporary files, run `systemctl show -p PrivateTmp <unit>` and expect `PrivateTmp=yes`, or a written reason why not.

## 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`, `mkstemp` and the language equivalents) and owner-only permissions.
- Prove the fix with a planted-symlink regression test, `strace` and the Linux `protected_*` sysctls, and harden the host.

Audience: Developers who write files from services, CLIs, build scripts and tests on shared hosts, and reviewers triaging temporary-file findings.

Before you start:

- Know Unix file permissions, umask, and symlinks versus hard links.
- Read C and one of Python, Java, JavaScript or Go.

Time: About 29 minutes: 11 to read and 18 for the exercise

## See how a predictable name becomes someone else's file

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 |

## Spot the check-then-open pattern

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.

```c
/* 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;
}
```

## Create the file with mkstemp and keep the descriptor

`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.

```c
/* 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, Node.js, .NET, Go and Rust

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.

## Use TemporaryDirectory and exclusive opens in Python

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.

```python
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()

```

## Use Files.createTempDirectory and explicit permissions in Java

`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.

```java
// 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
    }
}
```

## Turn on the Linux link and file protections

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`.

```ini
# /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 = 1

```

## Keep temporary files short-lived and never reopen them by name

Creation 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 an `O_TMPFILE` descriptor, or with `rename()` 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. `SIGKILL` and power loss skip all of them, so add `PrivateTmp=` 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.

## Find insecure temp file code in review and CI



- Search for the unsafe calls: `tmpnam`, `tempnam` and `mktemp(` in C, `mktemp -u` and `$$` in shell file names, `tempfile.mktemp` in Python, `File.createTempFile` and Guava `Files.createTempDir` in Java, `Path.GetTempFileName` in .NET, and string literals that start with `/tmp/` or `/var/tmp/`.
- CodeQL `py/insecure-temporary-file` (tagged CWE-377) flags `tempfile.mktemp` and `os.tempnam`; `java/local-temp-file-or-directory-information-disclosure` flags `File.createTempFile` and Guava `Files.createTempDir` writing to the shared temp directory.
- Bandit B306 (`mktemp_q`) flags `tempfile.mktemp`, and B108 (`hardcoded_tmp_directory`) flags strings that start with `/tmp`, `/var/tmp` or `/dev/shm`.
- At runtime, `strace -f -e trace=openat,mkdir,mkdirat` shows each create; look for `O_CREAT` without `O_EXCL` or `O_TMPFILE` under 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=rwx` lists 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 material

```text
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.

Worked answer: Predictable date name. `protected_symlinks` blocks the symlink, but `protected_regular=0` lets 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; `mv` into root-only `/var/backups`.

### 2. Signing directory (T3/O2)

Explain the race and O2, then rewrite T3.

Worked answer: Between `delete()` and `mkdir()` another user creates the directory first, so the key lands in theirs; when CI wins, O2 shows 0755 with a world-readable 0644 key. Use `Files.createTempDirectory` and create `key.pem` with a `rw-------` attribute (the CVE-2020-15250 mistake).

### 3. Upload preview (T4/O3)

Explain O3 and rewrite T4.

Worked answer: `originalname` is attacker input; `path.join` resolves its `../` out of `/tmp` into the web root (CWE-22) and `'w'` follows symlinks. Write to a per-request `mkdtemp` directory under a generated name with flag `'wx'`.

### 4. Lock file and host (T5/O4)

Explain O4, move T5's lock, and list host backstops.

Worked answer: A user pre-creates the lock; with `protected_regular=0` root opens it and the attacker's `flock` makes `LOCK_NB` fail with `EAGAIN` (O4). Use `RuntimeDirectory=log-shipper` (0700). Host: set `protected_regular`/`fifos=1`, run as systemd units with `PrivateTmp` and `UMask=0077`, add a planted-symlink CI test.

Deliverable: Patched T1-T5, a sysctl.d file and systemd drop-ins for the host, and a planted-symlink regression test for each path that writes a temporary file.

Scope: 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.

Articles and teaching material: CC BY 4.0. Original executable code: MIT. Attribution: Software Security.

## Sources

- [CWE-377: Insecure Temporary File](https://cwe.mitre.org/data/definitions/377.html). CWE 4.20.
- [SEI CERT C Coding Standard, FIO21-C: Do not create temporary files in shared directories](https://cmu-sei.github.io/secure-coding-standards/sei-cert-c-coding-standard/recommendations/input-output-fio/fio21-c/)
- [POSIX.1-2024 (The Open Group Base Specifications Issue 8): mkdtemp, mkostemp, mkstemp](https://pubs.opengroup.org/onlinepubs/9799919799/functions/mkdtemp.html)
- [Linux man-pages open(2): O_EXCL and O_TMPFILE](https://man.archlinux.org/man/open.2.en#O_TMPFILE)
- [Linux kernel documentation, /proc/sys/fs: protected_symlinks, protected_hardlinks, protected_regular, protected_fifos](https://docs.kernel.org/admin-guide/sysctl/fs.html#protected-symlinks)