Skip to content

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.

Updated 02 Nov 2026Checked against cli@de9b52a
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(...) (also posixpath/ntpath, an alias such as import os.path as osp, and a from-imported commonprefix) compared with == or !=. The Python documentation warns that it works character by character;
  • str.startswith() on a canonicalised path (os.path.realpath/abspath/normpath(...) or str(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 append os.sep or '/'.

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

  1. Use os.path.commonpath([base, p]) == base or Path(p).resolve().is_relative_to(base) (Python 3.9+).
  2. If you must use startswith, compare against base + os.sep, and handle p == base separately.

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.

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