XXE in a legacy XML import parser
A supplier feed parser resolved external entities, exposing server-side file contents. Remediated by disabling entity loading, switching to a safe parser configuration and validating against a schema.
Context
A legacy PHP integration accepted XML supplier feeds and parsed them with a permissively configured parser. Reviewed as part of an authorised secure code review with a follow-up dynamic test in a lab copy of the application.
Scope
The XML ingestion path only: upload handling, parsing, and downstream mapping. No production data was used; the lab copy was seeded with synthetic records.
Vulnerability
The parser loaded external entities, allowing an XML document with a document type definition to reference local resources and include their contents in parsed output.
Root cause
The code used a parser configuration copied from an old tutorial, with entity substitution enabled and no schema validation. Nobody had revisited the integration since it was written, and there were no tests around it.
Impact
An attacker able to submit a feed could read server-side files readable by the web user and cause outbound requests from the server. In this codebase that included configuration paths, so the finding was rated High.
Evidence
Testing used a benign, non-sensitive marker file created for the purpose. No real configuration or credential file was read, and the proof of concept is not published.
Remediation
- Disabled DTD processing and external entity loading explicitly rather than relying on defaults.
- Removed entity substitution flags from the parsing call.
- Validated every incoming document against an XSD schema before mapping.
- Rejected documents containing a DOCTYPE declaration outright.
- Added a size limit and an entity expansion guard against billion-laughs style resource exhaustion.
libxml_set_external_entity_loader(static fn () => null);
$document = new DOMDocument();
$document->loadXML($xml, LIBXML_NONET | LIBXML_NOENT ^ LIBXML_NOENT);
if ($document->doctype !== null) {
throw new FeedRejected('DTD declarations are not accepted.');
}
Retesting
Retesting resubmitted the original document, a parameter-entity variant, an out-of-band variant pointing at a controlled lab listener, and a nested-entity expansion document. All were rejected before parsing. The listener recorded no connections. Valid supplier feeds continued to import correctly, verified by the regression suite added alongside the fix.
Lessons
- Parser defaults vary by version and platform; secure configuration must be explicit and asserted in a test.
- Rejecting DOCTYPE declarations is a cheap, high-value control for feeds that never legitimately need them.
- Legacy integrations deserve the same review cadence as new features — age is not a security property.
Authorisation
This work was carried out under written authorisation or inside an authorised training environment. All evidence has been sanitised.
Continue reading
Related case studies
IDOR: missing server-side authorisation in an invoice workflow
An authenticated user could modify an invoice identifier and attempt to access another customer's resource. The remediation enforced ownership at the database query layer and was verified with positive and negative authorisation tests.
5 min read
Secure payment webhooks: validating PayPal, Stripe and Flutterwave callbacks
A secure payment integration case study covering server-side verification, webhook authenticity, payment-state validation, duplicate processing, secrets management and safe failure handling.
8 min read
SSRF in a server-side document import feature
An authorised security lab examining how a user-controlled URL can cause a web server to make unintended requests to internal or restricted destinations, followed by layered remediation and retesting.
9 min read