Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
BinaryFormatter verwendete das .NET Remoting: Binary Format für die Serialisierung. Dieses Format ist unter der Abkürzung MS-NRBF oder einfach NRBF bekannt. Eine häufige Herausforderung bei der Migration von BinaryFormatter ist der Umgang mit Nutzlasten, die im Speicher gespeichert werden, da für das Lesen dieser Nutzlasten bisher BinaryFormatter erforderlich war. Einige Systeme müssen die Möglichkeit behalten, diese Nutzlasten für die schrittweise Migration zu neuen Serialisierungsmodulen zu lesen und gleichzeitig einen Verweis auf BinaryFormatter selbst zu vermeiden.
Als Teil von .NET 9 wurde eine neue NrbfDecoder-Klasse eingeführt, um NRBF-Nutzlasten zu decodieren, ohne eine Deserialisierung der Nutzlast durchzuführen. Diese API kann sicher verwendet werden, um vertrauenswürdige oder nicht vertrauenswürdige Nutzlasten ohne die Risiken zu decodieren, die die BinaryFormatter-Deserialisierung mit sich bringt. Die NrbfDecoder-Klasse dekodiert die Daten jedoch lediglich in Strukturen, die eine Anwendung weiterverarbeiten kann. Bei der Verwendung von NrbfDecoder ist Vorsicht geboten, um die Daten sicher in die entsprechenden Instanzen zu laden.
Achtung
NrbfDecoder ist eine Implementierung eines NRBF-Readers, aber sein Verhalten folgt nicht streng der Implementierung von BinaryFormatter. Daher sollten Sie die Ausgabe von NrbfDecoder nicht verwenden, um festzustellen, ob ein Aufruf von BinaryFormatter sicher wäre.
Sie können sich NrbfDecoder als Entsprechung der Verwendung eines JSON/XML-Readers ohne Deserialisierer vorstellen.
NrbfDecoder
NrbfDecoder ist Teil des neuen NuGet-Pakets System.Formats.Nrbf. Es zielt nicht nur auf .NET 9, sondern auch ältere Moniker wie .NET Standard 2.0 und .NET Framework ab. Diese Festlegung von Zielversionen ermöglicht es allen Benutzern, die eine unterstützte Version von .NET verwenden, von BinaryFormatter zu migrieren. NrbfDecoder kann Nutzlasten lesen, die mit BinaryFormatter deserialisiert wurden, und zwar unter Verwendung von FormatterTypeStyle.TypesAlways (der Standard).
NrbfDecoder ist so konzipiert, dass alle Eingaben als nicht vertrauenswürdig behandelt werden. Daher gelten folgende Grundsätze:
- Kein Laden von Typen jeglicher Art (um Risiken wie die Ausführung von Remotecode zu vermeiden)
- Keine Rekursion jeglicher Art (um ungebundene Rekursion, Stapelüberlauf und Denial of Service zu vermeiden)
- Keine Puffervorabbelegung basierend auf der in der Nutzlast angegebenen Größe, wenn die Nutzlast zu klein ist, um die zugesagten Daten zu enthalten (um Speicherplatzmangel und Denial of Service zu vermeiden)
- Einmaliges Decodieren aller Eingabeteile (um den gleichen Arbeitsaufwand wie der potenzielle Angreifer zu haben, der die Nutzlast erstellt hat)
- Verwenden von konfliktbeständigem Hashing nach dem Zufallsprinzip zum Speichern von Datensätzen, auf die von anderen Datensätzen verwiesen wird (um zu wenig Arbeitsspeicher für Wörterbücher zu vermeiden, die von einem Array unterstützt werden, dessen Größe von der Anzahl der Hashcodekonflikte abhängt)
- Nur primitive Typen können implizit instanziiert werden. Arrays können bedarfsgesteuert instanziiert werden. Andere Typen werden nie instanziiert.
Achtung
Bei der Verwendung von NrbfDecoderist es wichtig, diese Funktionen nicht im allgemeinen Code wiedereinzuleiten, da dies diese Sicherheitsvorkehrungen negiert.
Deserialisieren eines geschlossenen Satzes von Typen
NrbfDecoder ist nur dann nützlich, wenn es sich bei der Liste der serialisierten Typen um eine bekannte, geschlossene Menge handelt. Anders ausgedrückt: Sie müssen vorab wissen, was Sie lesen möchten, da Sie auch Instanzen dieser Typen erstellen und mit Daten auffüllen müssen, die aus der Nutzlast gelesen wurden. Betrachten Sie zwei gegensätzliche Beispiele:
- Alle
[Serializable]
-Typen von Quartz.NET, die von der Bibliothek selbst gespeichert werden können, sind vom Typsealed
. Es gibt also keine benutzerdefinierten Typen, die Benutzer erstellen können, und die Nutzlast darf nur bekannte Typen enthalten. Die Typen stellen zudem öffentliche Konstruktoren bereit, sodass es möglich ist, diese Typen basierend auf den Informationen neu zu erstellen, die aus der Nutzlast gelesen werden. - Der SettingsPropertyValue-Typ macht die Eigenschaft PropertyValue des Typs
object
verfügbar, die intern möglicherweise BinaryFormatter verwendet, um alle Objekte zu serialisieren und zu deserialisieren, die in der Konfigurationsdatei gespeichert wurden. Es kann dazu verwendet werden, eine ganze Zahl, einen benutzerdefinierten Typ, ein Wörterbuch und vieles mehr zu speichern. Daher ist es unmöglich, diese Bibliothek zu migrieren, ohne dass Breaking Changes an der API eingeführt werden.
Ermitteln von NRBF-Nutzlasten
NrbfDecoder bietet zwei StartsWithPayloadHeader-Methoden, mit denen Sie überprüfen können, ob ein bestimmter Stream oder Puffer mit dem NRBF-Header beginnt. Es wird empfohlen, diese Methoden zu verwenden, wenn Sie mit BinaryFormatter gespeicherte Nutzlasten zu einem anderen Serialisierungsmodul migrieren:
- Überprüfen Sie, ob es sich bei der aus dem Speicher gelesenen Nutzlast um eine NRBF-Nutzlast mit NrbfDecoder.StartsWithPayloadHeader handelt.
- Wenn ja, lesen Sie sie mit NrbfDecoder.Decode, serialisieren Sie sie wieder mit einem neuen Serialisierungsmodul, und überschreiben Sie die Daten im Speicher.
- Wenn nicht, verwenden Sie das neue Serialisierungsmodul, um die Daten zu deserialisieren.
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;
}
Sicheres Lesen von NRBF-Nutzlasten
Die NRBF-Nutzlast besteht aus Serialisierungsdatensätzen, die die serialisierten Objekte und deren Metadaten darstellen. Um die gesamte Nutzlast zu lesen und das Stammobjekt abzurufen, müssen Sie die Decode-Methode aufrufen.
Die Decode-Methode gibt eine SerializationRecord-Instanz zurück. SerializationRecord ist eine abstrakte Klasse, die den Record der Serialisierung darstellt und drei selbstbeschreibende Eigenschaften bietet: Id, RecordType und TypeName.
Hinweis
Ein Angreifer könnte eine Nutzlast mit Zyklen erstellen (Beispiel: Klasse oder ein Array von Objekten mit einem Verweis auf sich selbst). Die Id gibt eine Instanz von SerializationRecordId zurück, die IEquatable<T> implementiert und u. a. verwendet werden kann, um Zyklen in decodierten Datensätzen zu erkennen.
SerializationRecord stellt eine Methode, TypeNameMatches, zur Verfügung, die den aus der Payload gelesenen (und über die Eigenschaft TypeName zur Verfügung gestellten) Typnamen mit dem angegebenen Typ vergleicht. Diese Methode ignoriert Assemblynamen, sodass Benutzer sich keine Gedanken über die Typweiterleitung und Assemblyversionsverwaltung machen müssen. Sie berücksichtigt außerdem keine Membernamen oder ihre Typen (da das Abrufen dieser Informationen das Laden von Typen erfordern würde).
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}`.");
}
}
Es gibt mehr als ein Dutzend verschiedener Datensatztypen für die Serialisierung. Diese Bibliothek bietet eine Reihe von Abstraktionen, sodass Sie sich nur mit einigen davon vertraut machen müssen:
-
PrimitiveTypeRecord<T>: beschreibt alle primitiven Typen, die nativ von NRBF unterstützt werden (
string
,bool
,byte
,sbyte
,char
,short
,ushort
,int
,uint
,long
,ulong
,float
,double
,decimal
,TimeSpan
undDateTime
).- Macht den Wert über die Eigenschaft
Value
verfügbar. -
PrimitiveTypeRecord<T> wird vom nicht generischen PrimitiveTypeRecord-Element abgeleitet, das auch eine Value-Eigenschaft verfügbar macht. Für die Basisklasse wird der Wert jedoch als
object
zurückgegeben (wodurch Boxing für Werttypen eingeführt wird).
- Macht den Wert über die Eigenschaft
-
ClassRecord: beschreibt alle
class
- undstruct
-Elemente neben den oben genannten primitiven Typen. - ArrayRecord: beschreibt alle Arraydatensätze, einschließlich Jagged Arrays und mehrdimensionaler Arrays angeben.
-
SZArrayRecord<T>: beschreibt eindimensionale Arraydatensätze mit Nullindex, wobei
T
entweder ein primitiver Typ oder SerializationRecord sein kann.
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.");
}
Außerdem Decode macht NrbfDecoder eine DecodeClassRecord-Methode verfügbar, die ClassRecord zurückgibt (oder auslöst).
ClassRecord
Der wichtigste Typ, der von SerializationRecord abgeleitet wird, ist das ClassRecord-Element, das alle class
-und struct
-Instanzen neben Arrays und nativen unterstützten primitiven Typen darstellt. Damit können Sie alle Membernamen und -werte lesen. Informationen dazu, was ein Member ist, finden Sie in der BinaryFormatter-Funktionsreferenz.
Die API stellt Folgendes bereit:
- Eigenschaft MemberNames, die die Namen serialisierter Member abruft
- HasMember-Methode, die überprüft, ob ein Member mit einem bestimmten Namen in der Nutzlast vorhanden war. Sie wurde für die Behandlung von Versionsverwaltungsszenarien entwickelt, in denen bestimmte Member möglicherweise umbenannt wurden.
- Ein Satz dedizierter Methoden zum Abrufen primitiver Werte des angegebenen Membernamens: GetString, GetBoolean, GetByte, GetSByte, GetChar, GetInt16, GetUInt16, GetInt32, GetUInt32, GetInt64, GetUInt64, GetSingle, GetDouble, GetDecimal, GetTimeSpan und GetDateTime.
- GetClassRecord ruft eine Instanz von [ClassRecord] ab. Im Falle eines Zirkels ist es die gleiche Instanz des aktuellen [ClassRecord] mit dem gleichen Id.
- GetArrayRecord ruft eine Instanz von [ArrayRecord] ab.
- GetSerializationRecord zum Abrufen eines beliebigen Serialisierungs-Records und GetRawValue zum Abrufen eines beliebigen Serialisierungs-Records oder eines Raw-Primitive-Wertes.
Der folgende Codeschnipsel zeigt ClassRecord in Aktion:
[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 definiert das Kernverhalten für NRBF-Arraydatensätze und bietet eine Basis für abgeleitete Klassen. Das Element stellt zwei Eigenschaften bereit:
- Rank, das den Rang des Arrays erhält.
- Lengths, der einen Puffer mit ganzen Zahlen erhält, die die Anzahl der Elemente in jeder Dimension darstellen. Es wird empfohlen, die Gesamtlänge des bereitgestellten Arraydatensatzes zu überprüfen, bevor GetArrayaufgerufen wird.
Es bietet auch eine Methode: GetArray. Wenn sie zum ersten Mal verwendet wird, wird ein Array zugewiesen und mit den Daten aus den serialisierten Datensätzen (im Fall von nativ unterstützten primitiven Typen wie string
oder int
) oder mit den serialisierten Datensätzen selbst (im Fall von Arrays komplexer Typen) gefüllt.
GetArray erfordert ein obligatorisches Argument, das den Typ des erwarteten Arrays angibt. Wenn der Datensatz beispielsweise ein 2D-Array mit ganzen Zahlen sein soll, muss expectedArrayType
als typeof(int[,])
angegeben werden, und das zurückgegebene Array ist ebenfalls 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[,]));
Wenn ein Typenkonflikt vorliegt (Beispiel: Der Angreifer hat eine Nutzlast mit einem Array von zwei Milliarden Zeichenfolgen bereitgestellt.), löst die Methode InvalidOperationException aus.
Achtung
Leider erleichtert das NRBF-Format einem Angreifer das Komprimieren einer großen Anzahl von Nullarrayelementen. Aus diesem Grund wird empfohlen, immer die Gesamtlänge des Arrays zu überprüfen, bevor GetArrayaufgerufen wird. Außerdem akzeptiert GetArray ein optionales boolesches allowNulls
Argument, das, wenn es auf false
festgelegt ist, Null-Fehler auslöst.
NrbfDecoder lädt oder instanziiert keine benutzerdefinierten Typen; im Falle von Arrays komplexer Typen wird ein Array von SerializationRecord zurückgegeben.
[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 hat Arrays ohne Nullindex innerhalb von NRBF-Nutzlasten unterstützt, diese Unterstützung wurde jedoch nie zu .NET (Core) portiert. NrbfDecoder unterstützt daher nicht die Dekodierung von indizierten Arrays ungleich Null.
SZArrayRecord
SZArrayRecord<T>
definiert das Kernverhalten für eindimensionale NRBF-Arraydatensätze mit Nullindex und bietet eine Basis für abgeleitete Klassen. Das T
-Element kann einer der nativ unterstützten primitiven Typ oder SerializationRecord sein.
Es stellt eine Length-Eigenschaft und eine GetArray-Überladung bereit, die T[]
zurückgibt.
[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()
};