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.

Create a free account to save this page, take notes and track your progress. Sign in

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

Strings vs an XML builderTry it yourself
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.

Was this page helpful?