Ładunki odczytu BinaryFormatter (NRBF)
BinaryFormatter użyto komunikacji wirtualnej platformy .NET: format binarny dla elementu serialization. Ten format jest znany ze skrótu MS-NRBF lub tylko NRBF. Typowym wyzwaniem podczas migracji z BinaryFormatter programu jest obsługa ładunków utrwalone w magazynie, ponieważ odczyt tych ładunków był wcześniej wymagany BinaryFormatter. Niektóre systemy muszą zachować możliwość odczytywania tych ładunków w celu stopniowego migracji do nowych serializatorów, unikając odwołania do BinaryFormatter samego siebie.
W ramach platformy .NET 9 wprowadzono nową klasę NrbfDecoder w celu dekodowania ładunków NRBF bez wykonywania deserializacji ładunku. Ten interfejs API można bezpiecznie użyć do dekodowania zaufanych lub niezaufanych ładunków bez ryzyka, które BinaryFormatter niesie deserializacja. Jednak NrbfDecoder jedynie dekoduje dane do struktur, które aplikacja może jeszcze bardziej przetworzyć. Podczas korzystania z narzędzia NrbfDecoder należy zachować ostrożność, aby bezpiecznie załadować dane do odpowiednich wystąpień.
Można traktować NrbfDecoder jako odpowiednik użycia czytnika JSON/XML bez deserializacji.
NrbfDecoder
NrbfDecoder jest częścią nowego pakietu NuGet System.Formats.Nrbf . Jest ona przeznaczona nie tylko dla platformy .NET 9, ale także starszych elementów monikers, takich jak .NET Standard 2.0 i .NET Framework. Ta wielowersyjność umożliwia wszystkim, którzy używają obsługiwanej wersji platformy .NET do migracji z dala od BinaryFormatterprogramu . Program NrbfDecoder może odczytywać ładunki, które zostały serializowane przy BinaryFormatter użyciu ( FormatterTypeStyle.TypesAlways wartość domyślna).
NrbfDecoder jest przeznaczony do traktowania wszystkich danych wejściowych jako niezaufanych. W związku z tym mają następujące zasady:
- Brak jakiegokolwiek rodzaju ładowania typu (aby uniknąć ryzyka, takiego jak zdalne wykonywanie kodu).
- Brak rekursji jakiegokolwiek rodzaju (aby uniknąć rekursji niezwiązanej, przepełnienia stosu i odmowy usługi).
- Brak wstępnego alokacji buforu na podstawie rozmiaru podanego w ładunku, jeśli ładunek jest zbyt mały, aby zawierać obiecane dane (aby uniknąć braku pamięci i odmowy usługi).
- Zdekoduj każdą część danych wejściowych tylko raz (aby wykonać tę samą ilość pracy co potencjalny atakujący, który utworzył ładunek).
- Użyj losowego skrótu odpornego na kolizję, aby przechowywać rekordy, do których odwołują się inne rekordy (aby uniknąć braku pamięci dla słownika wspieranego przez tablicę, której rozmiar zależy od liczby kolizji kodu skrótu).
- Wystąpienie tylko typów pierwotnych można utworzyć w niejawny sposób. Tablice można utworzyć na żądanie. Inne typy nigdy nie są tworzone.
W przypadku korzystania z narzędzia NrbfDecoder ważne jest, aby nie wprowadzać tych funkcji w kodzie ogólnego przeznaczenia, ponieważ spowoduje to negację tych zabezpieczeń.
Deserializowanie zamkniętego zestawu typów
NrbfDecoder jest przydatny tylko wtedy, gdy lista serializowanych typów jest znanym, zamkniętym zestawem. Aby umieścić go w inny sposób, musisz wiedzieć z góry, co chcesz odczytać, ponieważ musisz również utworzyć wystąpienia tych typów i wypełnić je danymi odczytanymi z ładunku. Rozważmy dwa przeciwległe przykłady:
- Wszystkie
[Serializable]
typy z Quartz.NET , które mogą być utrwalane przez samą bibliotekę, tosealed
. Nie ma więc typów niestandardowych, które użytkownicy mogą tworzyć, a ładunek może zawierać tylko znane typy. Typy udostępniają również konstruktory publiczne, więc można odtworzyć te typy na podstawie informacji odczytanych z ładunku. - Typ SettingsPropertyValue uwidacznia właściwość PropertyValue typu
object
, która może być używana BinaryFormatter wewnętrznie do serializacji i deserializacji dowolnego obiektu przechowywanego w pliku konfiguracji. Może służyć do przechowywania liczby całkowitej, typu niestandardowego, słownika lub dosłownie dowolnych elementów. Z tego powodu nie można migrować tej biblioteki bez wprowadzania zmian powodujących niezgodność w interfejsie API.
Identyfikowanie ładunków NRBF
NrbfDecoder udostępnia dwie StartsWithPayloadHeader metody umożliwiające sprawdzenie, czy dany strumień lub bufor zaczyna się od nagłówka NRBF. Zaleca się użycie tych metod podczas migrowania ładunków utrwałych BinaryFormatter do innego serializatora:
- Sprawdź, czy ładunek odczytany z magazynu jest ładunkiem NRBF za pomocą polecenia NrbfDecoder.StartsWithPayloadHeader.
- Jeśli tak, odczytaj go za pomocą NrbfDecoder.Decodepolecenia , serializuj go z powrotem przy użyciu nowego serializatora i zastąp dane w magazynie.
- Jeśli nie, użyj nowego serializatora, aby deserializować dane.
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;
}
Bezpieczne odczytywanie ładunków NRBF
Ładunek NRBF składa się z rekordów serialization reprezentujących serializowane obiekty i ich metadane. Aby odczytać cały ładunek i pobrać obiekt główny, należy wywołać metodę Decode .
Metoda Decode zwraca SerializationRecord wystąpienie. SerializationRecord jest abstrakcyjną klasą, która reprezentuje serialization rekord i udostępnia trzy samoopisujące się właściwości: Id, , RecordTypei TypeName. Uwidacznia jedną metodę , TypeNameMatchesktóra porównuje nazwę typu odczytaną z ładunku (i uwidoczniona za pośrednictwem TypeName właściwości) względem określonego typu. Ta metoda ignoruje nazwy zestawów, więc użytkownicy nie muszą martwić się o przekazywanie typów i przechowywanie wersji zestawu. Nie uwzględnia również nazw elementów członkowskich ani ich typów (ponieważ uzyskanie tych informacji wymaga ładowania typu).
using System.Formats.Nrbf;
static T Pseudocode<T>(Stream payload)
{
SerializationRecord record = NrbfDecoder.Read(payload);
if (!record.TypeNameMatches(typeof(T))
{
throw new Exception($"Expected the record to match type name `{typeof(T).AssemblyQualifiedName}`, but got `{record.TypeName.AssemblyQualifiedName}`."
}
}
Istnieje kilkanaście różnych serializationtypów rekordów. Ta biblioteka udostępnia zestaw abstrakcji, więc wystarczy nauczyć się tylko kilku z nich:
- PrimitiveTypeRecord<T>: opisuje wszystkie typy pierwotne natywnie obsługiwane przez NRBF (
string
,bool
,double
ushort
short
int
char
uint
long
sbyte
byte
float
decimal
ulong
TimeSpan
i ).DateTime
- Uwidacznia wartość za pośrednictwem
Value
właściwości . - PrimitiveTypeRecord<T> pochodzi z niegenerycznej PrimitiveTypeRecord, która również uwidacznia Value właściwość . Jednak w klasie bazowej wartość jest zwracana jako
object
(która wprowadza boxing dla typów wartości).
- Uwidacznia wartość za pośrednictwem
- ClassRecord: opisuje wszystkie
class
istruct
oprócz wyżej wymienionych typów pierwotnych. - ArrayRecord: opisuje wszystkie rekordy tablic, w tym tablice postrzępione i wielowymiarowe.
- SZArrayRecord<T>: opisuje jednowymiarowe, bezindeksowane rekordy tablicowe, w których
T
może być typem pierwotnym lub .ClassRecord
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.");
}
Obok Decodemetody , nrbfDecoder uwidacznia metodę DecodeClassRecord , która zwraca ClassRecord (lub zgłasza).
KlasaRecord
Najważniejszym typem pochodzącym z SerializationRecord klasy jest ClassRecord, który reprezentuje wszystkie struct
class
wystąpienia obok tablic i natywnie obsługiwanych typów pierwotnych. Umożliwia odczytywanie wszystkich nazw i wartości elementów członkowskich. Aby zrozumieć, czym jest element członkowski, zobacz dokumentację BinaryFormatterfunkcji.
Interfejs API, który udostępnia:
- MemberNames właściwość, która pobiera nazwy serializowanych elementów członkowskich.
- HasMember metoda sprawdzająca, czy element członkowski podanej nazwy znajdował się w ładunku. Został on zaprojektowany do obsługi scenariuszy przechowywania wersji, w których można było zmienić nazwę danego elementu członkowskiego.
- Zestaw dedykowanych metod pobierania wartości pierwotnych podanej nazwy składowej: GetString, , , GetDecimalGetInt16GetUInt16GetCharGetInt32GetSByteGetUInt32GetInt64GetByteGetBooleanGetSingleGetDoubleGetUInt64GetTimeSpani .GetDateTime
- GetClassRecord i GetArrayRecord metody pobierania wystąpienia danych typów rekordów.
- GetSerializationRecord w celu pobrania dowolnego serialization rekordu i GetRawValue pobrania dowolnego serialization rekordu lub nieprzetworzonej wartości pierwotnej.
Poniższy fragment kodu pokazuje ClassRecord działanie:
[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
ClassInstance = new()
{
Text = rootRecord
.GetClassRecord(nameof(Sample.ClassInstance))!
.GetString(nameof(Sample.Text))
}
};
TablicaRekord
ArrayRecord definiuje podstawowe zachowanie rekordów tablic NRBF i udostępnia bazę dla klas pochodnych. Zapewnia dwie właściwości:
- Rank , która pobiera rangę tablicy.
- Lengths które pobierają bufor liczb całkowitych reprezentujących liczbę elementów w każdym wymiarze.
Zapewnia również jedną metodę: GetArray. Gdy jest używany po raz pierwszy, przydziela tablicę i wypełnia ją danymi podanymi w serializowanych rekordach (w przypadku natywnie obsługiwanych typów pierwotnych, takich jak string
lub int
) lub serializacji rekordów (w przypadku tablic typów złożonych).
GetArray wymaga obowiązkowego argumentu, który określa typ oczekiwanej tablicy. Jeśli na przykład rekord powinien być tablicą liczb całkowitych 2D, expectedArrayType
element musi zostać podany jako typeof(int[,])
i zwracana tablica jest również int[,]
następująca:
ArrayRecord arrayRecord = (ArrayRecord)NrbfDecoder.Decode(stream);
int[,] array2d = (int[,])arrayRecord.GetArray(typeof(int[,]));
Jeśli występuje niezgodność typu (na przykład osoba atakująca dostarczyła ładunek z tablicą dwóch miliardów ciągów), metoda zgłasza błąd InvalidOperationException.
Funkcja NrbfDecoder nie ładuje ani nie tworzy wystąpienia żadnych typów niestandardowych, więc w przypadku tablic typów złożonych zwraca tablicę SerializationRecord.
[Serializable]
public class ComplexType3D
{
public int I, J, K;
}
ArrayRecord arrayRecord = (ArrayRecord)NrbfDecoder.Decode(payload);
SerializationRecord[] records = (SerializationRecord[])arrayRecord.GetArray(expectedArrayType: typeof(ComplexType3D[]));
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();
Program .NET Framework obsługiwał tablice indeksowane niezerowo w ładunkach NRBF, ale ta obsługa nigdy nie została przekierowana do platformy .NET (Core). W związku z tym nrbfDecoder nie obsługuje dekodowania niezerowych indeksowanych tablic.
SZArrayRecord
SZArrayRecord<T>
Definiuje podstawowe zachowanie dla jednowymiarowych, bezindeksowanych rekordów tablicy NRBF i zapewnia podstawę dla klas pochodnych. Może T
to być jeden z natywnie obsługiwanych typów pierwotnych lub SerializationRecord.
Zapewnia Length właściwość i GetArray przeciążenie, które zwraca wartość T[]
.
[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()
};