CWE-611: XML External Entity (XXE) Injection - 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.
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.
- Enables file disclosure, SSRF, and entity expansion DoS.
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.
- 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:
- Explicitly enables DTD and external entity processing.
- Reintroduces file disclosure and SSRF risk.
DataContractSerializer with Unsafe Resolver
// VULNERABLE - Custom XmlResolver
var serializer = new DataContractSerializer(typeof(User));
var settings = new XmlReaderSettings
{
XmlResolver = new XmlUrlResolver() // Allows external resources!
};
using (var reader = XmlReader.Create(new StringReader(xml), settings))
{
var user = (User)serializer.ReadObject(reader);
}
Why this is vulnerable:
- XmlUrlResolver allows external resource resolution.
- Enables file disclosure and SSRF through XML payloads.
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.
- Prohibiting DTDs and nulling XmlResolver prevents XXE.
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 IActionResult ProcessXml([FromBody] string xml)
{
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)
{
return BadRequest($"Invalid XML: {ex.Message}");
}
}
}
// Configure model binding for XML (Startup.cs / Program.cs)
builder.Services.AddControllers()
.AddXmlSerializerFormatters(options =>
{
options.XmlReaderSettings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit,
XmlResolver = null
};
});
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; }
}
// web.config - Secure XML reader quotas
<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.Configuration;
public class ConfigManager
{
public static string GetSetting(string key)
{
// ConfigurationManager uses secure defaults in .NET Core
return ConfigurationManager.AppSettings[key];
}
// For custom XML config files:
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");
}
}
}