XML in Production · Data Correctness
The Customer Named </name><admin>true</admin>
An order service built XML by string formatting. A company name with an ampersand broke every export, and a crafted value changed the document's structure.
Scenario
Example team: a B2B portal sends account updates to a provisioning system as XML built with f-strings. The provisioning system grants roles based on elements in the message.
What you see in production
First, the export for "Fish & Chips Ltd" fails with a parse error at the receiver. Later, a security review finds that a user who set their display name to x</name><role>admin</role><name>x received admin access in the downstream system.
The naive code
xml = f"<account><name>{user.name}</name><role>{user.role}</role></account>"
Reproduce it
from lxml import etree
def naive(name, role):
return f"<account><name>{name}</name><role>user</role></account>"
def safe(name, role):
account = etree.Element("account")
etree.SubElement(account, "name").text = name
etree.SubElement(account, "role").text = role
return etree.tostring(account).decode()
for name in ["Fish & Chips Ltd", "x</name><role>admin</role><name>x"]:
try:
roles = etree.fromstring(naive(name, "user")).xpath("//role/text()")
print(f"naive {name[:22]!r:26} -> roles {roles}")
except etree.XMLSyntaxError as e:
print(f"naive {name[:22]!r:26} -> receiver can't parse: {e.msg}")
roles = etree.fromstring(safe(name, "user")).xpath("//role/text()")
print(f"safe {name[:22]!r:26} -> roles {roles}")
Output
naive 'Fish & Chips Ltd' -> receiver can't parse: xmlParseEntityRef: no name, line 1, column 22 safe 'Fish & Chips Ltd' -> roles ['user'] naive 'x</name><role>admin</r' -> roles ['admin', 'user'] safe 'x</name><role>admin</r' -> roles ['user']
Root cause
String formatting doesn't know XML's rules: & and < must be escaped, and markup inside a value is interpreted as markup. This is XML injection, the same class of bug as SQL injection.
The fix
- Build XML with an API (lxml/ElementTree, DOM, XMLWriter, StAX, JAXB) that escapes text and attribute values.
- If you must use templates, use an XML-aware template engine with auto-escaping.
- Validate outgoing messages against the schema in tests and, for high-risk flows, in production.
- Receivers should never derive permissions from unvalidated, unauthenticated XML fields.
Trade-offs
- Builders are slightly more verbose than f-strings but remove a whole class of bugs.
- Validating every outgoing message costs CPU; sample or validate in tests if volume is very high.
Detect it in production
- Parse errors reported by receivers, grouped by message type.
- Search code for XML built with string formatting (
f"<,"<" +,sprintf("<). - Fuzz tests with
&,<, quotes and markup in every text field.
Key points
- Never build XML by string concatenation; escaping is not optional.
- XML injection can change structure, not just break parsing.
- Validate what you send and don't trust structure you didn't validate.
Practice questions
What is XML injection?
Answer
Inserting untrusted text into XML without escaping so that markup in the text changes the document's structure, for example adding elements or attributes the application then trusts.
Which characters must be escaped in XML text and attribute values?
Answer
Always & (as &) and < (as <); quotes inside attribute values delimited by the same quote; > in the sequence ]]>. A library does this for you.
Open in the app to mark lessons complete, take notes and track your progress.