Ужесточаем Десериализацию JSON в System.Text.Json. Начало
Рассмотрим следующий JSON:
{"Amount": 100, "Amount": -999}Два свойства с одинаковым именем. В разделе 4 RFC 8259 говорится, что имена объектов "ДОЛЖНЫ быть уникальными", и предупреждается, что поведение парсера непредсказуемо, если они таковыми не являются. System.Text.Json выбирает разрешительный путь: побеждает последняя запись, без предупреждений, без ошибок.
Значение, присвоенное злоумышленником, незаметно побеждает. Это не просто особенность дублирования свойств. Десериализация по умолчанию также игнорирует дополнительные поля, которые может внедрить злоумышленник, позволяет null проникать в свойства, не допускающие null, и пропускает отсутствующие обязательные данные. Каждое из этих «удобств» представляет потенциальную уязвимость.
JsonSerializerOptions.Strict
.NET 10 представляет JsonSerializerOptions.Strict, новую настройку наряду с Default и Web. Default отдаёт приоритет обратной совместимости, Web оптимизирует работу для типичных HTTP API, а Strict следует лучшим практикам безопасности:
using System.Text.Json;
string json = """{"Amount": 100, "Amount": -999}""";
try
{
JsonSerializer.Deserialize<Payment>(json,
JsonSerializerOptions.Strict);
}
catch (JsonException ex)
{
Console.WriteLine(ex.Message);
}
// Вывод: Duplicate property 'Amount' encountered during deserialization of type 'Payment'.
public record Payment(int Amount);
В каждой настройке 5 свойств. Рассмотрим их значения, и на что они влияют.
1. AllowDuplicateProperties (Принятие дублирующих свойств)
- Default: true
- Web: true
- Strict: false
Протоколы, использующие многоуровневый анализ JSON (OAuth 2.0, OpenID Connect, подписи веб-хуков), могут быть взломаны, если разные парсеры обрабатывают дублирующиеся входные данные по-разному. Один парсер видит первое значение, другой — последнее.
Эта защита распространяется не только на десериализацию обычных C# объектов. Также можно обнаруживать дубликаты в JsonDocument, JsonNode и Dictionary<string, T>.
2. UnmappedMemberHandling (Обработка несоответствующих членов)
- Default: Skip
- Web: Skip
- Strict: Disallow
Десериализация по умолчанию молча отбрасывает свойства JSON, которые не соответствуют вашему типу .NET. Это удобно во время разработки. Но это опасно на границе доверия.
Это позволяет выявлять класс ошибок, когда клиенты отправляют поля, которые API не должен принимать. Если вы их не отклоняете, вы не знаете об их наличии. UnmappedMemberHandling добавлено в .NET 8.
3. PropertyNameCaseInsensitive (Учёт регистра свойств)
- Default: false
- Web: true
- Strict: false
Web устанавливает свойство в true, чтобы "username" соответствовало свойству C# с именем Username. Это полезно для типичного взаимодействия JavaScript с .NET. Но в сочетании с UnmappedMemberHandling.Disallow в режиме Strict учёт регистра становится точным контрактом: имена свойств JSON должны точно совпадать с именами свойств C#, иначе десериализатор их отклонит.
Если ваши клиенты отправляют данные в формате camelCase, и вам нужна строгая проверка, добавьте
[JsonPropertyName("username")] к свойству. Так, контракт будет явно указан в определении типа, а не неявно в параметрах.4. RespectNullableAnnotations (Учёт nullable-аннотаций)
- Default: false
- Web: false
- Strict: true
Обнуляемые ссылочные типы C# помогают выявлять проблемы с null на этапе компиляции. Но System.Text.Json по умолчанию игнорирует их во время десериализации. Значение null в JSON легко помещается в строковое свойство без каких-либо проблем. RespectNullableAnnotations добавлено в .NET 9. В строгом режиме сериализатор обеспечивает соблюдение ваших nullable-аннотаций на границе десериализации. Если вы объявили string Name (а не string? Name), сериализатор будет придерживаться этого правила.
5. RespectRequiredConstructorParameters (Учёт обязательных параметров конструктора)
- Default: false
- Web: false
- Strict: true
В типах записей и классах с параметризованными конструкторами обязательные параметры могут быть незаметно заполнены значениями по умолчанию, если в JSON отсутствуют данные.
Это позволяет выявлять частичные данные, которые в противном случае привели бы к тому, что объекты оказались бы в недопустимом состоянии. В сочетании с обеспечением соблюдения nullable-аннотаций это означает, что ваши десериализованные объекты соответствуют объявленному контракту вашего типа.
Окончание следует…
Источник: https://duendesoftware.com/blog/20260430-harden-your-dotnet-json-deserialization