VNX-362: Check-then-use race on a file path (TOCTOU)
A file path is checked and then used by name a few lines later, so another process can replace the file (typically with a symlink) between the check and the use
| Field | Value |
|---|---|
| Rule ID | VNX-362 |
| Severity | Medium |
| SARIF level | warning |
| Kind | sast |
| Languages | Go, Python |
| CWE | CWE-362 |
| CAPEC | CAPEC-66 |
| ATT&CK | T1190 |
| CWSS vector | CWSS:1.0/TI:M/AP:RU/AL:S/IC:N/FC:LT/RP:RU/RL:S/AV:L/AS:N/IN:A/SC:A/BI:M/DI:H/EX:M/EC:N/P:C |
| Tags | race-condition toctou cwe-362 |
| Source | internal/sast/rules/vnx-362.rego |
Overview
VNX-362 maps to CWE-362: Concurrent Execution using Shared Resource with Improper Synchronization (‘Race Condition’). A file path is checked and then used by name a few lines later, so another process can replace the file (typically with a symlink) between the check and the use. Reported in Python: os.path.exists/isfile/lexists(p) or a pathlib p.exists()/p.is_file() followed within eight lines by a write or permission change on the same path (open(p, ‘w’/‘a’/‘r+’), os.open(p, …O_CREAT/O_WRONLY/O_RDWR…) without O_EXCL, os.chmod/os.chown, p.write_text/p.write_bytes/p.open(‘w’)), and os.access(p, …) followed by any open of p (the access check is the classic setuid TOCTOU). Reported in Go: os.Stat/os.Lstat(p) followed by os.Create, os.WriteFile/ioutil.WriteFile, os.Chmod/os.Chown/os.Truncate or os.OpenFile with a write or create flag and no O_EXCL on the same path, and syscall/unix.Access(p) followed by any open of p. Not reported: exclusive creation (mode ‘x’, O_EXCL), os.makedirs/os.MkdirAll, a plain read after an existence check, a check and a use on different paths, a Go Lstat that tests ModeSymlink (VNX-363 covers that), comments, strings and test files.
Why This Matters
Checking a path and then using it by name are two separate lookups. Between them another process can replace the file, usually with a symbolic link to a file the program can write but the attacker cannot, and the write lands there.
What Gets Flagged
# FLAGGED: existence check, then a write to the same path
if not os.path.exists(path):
with open(path, "w") as fh:
fh.write(data)
// FLAGGED: Stat, then Create on the same path
if _, err := os.Stat(path); os.IsNotExist(err) {
f, err := os.Create(path)
What Is Not Flagged
# exclusive creation fails if the file appeared in the meantime
with open(path, "x") as fh:
fh.write(data)
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
A plain read after an existence check, os.makedirs/os.MkdirAll, a check and a use on different paths or in different functions, comments and test files are not reported. A call wrapped over lines is read as one statement, so O_EXCL on a continuation line still counts as exclusive creation.
Remediation
- Drop the check and let the operation fail atomically: open with
'x'/O_EXCLto create, and handleFileExistsError/os.ErrExist. - Open once and operate on the descriptor (
os.fchmod,f.Chmod) instead of the path. - Never use
access()to decide whether a privileged process may open a file.
References
- CWE-362: Concurrent Execution using Shared Resource with Improper Synchronization (‘Race Condition’)
Suppressing this rule
Put a nosec comment on the line, or on line 1 to cover the whole file. Use the comment marker of the file’s language (#, //, -- or ;):
# nosec vnx-362
// nosec vnx-362
Or record an ignore rule: vulnetix ignore add --rule vnx-362 --reason "...". See Suppressing findings.