Полезные данные чтения BinaryFormatter (NRBF)
BinaryFormatter использовал удаленное взаимодействие .NET: двоичный формат для сериализации. Этот формат известен своим сокращением MS-NRBF или просто NRBF. Распространенная проблема, связанная с миграцией с BinaryFormatter полезных данных, сохраняемых в хранилище, как чтение этих полезных данных ранее необходимых BinaryFormatter. Некоторые системы должны сохранять возможность чтения этих полезных данных для постепенной миграции на новые сериализаторы, избегая ссылки на BinaryFormatter себя.
В рамках .NET 9 новый класс NrbfDecoder был представлен для декодирования полезных данных NRBF без десериализации полезных данных. Этот API можно безопасно использовать для декодирования доверенных или ненадежных полезных данных без каких-либо рисков десериализации BinaryFormatter . Однако NrbfDecoder просто декодирует данные в структуры, которые приложение может дополнительно обрабатывать. При использовании NrbfDecoder необходимо обеспечить безопасную загрузку данных в соответствующие экземпляры.
Осторожность
NrbfDecoder является реализацией средства чтения NRBF, но его поведение не полностью следует BinaryFormatterреализации. Таким образом, вы не должны использовать выходные данные NrbfDecoder, чтобы определить, будет ли вызов BinaryFormatter безопасным.
Можно считать NrbfDecoder эквивалентом использования средства чтения JSON/XML без десериализатора.
NrbfDecoder
NrbfDecoder является частью нового пакета NuGet System.Formats.Nrbf . Он предназначен не только для .NET 9, но и более старых моникеров, таких как .NET Standard 2.0 и платформа .NET Framework. Эта мультинацеливание позволяет всем, кто использует поддерживаемую версию .NET, перейти от BinaryFormatterнее. NrbfDecoder может считывать полезные данные, сериализованные с BinaryFormatter помощью FormatterTypeStyle.TypesAlways (по умолчанию).
NrbfDecoder предназначен для обработки всех входных данных как ненадежных. Таким образом, он имеет следующие принципы:
- Нет типа загрузки любого типа (чтобы избежать рисков, таких как удаленное выполнение кода).
- Нет рекурсии любого вида (чтобы избежать отмены исходящего рекурсии, переполнения стека и отказа в обслуживании).
- Нет предварительного выделения буфера на основе размера, предоставленного в полезных данных, если полезные данные слишком малы, чтобы содержать обещанные данные (чтобы избежать нехватки памяти и отказа в обслуживании).
- Декодировать каждую часть входных данных только один раз (для выполнения той же работы, что и потенциальный злоумышленник, создавший полезные данные).
- Используйте случайное хэширование с устойчивостью к столкновению для хранения записей, на которые ссылается другие записи (чтобы избежать нехватки памяти для словаря, поддерживаемого массивом, размер которого зависит от количества конфликтов хэш-кода).
- Только примитивные типы можно создать экземпляр неявным способом. Массивы можно создать по запросу. Другие типы никогда не создаются экземплярами.
Осторожность
При использовании NrbfDecoderважно не повторно вводить эти возможности в коде общего назначения, так как это приведет к отмене этих гарантий.
Десериализация закрытого набора типов
NrbfDecoder полезен только в том случае, если список сериализованных типов является известным закрытым набором. Чтобы поставить его другим способом, необходимо заранее узнать, что вы хотите прочитать, так как вам также нужно создать экземпляры этих типов и заполнить их данными, которые были считываются из полезных данных. Рассмотрим два противоположных примера:
- Все
[Serializable]
типы из Quartz.NET , которые могут быть сохраненыsealed
самой библиотекой. Поэтому пользовательские типы, которые пользователи могли создавать, и полезные данные могут содержать только известные типы. Типы также предоставляют общедоступные конструкторы, поэтому их можно воссоздать на основе сведений, считываемых из полезных данных. - Тип SettingsPropertyValue предоставляет свойство PropertyValue типа
object
, которое может использоваться BinaryFormatter для сериализации и десериализации любого объекта, хранящегося в файле конфигурации. Его можно использовать для хранения целого числа, пользовательского типа, словаря или буквально ничего. Из-за этого невозможно перенести эту библиотеку без внесения критических изменений в API.
Определение полезных данных NRBF
NrbfDecoder предоставляет два StartsWithPayloadHeader метода, которые позволяют проверить, начинается ли заданный поток или буфер с заголовком NRBF. Рекомендуется использовать эти методы при переносе полезных данных, сохраненных в BinaryFormatter другом сериализаторе:
- Проверьте, является ли полезные данные, считываемыми из хранилища, полезными данными NrbfDecoder.StartsWithPayloadHeader.
- Если да, прочитайте его NrbfDecoder.Decode, сериализуйте его обратно с помощью нового сериализатора и перезаписите данные в хранилище.
- В противном случае используйте новый сериализатор для десериализации данных.
internal static T LoadFromFile<T>(string path)
{
bool update = false;
T value;
using (FileStream stream = File.OpenRead(path))
{
if (NrbfDecoder.StartsWithPayloadHeader(stream))
{
value = LoadLegacyValue<T>(stream);
update = true;
}
else
{
value = LoadNewValue<T>(stream);
}
}
if (update)
{
File.WriteAllBytes(path, NewSerializer(value));
}
return value;
}
Безопасное чтение полезных данных NRBF
Полезные данные NRBF состоят из записей сериализации, представляющих сериализованные объекты и их метаданные. Чтобы прочитать всю полезные данные и получить корневой объект, необходимо вызвать Decode метод.
Метод Decode возвращает SerializationRecord экземпляр. SerializationRecord — это абстрактный класс, представляющий запись сериализации и предоставляющий три самостоятельно описывающих свойства: Id, RecordTypeи TypeName.
Заметка
Злоумышленник может создать вредоносную нагрузку с циклами (например, класс или массив объектов с ссылкой на себя). Id возвращает экземпляр SerializationRecordId, который реализует IEquatable<T> и среди прочего, его можно использовать для обнаружения циклов в декодированных записях.
SerializationRecord предоставляет один метод, TypeNameMatches, который сравнивает имя типа, считываемое из полезных данных (и предоставляемое через свойство TypeName) с указанным типом. Этот метод игнорирует имена сборок, поэтому пользователям не нужно беспокоиться о переадресации типов и использовании версий сборок. Кроме того, он не учитывает имена членов или их типы (так как для получения этой информации требуется загрузка типов).
using System.Formats.Nrbf;
static Animal Pseudocode(Stream payload)
{
SerializationRecord record = NrbfDecoder.Read(payload);
if (record.TypeNameMatches(typeof(Cat)) && record is ClassRecord catRecord)
{
return new Cat()
{
Name = catRecord.GetString("Name"),
WorshippersCount = catRecord.GetInt32("WorshippersCount")
};
}
else if (record.TypeNameMatches(typeof(Dog)) && record is ClassRecord dogRecord)
{
return new Dog()
{
Name = dogRecord.GetString("Name"),
FriendsCount = dogRecord.GetInt32("FriendsCount")
};
}
else
{
throw new Exception($"Unexpected record: `{record.TypeName.AssemblyQualifiedName}`.");
}
}
Существует более десятка различных типов записей сериализации . Эта библиотека предоставляет набор абстракций, поэтому вам нужно только узнать несколько из них:
-
PrimitiveTypeRecord<T>: описывает все примитивные типы, которые изначально поддерживаются NRBF (
string
,bool
byte
sbyte
char
short
ushort
int
uint
long
ulong
float
double
decimal
TimeSpan
и ).DateTime
- Предоставляет значение через
Value
свойство. -
PrimitiveTypeRecord<T> является производным от не универсального PrimitiveTypeRecord, который также предоставляет Value свойство. Но в базовом классе значение возвращается как
object
(которое вводит бокс для типов значений).
- Предоставляет значение через
-
ClassRecord: описывает все
class
иstruct
помимо упомянутых выше примитивных типов. - ArrayRecord: описывает все записи массива, включая многомерные и многомерные массивы.
-
SZArrayRecord<T>: описывает одномерные, нумерированные записи массива, где
T
может быть либо примитивный тип, либо .SerializationRecord
SerializationRecord rootObject = NrbfDecoder.Decode(payload); // payload is a Stream
if (rootObject is PrimitiveTypeRecord primitiveRecord)
{
Console.WriteLine($"It was a primitive value: '{primitiveRecord.Value}'");
}
else if (rootObject is ClassRecord classRecord)
{
Console.WriteLine($"It was a class record of '{classRecord.TypeName.AssemblyQualifiedName}' type name.");
}
else if (rootObject is SZArrayRecord<byte> arrayOfBytes)
{
Console.WriteLine($"It was an array of `{arrayOfBytes.Length}`-many bytes.");
}
Кроме того Decode, NrbfDecoder предоставляет DecodeClassRecord метод, который возвращает ClassRecord (или вызывает).
ClassRecord
Наиболее важным типом, производным от SerializationRecord этого, является ClassRecordто, что представляет все class
экземпляры рядом с struct
массивами и изначально поддерживаемыми примитивными типами. Он позволяет считывать все имена и значения элементов. Чтобы понять, что такое член, ознакомьтесь со ссылкой на BinaryFormatterфункции.
Api, который он предоставляет:
- MemberNames свойство, которое получает имена сериализованных элементов.
- HasMember Метод, который проверяет, присутствует ли член заданного имени в полезных данных. Она была разработана для обработки сценариев управления версиями, в которых данный член мог быть переименован.
- Набор выделенных методов для получения примитивных значений предоставленного имени члена: GetString, GetBooleanGetByteGetSByteGetCharGetInt16GetUInt16GetInt32GetUInt32GetInt64GetUInt64GetSingleGetDoubleGetDecimalGetTimeSpanи .GetDateTime
- GetClassRecord извлекает экземпляр [ClassRecord]. В случае цикла это тот же экземпляр текущего [ClassRecord] с тем же Id.
- GetArrayRecord извлекает экземпляр [ArrayRecord].
- GetSerializationRecord для получения любой записи сериализации и GetRawValue для получения любой записи сериализации или необработанного примитивного значения.
В действии показан ClassRecord следующий фрагмент кода:
[Serializable]
public class Sample
{
public int Integer;
public string? Text;
public byte[]? ArrayOfBytes;
public Sample? ClassInstance;
}
ClassRecord rootRecord = NrbfDecoder.DecodeClassRecord(payload);
Sample output = new()
{
// using the dedicated methods to read primitive values
Integer = rootRecord.GetInt32(nameof(Sample.Integer)),
Text = rootRecord.GetString(nameof(Sample.Text)),
// using dedicated method to read an array of bytes
ArrayOfBytes = ((SZArrayRecord<byte>)rootRecord.GetArrayRecord(nameof(Sample.ArrayOfBytes))).GetArray(),
};
// using GetClassRecord to read a class record
ClassRecord? referenced = rootRecord.GetClassRecord(nameof(Sample.ClassInstance));
if (referenced is not null)
{
if (referenced.Id.Equals(rootRecord.Id))
{
throw new Exception("Unexpected cycle detected!");
}
output.ClassInstance = new()
{
Text = referenced.GetString(nameof(Sample.Text))
};
}
ArrayRecord
ArrayRecord определяет основное поведение записей массива NRBF и предоставляет базу для производных классов. Он предоставляет два свойства:
- Rank, который определяет ранг массива.
- Lengths, который получает буфер целых чисел, представляющих количество элементов в каждом измерении. Перед вызовом GetArrayрекомендуется проверить общую длину предоставленной записи массива.
Он также предоставляет один метод: GetArray При первом использовании он выделяет массив и заполняет их данными, предоставленными в сериализованных записях (в случае собственных поддерживаемых примитивных типов илиstring
) или сериализованных записей (в случае массивов сложных типовint
).
GetArray требует обязательного аргумента, указывающего тип ожидаемого массива. Например, если запись должна быть массивом целых чисел 2D, expectedArrayType
необходимо указать как typeof(int[,])
, а возвращаемый массив также int[,]
:
ArrayRecord arrayRecord = (ArrayRecord)NrbfDecoder.Decode(stream);
if (arrayRecord.Rank != 2 || arrayRecord.Lengths[0] * arrayRecord.Lengths[1] > 10_000)
{
throw new Exception("The array had unexpected rank or length!");
}
int[,] array2d = (int[,])arrayRecord.GetArray(typeof(int[,]));
Если имеется несоответствие типа (например, злоумышленник предоставил полезные данные с массивом из двух миллиардов строк), метод вызывает исключение InvalidOperationException.
Осторожность
К сожалению, формат NRBF упрощает сжатие большого количества элементов массива NULL. Поэтому рекомендуется всегда проверять общую длину массива перед вызовом GetArray. Кроме того, GetArray принимает необязательный allowNulls
логический аргумент, который, если задано значение false
, будет вызываться для значений NULL.
NrbfDecoder не загружает или создает экземпляры пользовательских типов, поэтому в случае массивов сложных типов он возвращает массив SerializationRecord.
[Serializable]
public class ComplexType3D
{
public int I, J, K;
}
ArrayRecord arrayRecord = (ArrayRecord)NrbfDecoder.Decode(payload);
if (arrayRecord.Rank != 1 || arrayRecord.Lengths[0] > 10_000)
{
throw new Exception("The array had unexpected rank or length!");
}
SerializationRecord[] records = (SerializationRecord[])arrayRecord.GetArray(expectedArrayType: typeof(ComplexType3D[]), allowNulls: false);
ComplexType3D[] output = records.OfType<ClassRecord>().Select(classRecord => new ComplexType3D()
{
I = classRecord.GetInt32(nameof(ComplexType3D.I)),
J = classRecord.GetInt32(nameof(ComplexType3D.J)),
K = classRecord.GetInt32(nameof(ComplexType3D.K)),
}).ToArray();
платформа .NET Framework поддерживает ненулевых индексированных массивов в полезных данных NRBF, но эта поддержка никогда не была перенесена в .NET (Core). Поэтому NrbfDecoder не поддерживает декодирование ненулевых индексированных массивов.
SZArrayRecord
SZArrayRecord<T>
определяет основное поведение для одномерных записей массива NRBF и предоставляет базу для производных классов. Это T
может быть один из собственных поддерживаемых примитивных типов или SerializationRecord.
Он предоставляет свойство и перегрузкуLength, возвращающуюGetArrayT[]
.
[Serializable]
public class PrimitiveArrayFields
{
public byte[]? Bytes;
public uint[]? UnsignedIntegers;
}
ClassRecord rootRecord = NrbfDecoder.DecodeClassRecord(payload);
SZArrayRecord<byte> bytes = (SZArrayRecord<byte>)rootRecord.GetArrayRecord(nameof(PrimitiveArrayFields.Bytes));
SZArrayRecord<uint> uints = (SZArrayRecord<uint>)rootRecord.GetArrayRecord(nameof(PrimitiveArrayFields.UnsignedIntegers));
if (bytes.Length > 100_000 || uints.Length > 100_000)
{
throw new Exception("The array exceeded our limit");
}
PrimitiveArrayFields output = new()
{
Bytes = bytes.GetArray(),
UnsignedIntegers = uints.GetArray()
};