VNX-367: Time-of-check Time-of-use (TOCTOU) Race Condition
A file is checked by name and then opened or changed by the same name within ten lines, in C or C++, so another process can swap the file (typically for a symlink) between the check and the use
| Field | Value |
|---|---|
| Rule ID | VNX-367 |
| Severity | Medium |
| SARIF level | warning |
| Kind | sast |
| Languages | C, C++ |
| CWE | CWE-367 |
| 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 | toctou race-condition cwe-367 |
| Source | internal/sast/rules/vnx-367.rego |
Overview
VNX-367 maps to CWE-367: Time-of-check Time-of-use (TOCTOU) Race Condition. A file is checked by name and then opened or changed by the same name within ten lines, in C or C++, so another process can swap the file (typically for a symlink) between the check and the use. Reported: access(), eaccess() or euidaccess() on a path followed by any open, open64, fopen, freopen, creat, chmod, chown, lchown, truncate or exec* of that path (the access-then-open setuid race); and stat(), lstat(), stat64() or lstat64() on a path followed by a write or permission change on it: open with O_WRONLY/O_RDWR/O_CREAT/O_TRUNC/O_APPEND, fopen with a w/a/+ mode, creat, chmod, chown, lchown or truncate. The same check and use on one line count. The path may be a variable, a member, a std::string’s c_str() or a literal. Not reported: open with O_EXCL or O_NOFOLLOW, a read after a stat, fstat/fchmod on a descriptor (the safe form), a check and a use on different paths, comments and string literals.
Why This Matters
access() and stat() look a path up; the open() that follows looks it up again. Between the two another process can replace the file with a symbolic link, so a setuid program that checked the real user’s permission writes to a file the user could not.
What Gets Flagged
/* FLAGGED: access() check, then open of the same path */
if (access(path, W_OK) != 0)
return -1;
int fd = open(path, O_WRONLY | O_APPEND);
/* FLAGGED: stat(), then a write-mode fopen */
if (stat(cfg, &st) == 0 && st.st_uid == getuid())
fp = fopen(cfg, "w");
What Is Not Flagged
int fd = open(path, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW, 0600);
fstat(fd, &st); /* check the descriptor you opened */
A read after a stat() (the write flags are read from that call’s own text, so a later open(dst, O_CREAT ...) does not count), fstat/fchmod on a descriptor, a check and a use on different paths or in different functions, comments and string literals are not reported.
Remediation
- Open first, then check the descriptor with
fstat, and change it withfchmod/fchown. - Use
O_EXCLandO_NOFOLLOW; in a setuid program drop to the real user’s IDs before opening instead of callingaccess().
References
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-367
// nosec vnx-367
Or record an ignore rule: vulnetix ignore add --rule vnx-367 --reason "...". See Suppressing findings.