As shown in How to configure Json.NET to create a vulnerable web API, you can enable TypeNameHandling
during deserialization and model binding throughout your entire object graph by doing
services.AddMvc().AddJsonOptions(options =>
{
options.SerializerSettings.TypeNameHandling = TypeNameHandling.All;
});
in Startup.cs
.
However, doing this can introduce security risks as described in that very article as well as TypeNameHandling caution in Newtonsoft Json. As such I would not recommend this solution unless you create a custom ISerializationBinder
to filter out unwanted or unexpected types.
As an alternative to this risky solution, if you only need to make your list of root models be polymorphic, the following approach can be used:
Derive all of your polymorphic models from some common base class or interface defined in your application (i.e. not some system type such as CollectionBase
or INotifyPropertyChanged
).
Define a container DTO with single property of type List<T>
where T
is your common base type.
Mark that property with [JsonProperty(ItemTypeNameHandling = TypeNameHandling.Auto)]
.
Do not set options.SerializerSettings.TypeNameHandling = TypeNameHandling.All;
in startup.
To see how this works in practice, say you have the following model type hierarchy:
public abstract class ModelBase
{
}
public class ApplicationLoggingModel : ModelBase
{
public string Test { get; set; }
}
public class AnotherModel : ModelBase
{
public string AnotherTest { get; set; }
}
Then define your root DTO as follows:
public class ModelBaseCollectionDTO
{
[JsonProperty(ItemTypeNameHandling = TypeNameHandling.Auto)]
public List<ModelBase> Models { get; set; }
}
Then if you construct an instance of it as follows and serialize to JSON:
var dto = new ModelBaseCollectionDTO
{
Models = new List<ModelBase>()
{
new ApplicationLoggingModel { Test = "test value" },
new AnotherModel { AnotherTest = "another test value" },
},
};
var json = JsonConvert.SerializeObject(dto, Formatting.Indented);
The following JSON is generated:
{
"Models": [
{
"$type": "Namespace.Models.ApplicationLoggingModel, TestApp",
"Test": "test value"
},
{
"$type": "Namespace.Models.AnotherModel, TestApp",
"AnotherTest": "another test value"
}
]
}
This can then be deserialized back into a ModelBaseCollectionDTO
without loss of type information and without needing to set ItemTypeNameHandling
globally:
var dto2 = JsonConvert.DeserializeObject<ModelBaseCollectionDTO>(json);
Sample working fiddle.
However, if I attempt the attack shown in How to configure Json.NET to create a vulnerable web API as follows:
try
{
File.WriteAllText("rce-test.txt", "");
var badJson = JToken.FromObject(
new
{
Models = new object[]
{
new FileInfo("rce-test.txt") { IsReadOnly = false },
}
},
JsonSerializer.CreateDefault(new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto, Formatting = Formatting.Indented }));
((JObject)badJson["Models"][0])["IsReadOnly"] = true;
Console.WriteLine("Attempting to deserialize attack JSON: ");
Console.WriteLine(badJson);
var dto2 = JsonConvert.DeserializeObject<ModelBaseCollectionDTO>(badJson.ToString());
Assert.IsTrue(false, "should not come here");
}
catch (JsonException ex)
{
Assert.IsTrue(!new FileInfo("rce-test.txt").IsReadOnly);
Console.WriteLine("Caught expected {0}: {1}", ex.GetType(), ex.Message);
}
Then the file rce-test.txt
is not marked as read-only, and instead the following exception is thrown:
Newtonsoft.Json.JsonSerializationException: Type specified in JSON 'System.IO.FileInfo, mscorlib' is not compatible with 'Namespace.Models.ModelBase, Tile'. Path 'Models[0].$type', line 4, position 112.
Indicating that the attack gadget FileInfo
is never even constructed.
Notes:
By using TypeNameHandling.Auto
you avoid bloating your JSON with type information for non-polymorphic properties.
The correct format for the JSON in your post body can be determined by test-serializing the expected resulting ModelBaseCollectionDTO
during development.
For an explanation of why the attack fails see External json vulnerable because of Json.Net TypeNameHandling auto?. As long as no attack gadget is compatible with (assignable to) your base model type, you should be secure.
Because you do not set TypeNameHandling
in startup you are not making your other APIs vulnerable to attack or exposing yourself to attacks via deeply nested polymorphic properties such as those mentioned here.
Nevertheless, for added safety, you may still want to create a custom ISerializationBinder
.