Feb 10, 2026·6 min read·6 visits
The .NET `DataSet` class trusts inline XML schemas too much. Attackers can tell it to load a `XamlReader` disguised as a database column, leading to immediate RCE. This affects SharePoint, Visual Studio, and any .NET app using `ReadXml` on untrusted input.
A critical Remote Code Execution (RCE) vulnerability in the .NET Framework, SharePoint, and Visual Studio caused by unsafe handling of XML input in the `DataSet` and `DataTable` classes. By exploiting the `ReadXml` method, attackers can define arbitrary types via an inline schema, forcing the application to deserialize malicious gadgets that execute code in the context of the host process.
If you've been in the .NET world for a while, you remember DataSet and DataTable. They are the relics of a bygone era—bulky, memory-hungry objects designed to represent database tables in memory. They were the darlings of the Enterprise application stack in the mid-2000s. But like many things from that era (looking at you, SOAP), they have a fatal attraction to XML.
Here is the problem: DataSet doesn't just hold data; it holds metadata. It wants to know the schema of your data so it can enforce types. To make life easy for developers, Microsoft allowed DataSet to read this schema directly from the XML input via the ReadXml() method.
CVE-2020-1147 is the realization that if you let an attacker define the schema of the data they are sending you, they can make your application instantiate objects you never intended to exist. It's the deserialization vulnerability that keeps on giving, famously plaguing SharePoint and leading to the 'ToolShell' discoveries years later.
The root cause is a classic failure of trust boundaries. When DataSet.ReadXml() parses input, it looks for an inline XML schema (defined by xs:schema). Inside that schema, standard XML types are expected. However, Microsoft added a special attribute called msdata:DataType. This attribute tells the .NET runtime: "Hey, this column isn't just a string; it's actually a complex .NET object of this specific type."
Under the hood, DataSet sees this attribute and thinks, "Okay, the user says this column contains objects of type System.Windows.Markup.XamlReader. I should prepare the XmlSerializer to handle that."
This is where the logic falls apart. The application essentially takes a type name provided by the attacker and hands it over to the serializer. Unlike BinaryFormatter, which is dangerous by default, XmlSerializer is usually safe because it requires known types. But DataSet bypasses this safety by dynamically telling the serializer what types to expect based on the attacker's input. It creates a bridge between safe XML parsing and unsafe object instantiation.
> [!NOTE] > This isn't just about crashing the app. By specifying a gadget class, we can trick the serializer into setting properties on that class, triggering a chain reaction that ends in code execution.
So, we can instantiate a type. Great. But which type? We need a "gadget"—a class that does something dangerous when its properties are set or when it is initialized. Enter the ExpandedWrapper and XamlReader combo.
This specific attack chain is a work of art. We can't just instantiate Process.Start directly because XmlSerializer is picky. We need a wrapper. The gadget chain looks like this:
ExpandedWrapper: This is an internal class in System.Data.Services. It's a generic wrapper that allows us to bundle two items together. It's useful here because it's serializable and helps us satisfy type requirements.XamlReader: This is the nuclear option. The XamlReader class in WPF has a Parse() method. If you can pass a string of XAML to this method, it will compile and execute it. It's basically eval() for .NET UI markup.The vulnerability allows us to define a column in our fake DataTable with the type ExpandedWrapper<XamlReader, ObjectDataProvider>. When the serializer processes the row data, it hydrates this wrapper. Inside the wrapper, we place a payload that the XamlReader parses.
Here is a visualization of the flow:
Let's build the bomb. The payload consists of two parts: the Schema (the setup) and the DiffGram (the trigger).
First, we define the schema. We claim we are sending a DataSet named somedataset. Inside, we define a table Exp_Table with a column named pwn. The critical part is the msdata:DataType attribute, which points to our gadget chain:
<xs:element name="pwn"
msdata:DataType="System.Data.Services.Internal.ExpandedWrapper`2[[System.Windows.Markup.XamlReader, ...], [System.Windows.Data.ObjectDataProvider, ...]], ..."
type="xs:anyType" minOccurs="0"/>Next, we provide the data in the diffgr:diffgram block. This is where the actual serialized object lives. We wrap our malicious XAML inside a CDATA block effectively hidden inside the serialized stream intended for the XamlReader.
The XAML payload typically uses ObjectDataProvider to invoke System.Diagnostics.Process.Start. The final XML looks innocent enough to a firewall—just a bunch of schema definitions and data—but to the .NET runtime, it's a command execution instruction.
When DataSet.ReadXml(payload) is called:
ExpandedWrapper is instantiated.XamlReader wakes up, parses the command string, and pops a shell.Why was this such a big deal? Because DataSet is everywhere. It is the bedrock of legacy enterprise .NET applications.
SharePoint was the most high-profile victim. SharePoint relies heavily on XML and DataSet for internal communication and Web Parts. An attacker could send a crafted POST request to a vulnerable SharePoint endpoint and execute code as the service account (usually Network Service or a specific farm account). This typically means full compromise of the SharePoint farm.
The CVSS score of 7.8 (High) is slightly misleading; in many internal network scenarios or public-facing web apps using ReadXml on user uploads, this is a functional 9.8 (Critical). The EPSS score sits comfortably above 93%, meaning if you have this exposed, bots are likely already trying to exploit it.
Furthermore, this isn't just a "one and done" bug. As seen with the "ToolShell" research in 2025, incomplete patches and new gadget variations keep breathing life into this attack vector. Attackers leverage this to install webshells, dump databases, or pivot into the domain controller.
Microsoft's patch for CVE-2020-1147 introduced a type allowlist mechanism for DataSet and DataTable. Essentially, they stopped trusting msdata:DataType implicitly. If the type isn't on the list or clearly safe, the deserializer throws a tantrum and refuses to proceed.
However, patches are only as good as their application.
Developers must:
ReadXml(), stop. If you can't stop, use the overload that takes XmlReadMode. Avoid XmlReadMode.Auto or XmlReadMode.DiffGram on untrusted input.XmlReader that forbids DTD processing (DtdProcessing.Prohibit) and validate schemas strictly before passing them to DataSet.For Security Teams:
Write detection rules for the string System.Data.Services.Internal.ExpandedWrapper appearing in HTTP bodies. Normal traffic rarely, if ever, sends this specific internal type across the wire. It is a high-fidelity indicator of an attempted exploit.
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
.NET Framework Microsoft | 2.0 - 4.8 | July 2020 Security Rollup |
SharePoint Server 2019 Microsoft | < July 2020 Update | July 2020 PU |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-502 (Deserialization of Untrusted Data) |
| CVSS v3.1 | 7.8 (High) |
| Attack Vector | Network |
| EPSS Score | 0.9343 (93.43%) |
| Exploit Status | Active / Weaponized |
| Key Gadget | ExpandedWrapper + XamlReader |