Last reviewed: November 5, 2026 (Asia/Shanghai); sources fetched November 4 UTC.
Direct answer
Prevent XPath injection in coding-agent XML queries by keeping the expression under application control and passing external values through the XPath engine’s documented variable interface. Compile a fixed template, bind values separately, and test the exact nodes or identifiers returned. Keep namespace mappings, axes, predicates, operators, and function choices in reviewed code.
MITRE CWE-643 describes external input changing a dynamically constructed XPath expression, potentially exposing restricted XML content or bypassing application checks. The OWASP XPath Injection page recommends a parameterized interface where available. Compilation alone does not establish that boundary: compiling an expression after interpolating a request value still compiles caller-influenced query structure.
Consider an agent adding a document-title search to a tenant catalog. A normal title returns the right document, but a string-built predicate can interpret part of a title as XPath. The acceptance rule is stronger than “no syntax error”: every accepted title remains a literal value, the authorized tenant stays fixed, and no extra document appears.
Who this is for
Use this review when a coding agent changes XML catalog searches, configuration selectors, document metadata lookups, import filters, or wrappers that evaluate XPath. Trace values from requests, imported records, queued jobs, repository files, and model output into the evaluator. Inspect helpers reached by the diff as well as visible .xpath(...) calls.
This guide focuses on query interpretation after a tree exists. Apply the site’s XXE review to parsing and resource access, and the object-authorization review to the caller’s permitted records. Variable binding preserves a value’s meaning; the application must still authorize the scope it searches.
Key takeaways
- Keep XPath text constant and supply values as variables through the actual engine API.
- Compile code-owned templates before request values arrive. Review every dynamically assembled expression.
- Translate required search modes through a closed mapping to complete reviewed expressions.
- Define namespaces by URI. Do not broaden a failed namespace match automatically.
- Treat the context node as part of query evaluation, not an authorization sandbox.
- Assert exact result identifiers, including foreign-tenant exclusions and literal punctuation matches.
- Fail on missing variables or evaluator errors without retrying a broader query.
Sources checked
Three primary public references were reached successfully on November 4, 2026 UTC:
- MITRE CWE-643: Improper Neutralization of Data within XPath Expressions defines the weakness, its access-control and confidentiality consequences, and parameterization as a mitigation. The current entry is CWE 5.0, last updated October 28, 2026. Its history records changes to the description, consequences, name, alternate terms, and references; this is a taxonomy update, not a new vulnerability disclosure.
- OWASP: XPATH Injection explains how concatenated input changes selection logic and recommends parameterized XPath interfaces and controlled error responses. This review follows that parameterization recommendation; it does not prescribe a generic quote-replacement helper.
- The lxml project’s XPath and XSLT guide
documents XPath 1.0 support, variable arguments, compiled
XPathobjects, namespace mappings, context behavior, return types, and evaluation errors. These maintained API semantics support the Python example below; other engines require their own documented binding mechanism.
The references support an evergreen code-review task. The proposed contract, limits, and acceptance matrix are engineering recommendations. Local example checks are described below with their runtime and scope.
Contract details to verify
Separate values, structure, and authority
For each evaluator call, record these decisions before accepting the patch:
| Boundary | Review requirement |
|---|---|
| Expression | A code-owned constant or a closed selection of complete constants |
| Values | Named variables with explicit accepted types and lengths |
| Scope | A trusted server-side authorization decision determines the searchable tenant or dataset |
| Namespaces | Reviewed prefixes map to exact namespace URIs |
| Result | Expected type, allowed identifiers, cardinality, and projection are specified |
| Failure | Invalid input and evaluator failures terminate without a broader fallback |
| Resource use | Input tree size and evaluation work fit a documented budget |
A variable supplies a value within the expression; it does not safely splice an arbitrary axis, predicate, or function call into the grammar. If the feature offers “search by title” and “search by code,” map those two mode tokens to two complete fixed templates. Reject unknown modes before evaluation. Do not accept a raw XPath expression generated from a prompt as an ordinary search value.
Business validation remains useful, but it cannot replace binding. A real title can contain apostrophes, double quotes, brackets, or words such as or. XML serialization and XPath expression construction are different contexts. Use the engine’s variable API rather than assuming an XML or HTML encoder produces a safe XPath literal.
Bind values in a fixed lxml expression
Assume a previously validated catalog whose namespace is urn:example:catalog, whose document IDs are unique, and whose documents each have one title. The example policy permits reading all catalog document IDs within a tenant authorized by the server. Parsing, schema validation, authentication, and that authorization decision happen before this helper:
from lxml import etree
CATALOG_NS = "urn:example:catalog"
FIND_DOCUMENT_IDS = etree.XPath(
"/c:catalog/c:tenant[@id=$tenant_id]"
"/c:document[c:title=$title]/@id",
namespaces={"c": CATALOG_NS},
regexp=False,
smart_strings=False,
)
def document_ids(tree, authorized_tenant_id, title):
if type(authorized_tenant_id) is not str or not 1 <= len(authorized_tenant_id) <= 64:
raise ValueError("tenant_context_invalid")
if type(title) is not str or not 1 <= len(title) <= 200:
raise ValueError("title_invalid")
return FIND_DOCUMENT_IDS(
tree, tenant_id=authorized_tenant_id, title=title
)
The expression is compiled independently of both values. $tenant_id and $title receive strings through keyword arguments; their contents do not become query syntax. The proposed lengths count Python string characters and are example business limits. They do not bound the XML tree or total evaluation work.
authorized_tenant_id must come from the application’s trusted access decision. Copying a submitted tenant field into that argument would defeat the stated policy even though injection prevention still worked. If permissions differ between documents within a tenant, add the required document-level authorization before returning results.
The optional flags disable EXSLT regular expressions and request ordinary string results. They do not establish authorization or make arbitrary expressions safe. This helper registers no custom extension functions; inspect any application-wide registrations separately.
Preserve namespace and context semantics
lxml documents that XPath prefixes are supplied by the evaluator and need not match the prefixes used in the XML. A document using a default namespace still needs an explicit nonempty prefix in this XPath expression. Match the namespace URI, rather than changing the query to ignore namespaces when a fixture returns nothing.
For example, replacing c:document with *[local-name()='document'] removes the namespace constraint. Such a change needs its own explicit format policy and tests. Keep a fixture with the same local names under a foreign URI so a compatibility repair cannot silently accept it.
An element passed to the evaluator is also insufficient isolation. lxml evaluates absolute expressions against the root tree. A leading // can therefore search outside that element’s subtree, while ancestor navigation can also leave a relative starting point. Review the complete expression and its possible reach. In the example, the code-owned tenant predicate provides the intended selection constraint; a subtree argument alone would not provide it.
Bound collection as well as input
The lxml guide states that XPath collects all results before returning them. Slicing the resulting list to ten entries does not cap the work already performed. Bound the accepted document size and structure before evaluation, keep templates narrow, and test large legitimate fixtures against the service’s CPU and memory budget. Where costly inputs remain possible, use an isolated worker with an external deadline and resource limits.
Happy-path operator workflow
- Identify the patch revision, actual evaluator API, runtime version, and every wrapper that can evaluate an expression.
- Classify each input as a bound value, a structural choice, or trusted authorization context. Trace its origin.
- Replace value interpolation with documented variables; reduce structural choices to complete reviewed templates.
- Confirm namespace, schema, result-shape, and authorization assumptions at the real entry point.
- Run the synthetic matrix below and assert exact identifiers through the application’s response path.
- Attach the template ID, policy revision, runtime versions, fixture IDs, observed results, and resource measurements to the review.
Error-path operator workflow
Treat a fixed-template compilation failure as a configuration defect. Keep that query path unavailable until the template is repaired. For evaluation failures, such as a missing variable or invalid namespace wiring, return the application’s controlled failure without retrying through string interpolation, a namespace-blind selector, or an unscoped search.
lxml distinguishes compilation syntax errors from evaluation errors for compiled XPath objects. Catch the appropriate documented exception at the application boundary, preserve a stable internal reason code, and omit raw exception text from the public response. An ordinary no-match result remains distinct from a configuration failure.
Record the query template ID, revision, failure class, and sanitized result count. Exclude raw XML, titles, credentials, and returned document contents. If later code exports or updates selected records, keep that work behind successful selection and authorization; a failed query must not promote partial output or submit a job.
Build a harmless adversarial test matrix
Create two synthetic tenants with different document IDs and overlapping titles. Store punctuation-heavy titles as real fixture data so the test can prove exact literal matching, rather than merely observing an empty result.
| Case | Required observation |
|---|---|
| Ordinary title stored in both tenants | Only the authorized tenant’s expected ID is returned |
| Apostrophe, double quotes, and both quote types | Exact matching fixture IDs; input is preserved |
A stored title such as ' or '1'='1 or text containing brackets and a union bar | Only the literal match; no additional IDs |
| Unicode and maximum accepted length | Documented literal matching without alternate query construction |
| Empty, overlong, boolean, or collection input | Controlled rejection before evaluation |
| Unknown title | Empty result under the declared policy |
| Same namespace URI with different XML prefixes | Same intended result |
| Same local names under a foreign namespace URI | Schema rejection or no match; no namespace-blind retry |
| Missing variable or broken namespace configuration | Controlled evaluator failure; no fallback or downstream effects |
| Large permitted tree | Measured evaluation fits the chosen resource budget |
Local checks on Python 3.12.3, lxml 6.1.3, and libxml2 2.14.6 verified the helper’s literal matching, type and length rejection, no-match behavior, namespace mismatch, and missing-variable failure using synthetic in-memory trees. A separate control confirmed that a leading // evaluated on one tenant element could select both tenants’ documents. These checks cover the example’s XPath behavior; they do not establish a reader’s authentication, parser, application integration, or deployment safety. The full matrix remains an acceptance plan for that application.
Failure modes
The agent compiles an interpolated string. The expression already contains external grammar. Compile the constant template and pass variables separately.
The patch rejects apostrophes to make tests pass. Legitimate data breaks while other structural inputs may remain. Validate the business contract and preserve accepted values through binding.
A namespace mismatch triggers a broader selector. The fallback changes which format and records are accepted. Repair namespace wiring and retain a foreign-namespace negative test.
The test checks only result count. One unauthorized ID can replace an authorized ID without changing the count. Assert the exact identifiers and relevant response fields.
A small context element is assumed to contain the query. Absolute paths, leading //, and navigation can escape it. Review expression reach and test a neighboring tenant.
The query is bound but its tenant comes from the request. Structure is preserved while authority is wrong. Establish scope through server-side authorization.
FAQ
Does disabling external entities prevent XPath injection?
It addresses a different boundary. XXE controls what parsing can load; XPath binding controls how query inputs are interpreted. Review both when a patch parses and searches XML.
Is a compiled XPath automatically safe?
No. A compiled constant with separately bound variables preserves the intended separation. Compiling an expression assembled from external text preserves the unsafe structure already created.
Can a user choose the searched field?
Offer a closed application mode mapped to a reviewed fixed expression. Do not splice a submitted element name or predicate into the template. Keep authorization and output projection explicit for each mode.
Does variable binding impose a result limit?
No. It separates values from expression syntax. Input limits, query scope, cardinality expectations, and resource budgets need their own controls.
Reader next step
Choose one XPath call in the next coding-agent patch. Freeze its template, identify each variable’s origin, and add two tenants with overlapping punctuation-heavy titles. Require exact authorized IDs and controlled failure without a broader retry. Then use the XML parser review to verify the earlier parsing boundary before approving the complete path.