CWE-502: Insecure Deserialization - C# / .NET
Overview
Insecure deserialization in .NET can lead to remote code execution when untrusted data is deserialized using formatters like BinaryFormatter, NetDataContractSerializer, or ObjectStateFormatter. These formatters can instantiate arbitrary types and execute code during deserialization.
Primary Defence: Use System.Text.Json (JSON serialization) or DataContractSerializer with known types instead of BinaryFormatter. Never use BinaryFormatter with untrusted data.
Common Vulnerable Patterns
BinaryFormatter (DANGEROUS - Never Use!)
// VULNERABLE - BinaryFormatter with untrusted data
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
public class UserService
{
public object LoadUser(byte[] data)
{
var formatter = new BinaryFormatter();
using (var ms = new MemoryStream(data))
{
return formatter.Deserialize(ms); // RCE vulnerability!
}
}
}
Why this is vulnerable:
- Instantiates arbitrary .NET types from untrusted input.
- Executes constructors, property setters, and callbacks during deserialization.
- Enables gadget chains that lead to remote code execution.
- Attackers can craft payloads to trigger dangerous side effects.
NetDataContractSerializer
// VULNERABLE - Can deserialize any type
using System.IO;
using System.Runtime.Serialization;
public object Deserialize(byte[] data)
{
var serializer = new NetDataContractSerializer();
using (var ms = new MemoryStream(data))
{
return serializer.Deserialize(ms); // DANGEROUS!
}
}
Why this is vulnerable:
- Embeds type metadata in the payload.
- Attackers can choose arbitrary types to instantiate.
- Gadget chains can execute code during object creation.
- No safe configuration for untrusted input.
JavaScriptSerializer with Type Resolution
// VULNERABLE - Type resolver enables RCE
using System.Web.Script.Serialization;
var serializer = new JavaScriptSerializer(new SimpleTypeResolver());
object obj = serializer.Deserialize<object>(json); // Can instantiate any type!
Why this is vulnerable:
- Allows payloads to specify .NET type names.
- Instantiates attacker-chosen types during deserialization.
- Constructors and property setters can execute code.
- Enables gadget chains via type resolution.
Newtonsoft.Json with TypeNameHandling
// VULNERABLE - TypeNameHandling.All is dangerous
using Newtonsoft.Json;
var settings = new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.All // DANGEROUS!
};
var obj = JsonConvert.DeserializeObject(json, settings); // RCE risk!
Why this is vulnerable:
- Injects
$typemetadata into JSON payloads. - Lets attackers control which types get instantiated.
- Dangerous constructors and callbacks can run.
- Gadget chains enable remote code execution.
Secure Patterns
System.Text.Json (Recommended for .NET Core 3.0+)
// SECURE - System.Text.Json has no type resolution by default
using System.Text.Json;
public class UserService
{
public User LoadUser(string json)
{
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
// Only deserializes to specified type (User)
return JsonSerializer.Deserialize<User>(json, options);
}
public string SaveUser(User user)
{
return JsonSerializer.Serialize(user);
}
}
// Example usage:
var user = new User { Name = "John", Email = "john@example.com" };
string json = JsonSerializer.Serialize(user);
User deserialized = JsonSerializer.Deserialize<User>(json);
Why this works:
- Requires an explicit target type (
Deserialize<User>). - Does not resolve or honor
$typemetadata by default. - Creates only the specified CLR type and sets properties.
- Treats input as data (primitives/objects), not instructions.
- Blocks gadget chains that rely on arbitrary type instantiation.
Newtonsoft.Json WITHOUT TypeNameHandling
// SECURE - No type name handling
using Newtonsoft.Json;
public class UserService
{
public User LoadUser(string json)
{
// Safe: No TypeNameHandling, deserializes only to User type
return JsonConvert.DeserializeObject<User>(json);
}
public string SaveUser(User user)
{
return JsonConvert.SerializeObject(user);
}
}
// If you MUST use TypeNameHandling, use minimal scope:
var settings = new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.None, // SAFE (default)
// OR for polymorphism, use limited scope:
// TypeNameHandling = TypeNameHandling.Objects
// with SerializationBinder to allowlist types
};
Why this works:
- Default
TypeNameHandling.Noneignores$typemetadata. - Deserializes only to the specified target type.
- Prevents attacker-controlled type selection.
- Maps JSON to properties without executing extra logic.
- Allowlisting with a binder keeps polymorphism bounded.
DataContractSerializer (Safe for Known Types)
// SECURE - DataContractSerializer only deserializes known types
using System.IO;
using System.Runtime.Serialization;
using System.Xml;
public class UserService
{
public User LoadUser(string xml)
{
var serializer = new DataContractSerializer(typeof(User));
using (var reader = XmlReader.Create(new StringReader(xml)))
{
// Can only deserialize to User type
return (User)serializer.ReadObject(reader);
}
}
public string SaveUser(User user)
{
var serializer = new DataContractSerializer(typeof(User));
using (var sw = new StringWriter())
using (var writer = XmlWriter.Create(sw))
{
serializer.WriteObject(writer, user);
writer.Flush();
return sw.ToString();
}
}
}
Why this works:
- Requires explicit root type at construction time.
- Rejects types not in the known types list.
- Uses
[DataContract]/[DataMember]opt-in fields. - Avoids dynamic type resolution from the payload.
- Limits deserialization to intended members and types.
Custom SerializationBinder for Type Allowlisting
// SECURE - Allowlist allowed types
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
public class SafeSerializationBinder : ISerializationBinder
{
private readonly HashSet<string> _allowedTypes = new HashSet<string>
{
"MyApp.Models.User",
"MyApp.Models.Address",
"System.Collections.Generic.List`1[[MyApp.Models.User]]"
};
public Type BindToType(string assemblyName, string typeName)
{
string fullName = $"{typeName}, {assemblyName}";
if (!_allowedTypes.Contains(typeName) && !_allowedTypes.Contains(fullName))
{
throw new JsonSerializationException($"Type {typeName} is not allowed");
}
return Type.GetType(fullName);
}
public void BindToName(Type serializedType, out string assemblyName, out string typeName)
{
assemblyName = serializedType.Assembly.FullName;
typeName = serializedType.FullName;
}
}
// Usage:
var settings = new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.Objects,
SerializationBinder = new SafeSerializationBinder()
};
var obj = JsonConvert.DeserializeObject<User>(json, settings);
Why this works:
- Intercepts all type resolution requests via
BindToType(). - Allows only explicitly approved types to instantiate.
- Rejects unknown
$typevalues early. - Keeps polymorphism bounded to a safe allowlist.
- Blocks gadget chains that rely on arbitrary types.
Framework-Specific Guidance
ASP.NET Core
// SECURE - ASP.NET Core uses System.Text.Json by default
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
[HttpPost]
public IActionResult CreateUser([FromBody] User user)
{
// ASP.NET Core automatically deserializes JSON to User
// Uses System.Text.Json (safe by default)
_userService.Save(user);
return Ok(user);
}
[HttpGet("{id}")]
public IActionResult GetUser(int id)
{
var user = _userService.GetById(id);
// Automatically serialized to JSON
return Ok(user);
}
}
// Startup.cs / Program.cs - Configure JSON options
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
options.JsonSerializerOptions.WriteIndented = true;
// No type resolution by default - SAFE!
});
ASP.NET MVC (Legacy)
// SECURE - Use JSON.NET without TypeNameHandling
using Newtonsoft.Json;
public class UsersController : Controller
{
[HttpPost]
public ActionResult Create(string json)
{
// Deserialize to specific type only
var user = JsonConvert.DeserializeObject<User>(json);
_userService.Save(user);
return Json(new { success = true });
}
}
// Global.asax.cs - Configure JSON serializer
GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings = new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.None, // SAFE
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
};
WCF Services
// SECURE - Use DataContractSerializer (default for WCF)
[ServiceContract]
public interface IUserService
{
[OperationContract]
User GetUser(int id);
[OperationContract]
void SaveUser(User user);
}
[DataContract]
public class User
{
[DataMember]
public int Id { get; set; }
[DataMember]
public string Name { get; set; }
}
// Implementation
public class UserService : IUserService
{
public User GetUser(int id)
{
// WCF uses DataContractSerializer - safe
return _repository.GetById(id);
}
public void SaveUser(User user)
{
_repository.Save(user);
}
}
Migrating from BinaryFormatter
BinaryFormatter is obsolete and dangerous in .NET 5+. Migrate to safe alternatives:
// OLD CODE - Remove BinaryFormatter
/*
var formatter = new BinaryFormatter();
using (var ms = new MemoryStream(data))
{
return formatter.Deserialize(ms);
}
*/
// NEW CODE - Option 1: Use System.Text.Json
using System.Text.Json;
public T Deserialize<T>(byte[] data)
{
var json = Encoding.UTF8.GetString(data);
return JsonSerializer.Deserialize<T>(json);
}
public byte[] Serialize<T>(T obj)
{
var json = JsonSerializer.Serialize(obj);
return Encoding.UTF8.GetBytes(json);
}
// NEW CODE - Option 2: Use MessagePack (binary, fast)
// Install: dotnet add package MessagePack
using MessagePack;
public T Deserialize<T>(byte[] data)
{
return MessagePackSerializer.Deserialize<T>(data);
}
public byte[] Serialize<T>(T obj)
{
return MessagePackSerializer.Serialize(obj);
}
.NET Library Safety Matrix
When reviewing code, use this matrix to identify unsafe deserialization libraries:
Safe by Default
System.Text.Json (.NET Core 3.0+)
- Recommended for all new .NET applications
- No type resolution by default
- Cannot instantiate arbitrary types
DataContractSerializer
- Safe with known types
- Only deserializes specified types
var serializer = new DataContractSerializer(typeof(User));
var user = (User)serializer.ReadObject(stream); // Safe
XmlSerializer
- Safe with known types
- Cannot deserialize arbitrary types
var serializer = new XmlSerializer(typeof(User));
var user = (User)serializer.Deserialize(stream); // Safe
JSON.NET with TypeNameHandling.None
- Safe when TypeNameHandling is None (default)
var settings = new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.None // REQUIRED (default)
};
var user = JsonConvert.DeserializeObject<User>(json, settings); // Safe
CANNOT Be Used Safely
BinaryFormatter
- Microsoft states: "is dangerous and cannot be secured"
- Obsolete in .NET 5+
- Replace with JSON or MessagePack immediately
var formatter = new BinaryFormatter(); // NEVER USE!
var obj = formatter.Deserialize(stream); // RCE vulnerability
NetDataContractSerializer
- Allows arbitrary type deserialization
- No safe configuration
- Replace with DataContractSerializer or JSON
var serializer = new NetDataContractSerializer(); // UNSAFE!
var obj = serializer.Deserialize(stream); // Can instantiate any type
LosFormatter
- Legacy ASP.NET ViewState formatter
- Uses BinaryFormatter internally
- Replace with modern serialization
ObjectStateFormatter
- Legacy ViewState formatter
- Known vulnerabilities
- Use encrypted ViewState with MAC validation
JSON.NET with TypeNameHandling
- TypeNameHandling.All, .Objects, .Auto are dangerous
- Allows type resolution from JSON
- Use TypeNameHandling.None or implement SerializationBinder
// UNSAFE configurations:
var settings = new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.All // RCE risk!
};
// SAFE: Use None (default)
var settings = new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.None // Safe
};
Migration Recommendations
If you find these patterns in security scan results:
- BinaryFormatter.Deserialize() → Switch to System.Text.Json
- NetDataContractSerializer → Switch to DataContractSerializer with known types
- LosFormatter → Use modern ASP.NET Core with JSON
- ObjectStateFormatter → Enable ViewState MAC and encryption
- JSON.NET TypeNameHandling.All → Set to TypeNameHandling.None
Example migration:
// BEFORE (Unsafe)
var formatter = new BinaryFormatter();
var user = (User)formatter.Deserialize(stream);
// AFTER (Safe - System.Text.Json)
using var reader = new StreamReader(stream, Encoding.UTF8, leaveOpen: true);
var json = await reader.ReadToEndAsync();
var user = JsonSerializer.Deserialize<User>(json);
// OR (Safe - MessagePack for binary)
using var ms = new MemoryStream();
await stream.CopyToAsync(ms);
var data = ms.ToArray();
var user = MessagePackSerializer.Deserialize<User>(data);
Microsoft's Official Guidance:
- BinaryFormatter Security Guide
- Recommendation: "Do not use BinaryFormatter under any circumstances"
Input Validation
// Validate data after deserialization
using System.ComponentModel.DataAnnotations;
public class User
{
[Required]
[StringLength(100, MinimumLength = 1)]
public string Name { get; set; }
[Required]
[EmailAddress]
public string Email { get; set; }
[Range(0, 150)]
public int Age { get; set; }
}
// Controller with validation
[HttpPost]
public IActionResult CreateUser([FromBody] User user)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
// Additional custom validation
if (user.Email.EndsWith("@malicious.com"))
{
return BadRequest("Email domain not allowed");
}
_userService.Save(user);
return Ok(user);
}
// Manual validation
var context = new ValidationContext(user);
var results = new List<ValidationResult>();
bool isValid = Validator.TryValidateObject(user, context, results, true);
if (!isValid)
{
foreach (var error in results)
{
Console.WriteLine(error.ErrorMessage);
}
}
Signature Verification
// SECURE - Verify HMAC before deserializing
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
public class SignedDeserializer
{
private readonly byte[] _key;
public SignedDeserializer(byte[] key)
{
_key = key;
}
public T Deserialize<T>(byte[] signedData)
{
// Format: [32-byte HMAC][JSON data]
if (signedData.Length < 32)
throw new ArgumentException("Invalid signed data");
var signature = signedData[..32];
var data = signedData[32..];
// Verify HMAC
using (var hmac = new HMACSHA256(_key))
{
var expectedSignature = hmac.ComputeHash(data);
if (!CryptographicOperations.FixedTimeEquals(signature, expectedSignature))
{
throw new SecurityException("Invalid signature");
}
}
// Only deserialize if signature is valid
var json = Encoding.UTF8.GetString(data);
return JsonSerializer.Deserialize<T>(json);
}
public byte[] Serialize<T>(T obj)
{
var json = JsonSerializer.Serialize(obj);
var data = Encoding.UTF8.GetBytes(json);
using (var hmac = new HMACSHA256(_key))
{
var signature = hmac.ComputeHash(data);
// Combine signature and data
var result = new byte[signature.Length + data.Length];
Buffer.BlockCopy(signature, 0, result, 0, signature.Length);
Buffer.BlockCopy(data, 0, result, signature.Length, data.Length);
return result;
}
}
}
Verification
After implementing the recommended secure patterns, verify the fix through multiple approaches:
- Manual testing: Submit malicious payloads relevant to this vulnerability and confirm they're handled safely without executing unintended operations
- Code review: Confirm all instances use safe deserialization APIs and reject unsafe formats
- Static analysis: Use security scanners to verify no unsafe deserialization patterns remain
- Regression testing: Ensure legitimate user inputs and application workflows continue to function correctly
- Edge case validation: Test with special characters, boundary conditions, and unusual inputs to verify proper handling
- Framework verification: If using a framework or library, confirm the recommended APIs are used correctly according to documentation
- Authentication/session testing: Verify security controls remain effective and cannot be bypassed (if applicable to the vulnerability type)
- Rescan: Run the security scanner again to confirm the finding is resolved and no new issues were introduced
Configuration Security
<!-- web.config / app.config -->
<configuration>
<system.web>
<!-- Enable ViewState MAC and encryption -->
<pages enableViewStateMac="true"
viewStateEncryptionMode="Always"
enableEventValidation="true" />
<!-- Use strong machine key -->
<machineKey validationKey="[GENERATE_STRONG_KEY]"
decryptionKey="[GENERATE_STRONG_KEY]"
validation="HMACSHA256"
decryption="AES" />
</system.web>
</configuration>