-
Notifications
You must be signed in to change notification settings - Fork 1
Mapping Untrusted Input
A mapper copies every matching property it can. Pointed at a request body it will set anything whose name lines up, including a property the caller had no business setting. This is true of every convention-based mapper, Mapsicle included, and it is worth stating plainly rather than leaving for you to discover:
// An attacker controls the keys.
var body = new Dictionary<string, object?>
{
["Email"] = "user@example.com",
["IsAdmin"] = true, // not a field the caller should decide
};
var account = body.MapTo<Account>(); // account.IsAdmin is now trueMap untrusted input into a DTO that holds only the fields a caller may set, then map that into your entity:
public class AccountUpdateDto // no IsAdmin, no Balance
{
public string Email { get; set; } = "";
}
var dto = body.MapTo<AccountUpdateDto>();
dto.Map(existingAccount); // reaches nothing the DTO does not declareWhere a shared type is unavoidable, [IgnoreMap] is an enforceable control and is honoured on
every entry point, including the dictionary path.
What Mapsicle does guarantee about untrusted values:
- Values are copied, never interpreted. Nothing in a string is parsed, executed or sanitised. A value containing SQL, script or format-string syntax arrives byte for byte.
-
A value of the wrong type is dropped, not coerced and not thrown. A caller cannot use a type
mismatch to crash a request handler or to smuggle a value through a loose conversion.
Before 2.0.0 this was true of the object entry point and false of the dictionary one, which ran
Convert.ChangeTypeon anythingIConvertible. Both now behave the same. If you need the old parsing (a form post arrives as strings, and that is a legitimate reason to want it), setMapper.CoerceDictionaryValues = trueand it parses with the invariant culture. Lossless widening, enum and nullable conversions are unaffected and apply either way. - Unknown keys are ignored rather than throwing.
- Conversions do not depend on where the process runs. Numbers and dates format with the invariant culture, so the same input produces the same output in every region.
One thing this list deliberately does not claim is a deep copy. A destination member that can hold
the source instance as it is receives that instance, on every entry point, so mutating the source
afterwards reaches into the destination. That is the same choice AutoMapper makes and it is what
keeps mapping allocation-free beyond the destination object, but if you map onto a long-lived
domain entity it is worth knowing. DataIntegrityTests pins it.
All of this is covered by UntrustedInputTests and DataIntegrityTests in tests/Mapsicle.Tests.
A failure there means one of these statements stopped being true.
Mapsicle
Packages
- Core
- Fluent
- EntityFramework
- Validation
- NamingConventions
- Serilog
- Dapper
- AspNetCore
- Json
- Caching
- Audit
- DataAnnotations
Reference