GET /xml
Returns XML documents with parser-attack or well-formedness flaws. Targets SOAP, B2B, RSS/sitemap, and config-file ingestion pipelines. Default mode is visible misnesting; other modes cover entity-expansion DoS, external-entity references, and declaration-vs-body encoding mismatch.
mode
mismatched-tags (default; `<alpha><beta></alpha></beta>` — every conformant parser rejects), billion-laughs (3 levels of self-similar entity expansion; ~3KB expanded; hardened parsers reject), xxe-external (external entity referencing nonexistent.invalid per RFC 6761; parsers with external-entity resolution hang or fail), encoding-mismatch (declaration claims utf-8 but body bytes are CP1252; strict parsers fail, lenient produce mojibake).
control Compare against the well-formed counterpart: not.catastrophic.io/xml side-by-side
build a request:
expect: 200 OK with Content-Type: application/xml; charset=utf-8. Cache-Control: no-transform prevents the CDN edge from rewriting the body. X-Chaos-Xml-Mode reflects the selected mode; X-Chaos-Xml-Note explains the flaw.
curl -i 'https://chaos.catastrophic.io/xml?mode=mismatched-tags'
# defusedxml refuses billion-laughs, XXE, and DTD by default — the
# safe choice for ingesting untrusted XML. The stdlib xml.etree parser
# silently disables external entities since 3.7.1 but still parses
# internal entities, so billion-laughs hits home.
import urllib.request
from defusedxml import ElementTree as DET
from defusedxml.common import EntitiesForbidden, DTDForbidden
raw = urllib.request.urlopen("https://chaos.catastrophic.io/xml?mode=mismatched-tags").read()
try:
DET.fromstring(raw)
print("Parsed (defusedxml accepted)")
except (EntitiesForbidden, DTDForbidden) as e:
print(f"defusedxml refused: {type(e).__name__}: {e}")
except Exception as e:
print(f"Other parse error: {type(e).__name__}: {e}")
// Browser DOMParser is permissive by default; check parsererror node.
const res = await fetch("https://chaos.catastrophic.io/xml?mode=mismatched-tags");
const raw = await res.text();
const doc = new DOMParser().parseFromString(raw, "application/xml");
const err = doc.querySelector("parsererror");
console.log("Mode:", res.headers.get("X-Chaos-Xml-Mode"));
console.log("Parsed clean:", !err);
if (err) console.log("Parser said:", err.textContent);
package main
import (
"encoding/xml"
"fmt"
"io"
"net/http"
"strings"
)
func main() {
resp, _ := http.Get("https://chaos.catastrophic.io/xml?mode=mismatched-tags")
defer resp.Body.Close()
raw, _ := io.ReadAll(resp.Body)
fmt.Println("Mode:", resp.Header.Get("X-Chaos-Xml-Mode"))
// encoding/xml refuses DOCTYPE entirely (since Go 1.13). For modes
// that use a DOCTYPE (billion-laughs, xxe-external), the decoder
// returns an error immediately — which is the safe behaviour.
var v any
dec := xml.NewDecoder(strings.NewReader(string(raw)))
err := dec.Decode(&v)
fmt.Println("Parse error:", err)
}
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking"] }
// quick-xml = "0.36"
use quick_xml::reader::Reader;
use quick_xml::events::Event;
fn main() -> Result<(), Box> {
let raw = reqwest::blocking::get("https://chaos.catastrophic.io/xml?mode=mismatched-tags")?.text()?;
let mut reader = Reader::from_str(&raw);
reader.config_mut().expand_empty_elements = true;
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Start(e) => println!("start: {}", String::from_utf8_lossy(e.name().as_ref())),
Event::End(e) => println!("end: {}", String::from_utf8_lossy(e.name().as_ref())),
_ => {}
}
}
Ok(())
}
// Java's built-in javax.xml.parsers.SAXParser is vulnerable by default
// — disable DTDs and external entities explicitly when parsing
// untrusted input.
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.helpers.DefaultHandler;
import java.net.URI;
import java.net.http.*;
public class XmlChaos {
public static void main(String[] args) throws Exception {
var client = HttpClient.newHttpClient();
var req = HttpRequest.newBuilder(URI.create("https://chaos.catastrophic.io/xml?mode=mismatched-tags")).build();
var resp = client.send(req, HttpResponse.BodyHandlers.ofInputStream());
var f = SAXParserFactory.newInstance();
f.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
f.setFeature("http://xml.org/sax/features/external-general-entities", false);
try {
f.newSAXParser().parse(resp.body(), new DefaultHandler());
System.out.println("Parsed clean");
} catch (Exception e) {
System.err.println("Refused: " + e.getMessage());
}
}
}
// .NET 4.5.2+ disables DTD processing on XmlReader by default. Older
// configurations are vulnerable — set DtdProcessing.Prohibit explicitly.
using System.Xml;
using var client = new HttpClient();
var raw = await client.GetStringAsync("https://chaos.catastrophic.io/xml?mode=mismatched-tags");
var settings = new XmlReaderSettings {
DtdProcessing = DtdProcessing.Prohibit,
XmlResolver = null,
};
try {
using var reader = XmlReader.Create(new StringReader(raw), settings);
while (reader.Read()) { }
Console.WriteLine("Parsed clean");
} catch (XmlException e) {
Console.Error.WriteLine($"Refused: {e.Message}");
}
# Nokogiri::XML defaults to disallowing network access; DTD loading
# still happens unless NONET + NOENT are explicitly cleared.
require "net/http"
require "nokogiri"
resp = Net::HTTP.get_response(URI("https://chaos.catastrophic.io/xml?mode=mismatched-tags"))
raw = resp.body
puts "Mode: #{resp['X-Chaos-Xml-Mode']}"
doc = Nokogiri::XML(raw) { |c| c.strict.nonet }
puts "Errors: #{doc.errors}"
puts "Root: #{doc.root && doc.root.name}"
# [xml] casts via System.Xml.XmlDocument — strict by default; throws
# on malformed input. Use [xml]::new() with XmlResolver = $null to
# also reject external entity resolution.
$resp = Invoke-WebRequest -Uri 'https://chaos.catastrophic.io/xml?mode=mismatched-tags' -SkipHttpErrorCheck
$resp.Headers['X-Chaos-Xml-Mode']
try {
[xml]$resp.Content | Out-Null
'Parsed clean'
} catch {
"Refused: $($_.Exception.Message)"
}
headers
body