Skip to content

VNX-363: Race condition enabling link following (symlink check, then open by name)

A path is tested for being a symbolic link and then opened, written or re-permissioned by name a few lines later. The test and the use are two lookups, so a local attacker can swap a regular file for a link to /etc/passwd or a key file between them, and the program follows it

Updated 02 Nov 2026Checked against cli@de9b52a
Field Value
Rule ID VNX-363
Severity Medium
SARIF level warning
Kind sast
Languages Go, Java, JavaScript / TypeScript, PHP, Python, Ruby
CWE CWE-363
CAPEC CAPEC-97
ATT&CK T1557
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 symlink toctou cwe-363
Source internal/sast/rules/vnx-363.rego

Overview

VNX-363 maps to CWE-363: Race Condition Enabling Link Following. A path is tested for being a symbolic link and then opened, written or re-permissioned by name a few lines later. The test and the use are two lookups, so a local attacker can swap a regular file for a link to /etc/passwd or a key file between them, and the program follows it. Reported, with the use on the same path within ten lines of the check: Python os.path.islink(p) or p.is_symlink() then open(p), os.open(p) without O_NOFOLLOW, os.chmod/os.chown, shutil.copy*(p) or a pathlib p.open/read/write/chmod; Go os.Lstat(p) whose result is tested against os.ModeSymlink then os.Create/Open/OpenFile/ReadFile/WriteFile/Chmod/Chown(p); Java Files.isSymbolicLink(p) then a java.nio.file.Files read, write or open of p without LinkOption.NOFOLLOW_LINKS, or new FileOutputStream/FileWriter/FileInputStream/RandomAccessFile(p or p.toFile()); Node fs.lstat/lstatSync(p) tested with isSymbolicLink() then an fs read, write, open, chmod or chown of p without O_NOFOLLOW; PHP is_link($p) then fopen, file_put_contents, file_get_contents, readfile, chmod, chown, touch or copy of $p; Ruby File.symlink?(p) then File/IO.open, read, write, chmod or chown of p. Not reported: an open with O_NOFOLLOW or NOFOLLOW_LINKS, a use of a different path, os.readlink/readlinkSync (reading the link itself), unlink of the link, a check and use through a directory handle (os.Root, openat), comments and string literals.

Why This Matters

Testing whether a path is a symbolic link and then opening it by name leaves a window in which a local attacker can swap the regular file for a link to /etc/passwd or a key file. The program passed the check and follows the link anyway.

What Gets Flagged

# FLAGGED: islink() check, then open by name
if os.path.islink(dest):
    raise ValueError("refusing to follow a link")
with open(dest, "wb") as fh:
    fh.write(payload)
// FLAGGED
if (!is_link($cache)) {
    file_put_contents($cache, $data);
}

What Is Not Flagged

# the kernel refuses the link at open time
fd = os.open(dest, os.O_WRONLY | os.O_CREAT | os.O_NOFOLLOW, 0o600)
Files.newOutputStream(p, LinkOption.NOFOLLOW_LINKS);

Reading the link itself (readlink), unlinking it, a use of a different path, directory-handle APIs (os.Root, openat), comments and strings are not reported. A call wrapped over lines is read as one statement, so NOFOLLOW_LINKS on a continuation line is honoured.

Remediation

  1. Open with O_NOFOLLOW (or LinkOption.NOFOLLOW_LINKS) and check the opened descriptor with fstat.
  2. Work relative to a directory handle you trust (openat, Go os.Root).
  3. Keep files a privileged process writes in directories other users cannot write.

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-363
// nosec vnx-363

Or record an ignore rule: vulnetix ignore add --rule vnx-363 --reason "...". See Suppressing findings.

Edit this pageSources: cli/internal/sast/rules/vnx-363.rego, cli/internal/sast/docs/vnx-363.md
esc
↑ ↓ move↵ openSearches in your browser. A key like autonomy.max or a code like threshold_required jumps straight to it.