VNX-59: Improper Link Resolution Before File Access ('Link Following')
A fixed file name in a shared temporary directory is written or chmod'ed without O_EXCL/O_NOFOLLOW, a path is checked with access()/lstat() and then opened by name, or os.Readlink is given a request or command-line path.
| Field | Value |
|---|---|
| Rule ID | VNX-59 |
| Severity | High |
| SARIF level | error |
| Kind | sast |
| Languages | C, C++, Go, Python |
| CWE | CWE-59 |
| CAPEC | CAPEC-66 |
| ATT&CK | T1190 |
| CWSS vector | CWSS:1.0/TI:H/AP:RU/AL:S/IC:N/FC:LT/RP:RU/RL:S/AV:L/AS:N/IN:A/SC:A/BI:H/DI:H/EX:H/EC:N/P:H |
| Tags | symlink tocttou file cwe-59 |
| Source | internal/sast/rules/vnx-59.rego |
Overview
VNX-59 maps to CWE-59: Improper Link Resolution Before File Access (‘Link Following’). It reports writes through a path that another local user can plant a symbolic link at, and the check-then-open race that lets a link be swapped in.
Why This Matters
Anyone on the host can create /tmp/appd.pid before your program does, as a symbolic link to ~victim/.bashrc or /etc/cron.d/x. A plain fopen(..., "w"), os.Create or chmod then follows the link and truncates, overwrites or re-permissions the target with your program’s privileges. access() followed by open() has the same problem: the file checked is not necessarily the file opened.
What Gets Flagged
int fd = open("/tmp/appd.pid", O_WRONLY | O_CREAT | O_TRUNC, 0600); /* owner-only, but no O_EXCL */
FILE *f = fopen("/tmp/appd.state", "r+");
chmod("/tmp/appd.sock", 0777);
std::ofstream out("/tmp/cache.idx");
if (access(path, W_OK) == 0) { return open(path, O_WRONLY | O_APPEND); }
In C and C++, creating a fixed name in a shared temporary directory with permissions other users can use is reported by VNX-378, not here: fopen() with a w/a mode (no x), and open()/creat() with a mode that grants other users access and no O_EXCL. That is the same defect in its C-specific form, so the line is reported once.
FILE *f = fopen("/tmp/appd.log", "a"); /* reported by VNX-378 */
int fd = open("/tmp/appd.lock", O_WRONLY | O_CREAT | O_TRUNC, 0644); /* reported by VNX-378 */
with open("/tmp/report.csv", "w") as fh: ...
path = os.path.join(tempfile.gettempdir(), "cache.db")
with open(path, "wb") as fh: ...
os.chmod("/tmp/app.sock", 0o777)
f, err := os.Create("/tmp/agent.pid")
lock := filepath.Join(os.TempDir(), "agent.lock")
return os.Create(lock)
When access() and open() are on different lines, the finding is on the access() check line.
Not flagged: mkstemp, NamedTemporaryFile, os.CreateTemp, os.MkdirTemp names, opens with O_EXCL or O_NOFOLLOW, Python mode "x", reads, writes outside shared temporary directories, test code, the same text in comments or strings, and the C/C++ temporary-file creation that VNX-378 reports.
Remediation
- Let the system choose a unique name:
mkstemp(),tempfile.NamedTemporaryFile/mkstemp,os.CreateTemp. - If the name must be fixed, put it in a directory only your user can write (
$XDG_RUNTIME_DIR, a 0700 directory frommkdtemp), and open withO_CREAT | O_EXCL | O_NOFOLLOW. - Replace
access()+open()withopen()(O_NOFOLLOW) followed byfstat()on the descriptor.
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-59
// nosec vnx-59
Or record an ignore rule: vulnetix ignore add --rule vnx-59 --reason "...". See Suppressing findings.