Skip to content

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.DtdProcessing documents "The default is Prohibit" - the property has behaved that way since it was introduced in .NET Framework 4.0.
  • XmlReaderSettings.XmlResolver defaulted to "a new XmlUrlResolver with no credentials" until .NET Framework 4.5.2, from which "this setting has a default value of null".
  • XmlDocument is the exception. Microsoft documents no null default for XmlDocument.XmlResolver, and states that "if the document was not loaded using an XmlReader ... the XmlResolver on the XmlDocument is always used" - so LoadXml on 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 - XmlDocument never 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; XmlTextReader there carries an XmlUrlResolver unless 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.Parse turns 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.DtdProcessing defaults to Prohibit, 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 XmlUrlResolver fetches 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.Prohibit rejects DOCTYPE before the DOM is built.
  • XmlResolver = null blocks 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.Prohibit rejects DOCTYPE outright.
  • XmlResolver = null blocks external fetches even if misconfigured.
  • MaxCharactersFromEntities and MaxCharactersInDocument provide 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.Prohibit rejects the DOCTYPE, and XmlResolver = null leaves 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 XmlReader for XmlDocument.Load(XmlReader) while another call site in the same codebase still uses XmlDocument.LoadXml(string) or .Load(string path) directly - those overloads don't accept XmlReaderSettings, so that call falls back to the type's own default DTD/entity behavior instead of the hardened settings applied elsewhere.
  • Setting XmlResolver = null on the XmlReaderSettings but not on the XmlDocument/XmlTextReader instance itself - XmlDocument.XmlResolver is a separate property from the reader's, and on .NET Framework it carries a live XmlUrlResolver at every version; both need to be nulled out, which is why the secure pattern sets new XmlDocument { XmlResolver = null } in addition to the reader settings.
  • Assuming a Deserialize(Stream)/ReadObject(Stream) overload picks up the hardened XmlReaderSettings you 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", and ReadObject(Stream) reads through XmlDictionaryReader, 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.

Additional Resources