CWE-611: Improper Restriction of XML External Entity Reference - C# / .NET
Overview
XXE vulnerabilities in .NET occur when XML parsers process external entity references in untrusted XML. While modern .NET versions have safer defaults, older code and misconfigured parsers remain vulnerable.
Primary Defence: Parse untrusted XML through an XmlReader created with explicit XmlReaderSettings: set DtdProcessing = DtdProcessing.Prohibit, set XmlResolver = null, and pass that reader into XmlDocument, XDocument, serializers, and validators instead of relying on parser defaults.
Which Defaults Are Safe, and Since When
"Safe by default" is dated per API, not per runtime, so the patterns below are not all vulnerable on every version. Three separate dates:
XmlReaderSettings.DtdProcessingdocuments "The default isProhibit" - the property has behaved that way since it was introduced in .NET Framework 4.0.XmlReaderSettings.XmlResolverdefaulted to "a newXmlUrlResolverwith no credentials" until .NET Framework 4.5.2, from which "this setting has a default value ofnull".XmlDocumentis the exception. Microsoft documents no null default forXmlDocument.XmlResolver, and states that "if the document was not loaded using anXmlReader... theXmlResolveron theXmlDocumentis always used" - soLoadXmlon a stream or a file resolves external entities on .NET Framework at any version.
The last one is where the runtime does matter. On .NET Core and .NET 5+, XmlDocument holds no resolver unless one was set and passes none to the reader it builds, so external entities are not resolved by default. DTD parsing is still on there, which leaves entity-expansion denial of service rather than file disclosure or SSRF. None of this removes the need for the explicit settings above: a default is a property of the version you happen to be running, and the finding you are fixing is usually in code that will outlive it.
Common Vulnerable Patterns
XmlDocument with Default Settings
// VULNERABLE - XmlDocument allows DTDs by default (.NET Framework)
using System.Xml;
public void ParseXml(string xml)
{
var doc = new XmlDocument();
doc.LoadXml(xml); // DANGEROUS in .NET Framework!
// Attacker can read files:
// <?xml version="1.0"?>
// <!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///C:/Windows/win.ini">]>
// <root>&xxe;</root>
}
Why this is vulnerable:
- DTDs and external entities are processed by default in .NET Framework, at every version -
XmlDocumentnever got a safe resolver default, as above. - Enables file disclosure, SSRF, and entity expansion DoS. On .NET Core and .NET 5+ only the last of those, because the resolver is null unless set.
XmlTextReader Without ProhibitDtd
// VULNERABLE - XmlTextReader in .NET Framework
public void ReadXml(string xml)
{
using (var reader = new XmlTextReader(new StringReader(xml)))
{
// DTD processing enabled by default
while (reader.Read())
{
// Process XML
}
}
}
Why this is vulnerable:
- DTDs are enabled by default in .NET Framework;
XmlTextReaderthere carries anXmlUrlResolverunless one is assigned. - Enables file disclosure, SSRF, and DoS.
XmlReader with Unsafe Settings
// VULNERABLE - DtdProcessing.Parse allows DTDs
var settings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Parse // DANGEROUS!
};
using (var reader = XmlReader.Create(new StringReader(xml), settings))
{
// Can process external entities
}
Why this is vulnerable:
DtdProcessing.Parseturns DTD processing back on, which reopens entity-expansion DoS.- External entities resolve on top of that wherever the settings carry a resolver - before .NET Framework 4.5.2, or wherever one has been assigned - and that is where file disclosure and SSRF come from.
DataContractSerializer with DTD Processing and a Live Resolver
// VULNERABLE - DTD processing enabled together with a live XmlResolver
var serializer = new DataContractSerializer(typeof(User));
var settings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Parse, // without this the DOCTYPE is rejected
XmlResolver = new XmlUrlResolver() // this is what fetches the entity
};
using (var reader = XmlReader.Create(new StringReader(xml), settings))
{
var user = (User)serializer.ReadObject(reader);
}
Why this is vulnerable:
- Neither setting is sufficient on its own.
XmlReaderSettings.DtdProcessingdefaults toProhibit, so with only the resolver assigned the parser rejects the DOCTYPE and never consults it - the reader reports "For security reasons DTD is prohibited in this XML document". - Setting both is what opens the weakness: the DOCTYPE is parsed and the
XmlUrlResolverfetches the target, so&xxe;comes back as the contents of the referenced file. That is the file disclosure and SSRF path, and it is why the fix nulls the resolver as well as prohibiting DTDs.
Secure Patterns
XmlDocument with Secure XmlReader
// SECURE - Load XmlDocument through a hardened XmlReader
using System.Xml;
public void ParseXmlSecure(string xml)
{
var settings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit,
XmlResolver = null,
MaxCharactersFromEntities = 1024,
MaxCharactersInDocument = 10_000_000
};
using var reader = XmlReader.Create(new StringReader(xml), settings);
var doc = new XmlDocument { XmlResolver = null };
doc.Load(reader);
// Process safely
var name = doc.SelectSingleNode("//name")?.InnerText;
}
Why this works:
DtdProcessing.Prohibitrejects DOCTYPE before the DOM is built.XmlResolver = nullblocks external resource resolution if settings are later changed.
XmlReader with DtdProcessing.Prohibit
// SECURE - Explicitly prohibit DTDs
using System.Xml;
public void ReadXmlSecure(string xml)
{
var settings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit, // Reject DTDs entirely
XmlResolver = null, // No external entity resolution
MaxCharactersFromEntities = 1024,
MaxCharactersInDocument = 10_000_000
};
using (var reader = XmlReader.Create(new StringReader(xml), settings))
{
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element && reader.Name == "name")
{
string name = reader.ReadElementContentAsString();
// Process safely
}
}
}
}
Why this works:
DtdProcessing.Prohibitrejects DOCTYPE outright.XmlResolver = nullblocks external fetches even if misconfigured.MaxCharactersFromEntitiesandMaxCharactersInDocumentprovide resource limits if DTD handling is later changed.
XDocument with Secure Settings
// SECURE - XDocument with safe XmlReader
using System.Xml.Linq;
using System.Xml;
public XDocument ParseXmlToXDocument(string xml)
{
var settings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit,
XmlResolver = null
};
using (var reader = XmlReader.Create(new StringReader(xml), settings))
{
return XDocument.Load(reader);
}
}
// Usage:
var doc = ParseXmlToXDocument(untrustedXml);
var name = doc.Root?.Element("name")?.Value;
Why this works:
- XDocument inherits the secure XmlReader settings.
DtdProcessing.Prohibitrejects the DOCTYPE, andXmlResolver = nullleaves nothing to fetch an external entity with.
XmlSerializer with Secure Reader
// SECURE - XmlSerializer with safe XmlReader
using System.Xml.Serialization;
public T DeserializeXml<T>(string xml)
{
var serializer = new XmlSerializer(typeof(T));
var settings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit,
XmlResolver = null,
MaxCharactersFromEntities = 1024,
MaxCharactersInDocument = 10_000_000
};
using (var reader = XmlReader.Create(new StringReader(xml), settings))
{
return (T)serializer.Deserialize(reader);
}
}
// Usage:
var user = DeserializeXml<User>(xmlString);
Why this works:
- A hardened XmlReader prevents DTDs and external entities.
- Works for any type without relying on default reader settings.
Framework-Specific Guidance
ASP.NET Core
// SECURE - ASP.NET Core API endpoint
using Microsoft.AspNetCore.Mvc;
using System.Xml;
[ApiController]
[Route("api/[controller]")]
public class DataController : ControllerBase
{
[HttpPost]
[Consumes("application/xml")]
public async Task<IActionResult> ProcessXml()
{
// Read the raw body. Do not bind it with [FromBody] string - with the
// XML formatters registered, that expects a serialized <string>
// element, not an XML document, so the document never arrives intact.
using var body = new StreamReader(Request.Body);
var xml = await body.ReadToEndAsync();
try
{
var settings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit,
XmlResolver = null,
MaxCharactersFromEntities = 1024,
MaxCharactersInDocument = 10_000_000
};
using (var reader = XmlReader.Create(new StringReader(xml), settings))
{
var doc = new XmlDocument { XmlResolver = null };
doc.Load(reader);
// Process XML
var result = ProcessDocument(doc);
return Ok(result);
}
}
catch (XmlException ex)
{
// Do not echo ex.Message - parser errors quote document content
return BadRequest("Invalid XML");
}
}
}
Model binding needs no hardening, and cannot be hardened the way it is often written. AddXmlSerializerFormatters() takes an Action<MvcXmlOptions> overload, but MvcXmlOptions has no XmlReaderSettings property - assigning one does not compile. There is nothing to configure because the default is already strict: XmlSerializerInputFormatter builds its reader from XmlDictionaryReader, which has no DTD support at all and rejects a document the moment it meets <!DOCTYPE:
plain document ACCEPTED
XXE file entity rejected: XmlException
DOCTYPE, no entity rejected: XmlException
The error it raises is CData elements not valid at top level of an XML document, which is misleading enough to be worth recognising - it means the reader hit a <! construct it does not implement, not that the document contains CDATA. If you do need to change how the body is read, the extension point is XmlSerializerInputFormatter.CreateXmlReader(Stream, Encoding), which is virtual for that purpose.
WCF Services
// SECURE - WCF with DataContractSerializer
using System.ServiceModel;
using System.Runtime.Serialization;
[ServiceContract]
public interface IUserService
{
[OperationContract]
User GetUser(int id);
}
[DataContract]
public class User
{
[DataMember]
public string Name { get; set; }
[DataMember]
public string Email { get; set; }
}
WCF's DataContractSerializer reads through XmlDictionaryReader, which has no DTD support, so the XXE paths are closed before configuration. The reader quotas below are a separate control - they bound message size and nesting depth against resource exhaustion, and do not affect entity handling:
<!-- web.config - reader quotas bound message size, not entity behaviour -->
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="SecureBinding">
<readerQuotas maxDepth="32"
maxStringContentLength="8192"
maxArrayLength="16384"
maxBytesPerRead="4096"
maxNameTableCharCount="16384" />
<security mode="Transport" />
</binding>
</basicHttpBinding>
</bindings>
</system.serviceModel>
XML Configuration Files
// SECURE - Reading app configuration XML
using System.Xml;
using Microsoft.Extensions.Configuration;
public class ConfigManager
{
// On .NET Core and later, configuration comes from IConfiguration
// (appsettings.json, environment, secrets) rather than the .NET Framework
// ConfigurationManager/app.config model. Neither reads untrusted XML, so
// neither is the XXE exposure - a custom XML config file is.
private readonly IConfiguration _configuration;
public ConfigManager(IConfiguration configuration) => _configuration = configuration;
public string? GetSetting(string key) => _configuration[key];
// For custom XML config files, harden the reader as anywhere else
public static void LoadCustomConfig(string configPath)
{
var settings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit,
XmlResolver = null
};
using (var reader = XmlReader.Create(configPath, settings))
{
var doc = new XmlDocument { XmlResolver = null };
doc.Load(reader);
// Process configuration
}
}
}
Reusable Secure XML Utility
// Utility class for secure XML operations
public static class SecureXmlHelper
{
/// <summary>
/// Creates secure XmlReaderSettings
/// </summary>
public static XmlReaderSettings GetSecureSettings()
{
return new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit,
XmlResolver = null,
MaxCharactersFromEntities = 1024,
MaxCharactersInDocument = 10000000 // 10MB limit
};
}
/// <summary>
/// Parse XML string to XmlDocument securely
/// </summary>
public static XmlDocument ParseXml(string xml)
{
var settings = GetSecureSettings();
using (var reader = XmlReader.Create(new StringReader(xml), settings))
{
var doc = new XmlDocument { XmlResolver = null };
doc.Load(reader);
return doc;
}
}
/// <summary>
/// Parse XML string to XDocument securely
/// </summary>
public static XDocument ParseToXDocument(string xml)
{
var settings = GetSecureSettings();
using (var reader = XmlReader.Create(new StringReader(xml), settings))
{
return XDocument.Load(reader);
}
}
/// <summary>
/// Deserialize XML to object securely
/// </summary>
public static T Deserialize<T>(string xml)
{
var serializer = new XmlSerializer(typeof(T));
var settings = GetSecureSettings();
using (var reader = XmlReader.Create(new StringReader(xml), settings))
{
return (T)serializer.Deserialize(reader);
}
}
}
// Usage:
var doc = SecureXmlHelper.ParseXml(untrustedXml);
var xdoc = SecureXmlHelper.ParseToXDocument(untrustedXml);
var user = SecureXmlHelper.Deserialize<User>(xmlString);
Input Validation
// Validate XML content after parsing
using System.Xml.Schema;
public class XmlValidator
{
public static bool ValidateAgainstSchema(string xml, string xsdPath)
{
var settings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit,
XmlResolver = null,
ValidationType = ValidationType.Schema
};
// Load schema
settings.Schemas.Add(null, xsdPath);
settings.ValidationEventHandler += (sender, args) =>
{
throw new XmlSchemaValidationException(args.Message);
};
using (var reader = XmlReader.Create(new StringReader(xml), settings))
{
while (reader.Read()) { }
return true;
}
}
public static void ValidateContent(XmlDocument doc)
{
// Validate expected structure
var root = doc.DocumentElement;
if (root == null || root.Name != "user")
{
throw new InvalidOperationException("Invalid XML structure");
}
var name = root.SelectSingleNode("name")?.InnerText;
if (string.IsNullOrWhiteSpace(name) || name.Length > 100)
{
throw new InvalidOperationException("Invalid name");
}
var email = root.SelectSingleNode("email")?.InnerText;
if (string.IsNullOrWhiteSpace(email) || !email.Contains("@"))
{
throw new InvalidOperationException("Invalid email");
}
}
}
Common Pitfalls
- Hardening one parsing path with a secure
XmlReaderforXmlDocument.Load(XmlReader)while another call site in the same codebase still usesXmlDocument.LoadXml(string)or.Load(string path)directly - those overloads don't acceptXmlReaderSettings, so that call falls back to the type's own default DTD/entity behavior instead of the hardened settings applied elsewhere. - Setting
XmlResolver = nullon theXmlReaderSettingsbut not on theXmlDocument/XmlTextReaderinstance itself -XmlDocument.XmlResolveris a separate property from the reader's, and on .NET Framework it carries a liveXmlUrlResolverat every version; both need to be nulled out, which is why the secure pattern setsnew XmlDocument { XmlResolver = null }in addition to the reader settings. - Assuming a
Deserialize(Stream)/ReadObject(Stream)overload picks up the hardenedXmlReaderSettingsyou built for another call site. It does not - the serializer supplies its own reader. On current .NET that reader is not an XXE hole:XmlSerializer.Deserialize(Stream)rejects the DOCTYPE with "For security reasons DTD is prohibited", andReadObject(Stream)reads throughXmlDictionaryReader, which has no DTD support at all. The cost is that the call site's behaviour is the framework's default rather than the settings you reviewed -XmlSerializer.Deserialize(TextReader)accepts the DOCTYPE and yields an empty value instead of raising.