VNX-706: Directory containment decided by a character prefix instead of a path prefix
Reports Python containment checks that compare characters instead of path components: os.path.commonprefix(...) compared with == or !=, and startswith() on a realpath/abspath/normpath/resolve() path against a base directory with no trailing separator.
| Field | Value |
|---|---|
| Rule ID | VNX-706 |
| Severity | Low |
| SARIF level | note |
| Kind | sast |
| Languages | Python |
| CWE | CWE-706 |
| CAPEC | CAPEC-66 |
| ATT&CK | T1190 |
| CWSS vector | CWSS:1.0/TI:L/AP:RU/AL:S/IC:N/FC:LT/RP:RU/RL:S/AV:L/AS:N/IN:A/SC:A/BI:L/DI:H/EX:L/EC:N/P:L |
| Tags | path-equivalence path-traversal cwe-706 |
| Source | internal/sast/rules/vnx-706.rego |
Overview
VNX-706 maps to CWE-706: Use of Incorrectly-Resolved Name or Reference. A check that a resolved path lies inside a base directory must compare path components. Comparing characters resolves a sibling directory whose name merely starts with the base name (/srv/uploads-private, /var/www2) as inside the base, so it passes the check.
The rule reports, in Python:
os.path.commonprefix(...)(alsoposixpath/ntpath, an alias such asimport os.path as osp, and a from-importedcommonprefix) compared with==or!=. The Python documentation warns that it works character by character;str.startswith()on a canonicalised path (os.path.realpath/abspath/normpath(...)orstr(Path.resolve()), inline or through a variable assigned from one in the 10 lines above) whose argument is a base directory with no trailing separator: a path literal not ending in/, or a name whose assignment in the file does not appendos.sepor'/'.
Not reported: os.path.commonpath, Path.is_relative_to, a prefix with + os.sep, os.path.join(base, '') or a trailing '/', a base variable assigned with a trailing separator, commonprefix used to compute a shared stem, startswith on a non-path literal, comments and string literals. Prefix checks on paths that were never canonicalised (base/../x) are VNX-41.
What Gets Flagged
target = os.path.realpath(os.path.join(UPLOAD_ROOT, name))
if os.path.commonprefix([UPLOAD_ROOT, target]) != UPLOAD_ROOT: # FLAGGED
raise PermissionError(name)
if not os.path.realpath(os.path.join(STATIC, name)).startswith(STATIC): # FLAGGED
abort(403)
What Is Not Flagged
if os.path.commonpath([ROOT, target]) != ROOT:
raise PermissionError(name)
if not target.startswith(ROOT + os.sep):
raise PermissionError(name)
Remediation
- Use
os.path.commonpath([base, p]) == baseorPath(p).resolve().is_relative_to(base)(Python 3.9+). - If you must use
startswith, compare againstbase + os.sep, and handlep == baseseparately.
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-706
// nosec vnx-706
Or record an ignore rule: vulnetix ignore add --rule vnx-706 --reason "...". See Suppressing findings.