VNX-41: Improper Resolution of Path Equivalence
A directory-containment check (startsWith, startswith, is_relative_to, strings.HasPrefix) is made on a path that was never canonicalised, so an equivalent spelling such as base/../etc/passwd passes it.
| Field | Value |
|---|---|
| Rule ID | VNX-41 |
| Severity | Medium |
| SARIF level | warning |
| Kind | sast |
| Languages | Go, Java, JavaScript / TypeScript, Python |
| CWE | CWE-41 |
| CAPEC | CAPEC-66 |
| ATT&CK | T1190 |
| CWSS vector | CWSS:1.0/TI:M/AP:A/AL:A/IC:N/FC:LT/RP:N/RL:A/AV:I/AS:N/IN:A/SC:A/BI:M/DI:H/EX:M/EC:N/P:C |
| Tags | path-equivalence traversal cwe-41 |
| Source | internal/sast/rules/vnx-41.rego |
Overview
VNX-41 maps to CWE-41: Improper Resolution of Path Equivalence. It reports a prefix check against a base directory where the checked path was built (joined, resolved, concatenated) but not canonicalised, inline or through a variable assigned in the 15 lines above.
Why This Matters
/srv/files/../../etc/passwd starts with /srv/files, and so does /srv/files-backup/x. A containment check on the raw string, or on a java.nio.file.Path that has not been normalised, accepts both. The check looks like path-traversal protection and is not.
What Gets Flagged
if (!f.getAbsolutePath().startsWith(ROOT)) { ... }
Path target = base.resolve(name);
if (!target.startsWith(base)) { ... }
full = os.path.join(UPLOAD_DIR, request.args["name"])
if not full.startswith(UPLOAD_DIR): ...
target = BASE / name
if not target.is_relative_to(BASE): ...
const filePath = PUBLIC_DIR + req.params.name;
if (!filePath.startsWith(PUBLIC_DIR)) { ... }
p := rootDir + r.URL.Query().Get("name")
if !strings.HasPrefix(p, rootDir) { ... }
Not flagged: paths canonicalised before the check (getCanonicalPath, normalize(), toRealPath, os.path.realpath/abspath/normpath, Path.resolve(), path.resolve/normalize/join, filepath.Join/Clean), a variable reassigned to its canonical form before the check, and prefix checks against a non-directory literal such as "https://" or a bare "/".
Remediation
- Canonicalise the candidate path and the base:
getCanonicalPath()ortoRealPath(),os.path.realpath,path.resolve,filepath.Clean/EvalSymlinks. - Compare against the canonical base followed by a separator, so
/srv/files-backupdoes not match/srv/files. - Prefer an API that serves from a root safely (
send_from_directory,res.sendFile(name, { root }),http.FileServer).
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-41
// nosec vnx-41
Or record an ignore rule: vulnetix ignore add --rule vnx-41 --reason "...". See Suppressing findings.