Mert Özen Further With Every Line
Security Web Security (OWASP Top 10) — #18

OWASP Top 10:2017: XXE, Insecure Deserialization and the Lessons That Remain

Mert Özen Nov 6, 2026 8 min 5 views
OWASP Top 10:2017: XXE, Insecure Deserialization and the Lessons That Remain

We look at the OWASP Top 10:2017 list, the web it was written for and what changed from 2013 to 2017. We see with a real request how .NET 10 rejects XXE by default, show in code why BinaryFormatter no longer compiles or runs, and compare the years when XSS had its own category with today's list.

Version note: This post covers the OWASP Top 10:2017; today's equivalents follow the OWASP Top 10:2025, and the code was tested with ASP.NET Core 10 (.NET 10).

Guess First

In the 2017 list, XML External Entities (XXE) stood on its own at number four. The 2025 list has no category with that name. If you send XML containing a DOCTYPE to the Bookstore API, and the code never touches the XML settings, what does .NET 10 do: process the document or reject it?

We will see the answer with a real request shortly. With this post we move to the second block of the series, the evolution of the list across editions. The first stop is the OWASP Top 10:2017: what was on the list, which web was it written for, and which of that year's lessons still hold today? As always, the code runs only on your own machine; only try these techniques on systems you own or have written permission to test.

The 2017 List

The OWASP Top 10:2017 was published in November 2017; the first release candidate had come out in April 2017. It is also the only edition that has an official PDF on the project page today. Back then the numbers had a single digit: A1, not A01.

A1 Injection was at the top: untrusted data reaching an interpreter such as SQL, an OS command or LDAP as part of a command. A2 Broken Authentication collected session and authentication mistakes. A3 Sensitive Data Exposure was about sensitive data stored or transmitted without protection. A4 XML External Entities (XXE) covered flaws caused by XML processors resolving external entities. A5 Broken Access Control was about users reaching data or operations they had no right to.

A6 Security Misconfiguration meant default settings, error pages left open and missing hardening. A7 Cross-Site Scripting (XSS) was its own category. A8 Insecure Deserialization was the risk of building objects from untrusted data. A9 Using Components with Known Vulnerabilities was about depending on components with known flaws. A10 Insufficient Logging & Monitoring was the missing logging and monitoring that makes it hard to notice an attack and respond to it.

What stands out is that most categories name a symptom or a technology. "Sensitive Data Exposure" describes an outcome, "XML External Entities" a specific format. In the next post we will see how 2021 turned this view toward root causes.

The Web of Those Years

2017 was a time when single-page apps and JSON APIs were spreading fast, but enterprise integrations still talked mostly over XML and SOAP. In the .NET world most projects ran on .NET Framework; ASP.NET Core was new. Applications that rendered HTML on the server were the majority, so it was natural for XSS to have its own category. On the Java side, object serialization and XML configuration were everywhere; XXE and insecure deserialization entering the list fit that picture.

The same year showed why A9 mattered: the Equifax incident exploited CVE-2017-5638 in Apache Struts 2. OWASP still cites this example today on the A03:2025 Software Supply Chain Failures page. So the 2017 idea of "a component with a known vulnerability" grew, by 2025, into a category covering the whole supply chain.

From 2013 to 2017

The 2017 list was built on the 2013 edition. Here I will only mention the changes you see when you put the two documents side by side. Insecure Direct Object References and Missing Function Level Access Control, separate in 2013, were merged under A5 Broken Access Control in 2017. What we call IDOR today stayed under that roof, and it has been number one on the list since 2021. Cross-Site Request Forgery (CSRF) and Unvalidated Redirects and Forwards left the list in 2017. XXE, Insecure Deserialization and Insufficient Logging & Monitoring were new entries.

CSRF leaving the list did not mean "problem solved". Today CWE-352 (CSRF) is among the CWEs mapped to A01:2025 Broken Access Control. Frameworks making antiforgery support the default and browsers' SameSite cookies reduced the risk, but every application that authenticates with cookies still has to answer this question.

XXE: .NET 10's Closed Door

Let's add an endpoint to the Bookstore API that imports books from XML. We write nothing security-related in the XML settings:

using System.Xml;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails();
WebApplication app = builder.Build();

app.UseExceptionHandler();

app.MapPost("/books/import", async (HttpRequest request, ILogger<Program> logger) =>
{
    List<string> titles = [];
    XmlReaderSettings settings = new() { Async = true };
    try
    {
        using XmlReader reader = XmlReader.Create(request.Body, settings);
        while (await reader.ReadAsync())
        {
            if (reader.NodeType == XmlNodeType.Element && reader.Name == "title")
            {
                titles.Add(await reader.ReadElementContentAsStringAsync());
            }
        }
    }
    catch (XmlException ex)
    {
        // details go to the log, the client gets a generic message
        logger.LogWarning("Rejected XML import: {Reason}", ex.Message);
        return Results.Problem(statusCode: StatusCodes.Status400BadRequest, title: "Invalid XML.");
    }
    return Results.Ok(new { Imported = titles });
});

app.Run();

First we send a normal document, then a document with a DOCTYPE that declares a harmless internal entity:

$ cat books.xml
<books><book><title>Clean Code</title></book></books>
$ curl -i -X POST http://localhost:5181/books/import -H "Content-Type: application/xml" --data-binary @books.xml
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
...
{"imported":["Clean Code"]}

$ cat books-dtd.xml
<?xml version="1.0"?>
<!DOCTYPE books [<!ENTITY publisher "Lab Press">]>
<books><book><title>&publisher;</title></book></books>
$ curl -i -X POST http://localhost:5181/books/import -H "Content-Type: application/xml" --data-binary @books-dtd.xml
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
...
{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1","title":"Invalid XML.","status":400,"traceId":"00-c2835a5c...-00"}

The server log states the reason plainly:

warn: Program[0]
      Rejected XML import: For security reasons DTD is prohibited in this XML document. To enable DTD processing set the DtdProcessing property on XmlReaderSettings to Parse and pass the settings into XmlReader.Create method.

The answer to the opening question: it rejects it. The default DtdProcessing value of XmlReaderSettings is Prohibit. When I printed the defaults on the same machine, MaxCharactersFromEntities was 10,000,000; that is a ceiling against "billion laughs" style attacks that blow up memory through entity expansion. In my test, even after switching DtdProcessing to Parse, external resources were not resolved by default; that requires assigning an XmlResolver on top. So the 2017 lesson is now built into the framework's defaults. Your job is not to change these two settings without a reason, and if you don't really need DTDs (most APIs don't), to reject such documents as they are.

In 2021 XXE was folded into A05:2021 Security Misconfiguration; in 2025 CWE-611 is still under A02:2025 Security Misconfiguration. The category name disappeared because the problem is not XML itself but a dangerous feature left switched on.

Insecure Deserialization: BinaryFormatter Removed

The best-known .NET example of A8:2017 was BinaryFormatter: from an untrusted byte array it produced objects of whatever type was written inside. Let's see what happens in .NET 10:

using System.Runtime.Serialization.Formatters.Binary;

BinaryFormatter formatter = new();
formatter.Serialize(new MemoryStream(), "Clean Code");
$ dotnet build
Program.cs(3,1): error SYSLIB0011: 'BinaryFormatter' is obsolete: 'BinaryFormatter serialization is obsolete and should not be used. See https://aka.ms/binaryformatter for more information.' (https://aka.ms/dotnet-warnings/SYSLIB0011)

The code does not even compile. Suppressing the diagnostic and running it does not change the outcome:

using System.Runtime.Serialization.Formatters.Binary;

#pragma warning disable SYSLIB0011
BinaryFormatter formatter = new();
try
{
    formatter.Serialize(new MemoryStream(), "Clean Code");
}
catch (PlatformNotSupportedException ex)
{
    Console.WriteLine($"{ex.GetType().Name}: {ex.Message}");
}
$ dotnet run
PlatformNotSupportedException: BinaryFormatter serialization and deserialization have been removed. See https://aka.ms/binaryformatter for more information.

Since .NET 9 the runtime contains no implementation of BinaryFormatter; the APIs are still there, but every call throws this exception. The only way back is a separate, unsupported NuGet package. The lesson: any serializer that accepts type information together with the data carries the same risk. System.Text.Json does no polymorphism based on type names; it only accepts the types you allow with [JsonDerivedType]. Setting Newtonsoft.Json's TypeNameHandling to anything other than None rewrites the mistake of 2017 today. This category was merged into A08 Software and Data Integrity Failures in 2021, and CWE-502 sits under A08:2025 today.

The Years When XSS Had Its Own Category

In 2017 XSS had its own category, which suited the application structure of those years: HTML was rendered on the server, and forgetting to encode was the most common mistake. Razor's automatic HTML encoding with @value largely closed that mistake by default on the .NET side. Angular also always escapes interpolation.

In 2021 XSS was folded into A03:2021 Injection; in 2025 CWE-79 is under A05:2025 Injection and, according to OWASP's data, with more than 30,000 CVEs it is the most crowded CWE in that category. So XSS stopped being a separate heading, but it did not shrink. What changed is the view: SQL injection and XSS come from the same root cause, data treated as code by an interpreter. We discuss that in detail in the next post.

Common Pitfalls

The first is reading "A4" in an old security document against today's list. A4 in 2017 is XXE; A04 in 2025 is Cryptographic Failures. Always read and write the number with its year.

The second is assuming "if it left the list, it doesn't matter". CSRF left in 2017, XXE and XSS were folded into other categories in 2021; all three are still on today's list as CWEs. As the list got shorter, the categories got wider.

The third is switching off a framework's safe default because "it didn't work somewhere". Copying DtdProcessing = DtdProcessing.Parse or #pragma warning disable SYSLIB0011 from Stack Overflow brings back two 2017 categories in a single line. Flag these lines separately in code review.

The fourth is assuming old code ported from .NET Framework behaves like the new defaults. When porting XmlDocument, XmlTextReader and serializer usage from old projects, review them together with their settings.

Checklist

In every endpoint that accepts XML, use XmlReader with default settings or explicitly with DtdProcessing.Prohibit; don't assign an XmlResolver. Put a limit on the body size. Write error details to the log and return a generic message to the client. Don't use BinaryFormatter or its compatibility package; keep type-name polymorphism off in JSON, and if you use Newtonsoft, leave it at TypeNameHandling.None. Justify every use of @Html.Raw in Razor and bypassSecurityTrust* in Angular. Update category numbers in old documents together with their year.

A Small Experiment

★☆☆ Run the /books/import endpoint from this post in your own lab and see that the document without a DOCTYPE is accepted and the one with a DOCTYPE is rejected with 400. Then find the log line and compare it with the response that reached the client.

★★☆ Add a ceiling with MaxCharactersInDocument to the endpoint and limit the body size with RequestSizeLimit. Watch which layer kicks in first with a large document.

★★★ Build a table that maps the ten items of the 2017 list to their counterparts in the 2025 list (using what you found in this post and your own research). Which item changed its name, which was merged into another category, and which one's number shifted?

In the next post we move from 2017 to 2021: the turn from symptoms to root causes, Broken Access Control rising to number one, the arrival of Insecure Design and SSRF, and how the methodology changed.