Skip to content

VNX-643: XPath injection: query built from a dynamic string

Reports an XPath query that is built by concatenation, formatting or an f-string and then compiled or evaluated (Java javax.xml.xpath and dom4j; Python lxml, elementpath and ElementTree; a Python expression assembled in an `io.StringIO` with `write()` counts as built). Parameterised queries, constant expressions and operands the enclosing method proves constant are not reported.

Updated 02 Nov 2026Checked against cli@de9b52a
Field Value
Rule ID VNX-643
Severity High
SARIF level error
Kind sast
Languages Java, Python
CWE CWE-643
CAPEC CAPEC-66
ATT&CK T1190
CWSS vector CWSS:1.0/TI:H/AP:A/AL:A/IC:N/FC:LT/RP:N/RL:A/AV:I/AS:N/IN:A/SC:A/BI:H/DI:H/EX:H/EC:N/P:H
Tags xpath-injection injection cwe-643
Source internal/sast/rules/vnx-643.rego

Overview

VNX-643 maps to CWE-643: Improper Neutralization of Data within XPath Expressions (‘XPath Injection’). It reports an XPath expression that splices a variable into the query text, either inline in the call or in a variable assigned up to 20 lines above, when that expression reaches an XPath compile or evaluate call.

Why This Matters

XPath has no prepared statements by default. A value such as ' or '1'='1 closes the quoted literal and turns a lookup for one user into a match on every node, which is how XML-backed logins and directories are bypassed and dumped.

What Gets Flagged

xpath.evaluate("//stock[@supplier='" + supplier.getCode() + "']", doc, XPathConstants.NODESET);

String path = "//bin[@label='" + bin + "']";
XPathExpression expr = xpath.compile(path);

doc.selectSingleNode(String.format("//product[sku='%s']", sku));

A Java expression traced to request data (request.getParameter, a header or cookie, a @RequestParam or other annotated handler parameter, or a String method parameter spliced into a quoted value as in "//user[@name='" + name + "']") is reported by VNX-JAVA-013, the Java taint rule for the same injection, so the line appears once. This rule keeps the Java expressions built from any other non-constant value, and every Python one.

tree.xpath(f"//user[@name='{user}' and @password='{pw}']")

query = f"/Employees/Employee[@emplid='{param}']"
run_query = lxml.etree.XPath(query)

root.findall(".//book[@author='" + author + "']")

Not flagged: xpath.evaluate("//user[name/text()=$user]", doc) with an XPathVariableResolver, tree.xpath("//user[@name=$name]", name=user), constant expressions such as ROOT + "/server/host", and other evaluate() calls (ScriptEngine, browser document.evaluate).

Reading The Method First

Before reporting, the rule reads the enclosing Java method or Python function (at most 120 lines up) the way a reviewer would, and does not report an operand that provably holds a constant where the query is built:

  • a literal it was last assigned on every path that reaches the query;
  • the branch an if/else (Python if/elif/else) or a conditional expression actually takes, when the condition folds to a constant: integer literals and integer locals assigned once (if (4 * 10) + n > 30 with n = 5), or in Python a literal tested with in / not in against a string local;
  • the case a switch (Java) or match (Python) on a constant selects;
  • the element a list index, or a map, dict or ConfigParser key, actually holds after the adds, removes, puts and pops above it, when that element is a literal;
  • the result of a method or function in the same file (a nested class included) whose every return is constant.
sku = request.args.get("sku", "")
batch = 8
if batch * 5 - 10 > 20:      # always true
    pick = "SKU-100"
else:
    pick = sku               # never runs
tree.xpath(f"//item[@sku='{pick}']")   # not flagged

In Python, a value placed inside an apostrophe-quoted XPath literal is also not reported when its apostrophes are replaced ({name.replace("'", "'")}) or when an early return has proven it has none (if "'" in name: return ... above the query, in a block that encloses it). A value between double quotes is still reported, because the apostrophe check does not protect that delimiter.

Anything the analysis cannot resolve stays reported: a condition on run-time data, a helper in another file, a collection that is passed to a call or aliased, a value assigned inside try, a loop, or rebound by a tuple, for, with or walrus target.

Calls that cannot bring data in

A call into code the scan cannot see (another class or module) is treated as inert, and its value as a constant, when all of these hold:

  • its name is not that of a source (read*, input, getenv, get_* of a parameter, header, cookie or value, query, execute, popen);
  • every argument is a constant: a literal, or a name the method fixes;
  • its receiver is a class, an imported module, or a local built from such a call or from a constructor with constant arguments, and that local is only ever called with constant arguments.
# NOT REPORTED: the helper is given literals only
book = DeptBook.standard()
code = book.code_of("staff")
TREE.xpath("//user[@dept='" + code + "']")

# REPORTED: a run-time argument
code = book.code_of(dept)

# REPORTED: the receiver was built from, or fed, a run-time value
book = DeptBook(request.args.get("scope", ""))
code = book.code_of("staff")

# REPORTED: a source-like method name
code = helpers.read_line("dept")

Anything that fails one of the tests stays reported, so a helper that hands back its argument is never mistaken for a constant.

Remediation

  1. Bind values as XPath variables: XPath.setXPathVariableResolver in Java, keyword arguments with $name in lxml.
  2. If a variable cannot be bound, validate the value against a strict allow-list (for example digits only for an id).
  3. Never build the expression text from request data.

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

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

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