Safeguard
Industry Analysis

XXE Prevention in C# by Disabling XmlResolver/DTD Processing

XXE in C# lives at the XmlResolver and DtdProcessing settings. Here's how .NET's defaults evolved since 2014 and exactly how to lock down XmlDocument, XmlTextReader, and XmlReaderSettings.

Aman Khan
AppSec Engineer
7 min read

In 2014, a researcher earned a $33,500 bug bounty from Facebook after smuggling a malicious DOCTYPE declaration into an XML parser and reading files off an internal server. The bug class behind that payout — XML External Entity (XXE) injection, tracked today as CWE-611 — has never gone away; it just keeps surfacing in new codebases. In C# and the .NET ecosystem, the same failure mode appears whenever XmlDocument, XmlTextReader, or a hand-built XmlReaderSettings object is allowed to resolve external entities and DTDs on its own. Microsoft has spent over a decade hardening .NET's defaults, starting with .NET Framework 4.5.2 in May 2014 and continuing through the safe-by-default behavior shipped in .NET Core and .NET 5+. Yet legacy applications, third-party XML wrappers, and misconfigured XmlReaderSettings keep XXE showing up in security assessments. This post explains why XmlResolver is the crux of the problem, how .NET's defaults evolved, and exactly how to lock down DTD processing in production C# code.

What Is an XXE Attack, and Why Does It Matter for C# Applications?

An XXE attack happens when an XML parser processes a Document Type Definition (DTD) that declares an external entity, letting an attacker read local files, trigger server-side request forgery (SSRF) against internal endpoints, or exhaust memory through recursive entity expansion — and C#'s System.Xml stack has historically parsed DTDs without asking. A textbook payload looks like this:

<?xml version="1.0"?>
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///C:/Windows/win.ini"> ]>
<foo>&xxe;</foo>

If a vulnerable XmlDocument or XmlTextReader parses this, the contents of win.ini get pulled into the &xxe; entity and can end up echoed back in an API response, a log file, or an error message. The class of bug is old — it was formalized in OWASP's Top 10 as A4:2017-XML External Entities and later folded into A05:2021-Security Misconfiguration — but it remains relevant to C# specifically because .NET's XML APIs shipped with insecure defaults for well over a decade, and a large share of enterprise .NET code (WCF services, SOAP endpoints, SAML consumers, RSS/Atom parsers) still runs on framework versions or coding patterns from that era.

Why Is XmlResolver the Root Cause of XXE in .NET?

XmlResolver is the root cause because it's the single component every .NET XML class delegates to whenever a DTD or external entity references a URI, and for years several core classes instantiated a working one — XmlUrlResolver — automatically. XmlUrlResolver will happily fetch file://, http://, and ftp:// URIs on the parser's behalf, which is exactly the primitive an XXE payload needs to read files or reach internal services. Setting XmlResolver to null is the single most effective mitigation available: even if DTD parsing is still technically enabled, a null resolver has nothing to hand the entity reference to, so resolution simply fails instead of executing. That's why Microsoft's own security guidance and the OWASP XXE Prevention Cheat Sheet both treat XmlResolver = null as the non-negotiable first line of defense, ahead of — not instead of — disabling DTD processing outright.

When Did Microsoft Change .NET's Default DTD Behavior?

Microsoft shipped its first major fix in .NET Framework 4.5.2, released in May 2014, which changed the default DtdProcessing setting used by XmlReader.Create(XmlReaderSettings) from Parse to Prohibit, causing an XmlException the moment a DOCTYPE is encountered. The second major fix arrived with .NET Core 1.0 in 2016 and carried forward into .NET 5, 6, 8, and beyond: XmlDocument.XmlResolver now defaults to null platform-wide, instead of the implicit XmlUrlResolver instance .NET Framework used. The practical version breakdown looks like this:

  • .NET Framework 1.0 – 4.5.1: XmlDocument and XmlTextReader resolve external entities by default — vulnerable out of the box.
  • .NET Framework 4.5.2 and later: XmlReaderSettings-based readers default to DtdProcessing.Prohibit, but XmlDocument.XmlResolver still defaults to a live XmlUrlResolver unless a developer explicitly nulls it out.
  • .NET Core 1.0+ / .NET 5, 6, 8, 9, 10: XmlDocument.XmlResolver defaults to null; DTD parsing is disabled by default across the board.

That middle row is the trap. Plenty of teams that migrated off old .NET Framework versions assume the platform "fixed XXE," when in reality XmlDocument on .NET Framework — even on 4.8.1, the latest release — is still exploitable the moment a resolver is assigned or the code runs on a version predating 4.5.2.

How Do You Correctly Disable XmlResolver and DTD Processing in C#?

You disable both by explicitly setting XmlResolver to null and DtdProcessing to Prohibit (or Ignore) on every parser entry point, rather than relying on framework defaults you haven't verified. For XmlDocument:

var doc = new XmlDocument();
doc.XmlResolver = null; // blocks external entity and DTD resolution
doc.LoadXml(untrustedXml);

For XmlReaderSettings, which backs XmlReader, XDocument.Load, and most LINQ to XML paths:

var settings = new XmlReaderSettings
{
    DtdProcessing = DtdProcessing.Prohibit,
    XmlResolver = null
};

using var reader = XmlReader.Create(untrustedStream, settings);
var doc = XDocument.Load(reader);

For the legacy XmlTextReader class, which many pre-2015 codebases still use directly:

using var reader = new XmlTextReader(untrustedStream)
{
    DtdProcessing = DtdProcessing.Prohibit,
    XmlResolver = null
};

Note that XDocument.Load(string) and XDocument.Load(Stream) overloads without an explicit XmlReaderSettings inherit whatever the underlying XmlReader implementation defaults to — on .NET Framework that can still mean DTD parsing is silently permitted. The safest pattern is to never call the parameterless overloads on untrusted input; always construct the XmlReader yourself with DtdProcessing.Prohibit and XmlResolver = null set explicitly, regardless of which .NET version you're targeting.

Which .NET XML Entry Points Are Still Risky in 2026?

Three call sites remain risky even on current .NET releases: XmlDocument running under .NET Framework 4.5.1 or earlier, direct XmlTextReader instantiation without explicit settings, and third-party libraries that wrap System.Xml internally without exposing a way to configure the resolver. SAML is the recurring real-world example — identity federation libraries parse SAML assertions as XML, and several older SAML consumer libraries for .NET shipped with permissive parsing that made SAML endpoints a favorite XXE target during the mid-2010s. Office Open XML processing (DOCX, XLSX are ZIP archives of XML) and RSS/Atom feed ingestion are the other two categories that consistently turn up in code review, because developers reach for XmlDocument for quick file parsing and don't think of a spreadsheet upload as "untrusted XML." Any code path where a C# application deserializes XML supplied by a user, a partner API, or an uploaded file — not just obvious API bodies — needs the same XmlResolver = null / DtdProcessing.Prohibit treatment.

How Safeguard Helps

Safeguard closes the gap between "Microsoft fixed the default" and "your codebase is actually safe" by scanning C# repositories and their dependency trees for the exact insecure patterns described above — XmlDocument instances without XmlResolver = null, XmlTextReader usage lacking explicit DtdProcessing settings, and transitive packages that parse XML through unvetted wrappers. Because XXE risk in .NET is as much a supply chain problem as a first-party coding problem — a single vendored SAML or Office-document library can reintroduce the vulnerability across dozens of consuming services — Safeguard correlates SBOM data with known-vulnerable XML parsing behavior in third-party components, not just your own source. Findings are enforced as policy gates in CI/CD, so a pull request that instantiates XmlDocument without disabling XmlResolver, or upgrades a dependency with a documented XXE advisory, is flagged before merge rather than discovered in a pen test. That turns a decade-old, well-understood vulnerability class into something your pipeline catches automatically, instead of something that resurfaces every time a new engineer copies an old XML-parsing snippet.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.