Building a query by pasting user input into a string is SQL injection in another language. A quote in the input ends the string literal early, and the rest is parsed as XPath. This lookup by title shows the bug and the fix, an XPath variable passed separately from the expression:
from lxml import etree
root = etree.parse("booknest-catalog.xml").getroot()
def unsafe(title): # builds the query from user input: don't
return root.xpath(f"//book[title = '{title}']/@id")
def safe(title): # passes the input as an XPath variable
return root.xpath("//book[title = $t]/@id", t=title)
for user_input in ["Gardens in Glass", "The Clockmaker's Paradox", "x' or 'a'='a"]:
for fn in (unsafe, safe):
try:
result = fn(user_input)
except etree.XPathEvalError as e:
result = f"XPathEvalError: {e}"
print(f"{fn.__name__:6} {user_input!r:28} -> {result}")unsafe 'Gardens in Glass' -> ['b6'] safe 'Gardens in Glass' -> ['b6'] unsafe "The Clockmaker's Paradox" -> XPathEvalError: Invalid predicate safe "The Clockmaker's Paradox" -> ['b5'] unsafe "x' or 'a'='a" -> ['b1', 'b2', 'b3', 'b4', 'b5', 'b6'] safe "x' or 'a'='a" -> []
The concatenated version fails on an honest title with an apostrophe and returns every book for a crafted one; the parameterized version treats both as plain strings. Every engine in this chapter has a parameter mechanism: xpath(expr, name=value) in lxml 3,063 , external variables (declare variable $t external;) in XQuery with Saxon 726,956 and BaseX 693,830 , xsl:param in XSLT, and XPathVariableResolver in Java. Treat xsl:evaluate and xquery:eval() like eval(): never on untrusted text.