Leesladingen BinaryFormatter (NRBF)
BinaryFormatter de .NET Remoting gebruikt: Binaire indeling voor serialisatie. Deze indeling staat bekend door de afkorting MS-NRBF of gewoon NRBF. Een veelvoorkomende uitdaging bij het migreren van BinaryFormatter waaruit wordt gemigreerd, is te maken met nettoladingen die naar de opslag worden bewaard, omdat deze nettoladingen eerder vereist waren BinaryFormatter. Sommige systemen moeten de mogelijkheid behouden om deze nettoladingen te lezen voor geleidelijke migraties naar nieuwe serializers, terwijl een verwijzing naar BinaryFormatter zichzelf wordt vermeden.
Als onderdeel van .NET 9 werd een nieuwe NrbfDecoder-klasse geïntroduceerd om NRBF-nettoladingen te decoderen zonder deserialisatie van de nettolading uit te voeren. Deze API kan veilig worden gebruikt om vertrouwde of niet-vertrouwde nettoladingen te decoderen zonder de risico's die BinaryFormatter deserialisatie met zich meebrengt. NrbfDecoder ontsleutelt echter alleen de gegevens in structuren die een toepassing verder kan verwerken. Zorg ervoor dat u nrbfDecoder gebruikt om de gegevens veilig in de juiste exemplaren te laden.
Voorzichtigheid
NrbfDecoder is een implementatie van een NRBF-lezer, maar het gedrag ervan volgt niet strikt BinaryFormatterimplementatie. U moet dus niet de uitvoer van NrbfDecoder gebruiken om te bepalen of een aanroep naar BinaryFormatter veilig is.
U kunt het equivalent van het gebruik van een JSON-/XML-lezer beschouwen NrbfDecoder zonder de deserializer.
NrbfDecoder
NrbfDecoder maakt deel uit van het nieuwe NuGet-pakket System.Formats.Nrbf . Het is niet alleen gericht op .NET 9, maar ook op oudere monikers zoals .NET Standard 2.0 en .NET Framework. Deze multi-targeting maakt het mogelijk voor iedereen die een ondersteunde versie van .NET gebruikt om vandaan BinaryFormatterte migreren. NrbfDecoder kan nettoladingen lezen die zijn geserialiseerd met BinaryFormatter behulp van FormatterTypeStyle.TypesAlways (de standaardinstelling).
NrbfDecoder is ontworpen om alle invoer te behandelen als niet-vertrouwd. Als zodanig heeft het de volgende principes:
- Geen type laden van welk type dan ook (om risico's zoals het uitvoeren van externe code te voorkomen).
- Geen recursie van elk type (om niet-afhankelijke recursie, stack-overloop en Denial of Service te voorkomen).
- Er is geen buffer vooraf toegewezen op basis van de grootte die is opgegeven in de nettolading, als de nettolading te klein is om de beloofde gegevens te bevatten (om te voorkomen dat er onvoldoende geheugen en Denial of Service beschikbaar is).
- Decodeer elk deel van de invoer slechts één keer (om dezelfde hoeveelheid werk uit te voeren als de potentiële aanvaller die de nettolading heeft gemaakt).
- Gebruik botsingsbestendige gerandomiseerde hashing om records op te slaan waarnaar wordt verwezen door andere records (om te voorkomen dat er onvoldoende geheugen beschikbaar is voor woordenlijst die wordt ondersteund door een matrix waarvan de grootte afhankelijk is van het aantal hash-codeconflicten).
- Alleen primitieve typen kunnen op impliciete wijze worden geïnstantieerd. Matrices kunnen op aanvraag worden geïnstantieerd. Andere typen worden nooit geïnstantieerd.
Voorzichtigheid
Wanneer u NrbfDecodergebruikt, is het belangrijk om deze mogelijkheden niet opnieuw in te voeren in algemene code, omdat dit deze beveiligingen zou ontzegt.
Een gesloten set typen deserialiseren
NrbfDecoder is alleen nuttig wanneer de lijst met geserialiseerde typen een bekende, gesloten set is. Als u het op een andere manier wilt plaatsen, moet u vooraf weten wat u wilt lezen, omdat u ook exemplaren van deze typen moet maken en ze moet vullen met gegevens die zijn gelezen uit de nettolading. Bekijk twee tegenovergestelde voorbeelden:
- Alle
[Serializable]
typen van Quartz.NET die door de bibliotheek zelf kunnen worden bewaard, zijnsealed
. Er zijn dus geen aangepaste typen die gebruikers kunnen maken en de nettolading kan alleen bekende typen bevatten. De typen bieden ook openbare constructors, dus het is mogelijk om deze typen opnieuw te maken op basis van de informatie die wordt gelezen uit de nettolading. - Het SettingsPropertyValue type toont de eigenschap PropertyValue van het type
object
dat intern kan worden gebruikt BinaryFormatter voor het serialiseren en deserialiseren van objecten die zijn opgeslagen in het configuratiebestand. Het kan worden gebruikt om een geheel getal, een aangepast type, een woordenlijst of letterlijk iets op te slaan. Daarom is het onmogelijk om deze bibliotheek te migreren zonder wijzigingen in de API te introduceren die fouten veroorzaken.
NRBF-nettoladingen identificeren
NrbfDecoder biedt twee StartsWithPayloadHeader methoden waarmee u kunt controleren of een bepaalde stream of buffer begint met de NRBF-header. Het is raadzaam om deze methoden te gebruiken wanneer u nettoladingen migreert die BinaryFormatter behouden blijven naar een andere serializer:
- Controleer of de nettolading gelezen uit de opslag een NRBF-nettolading is met NrbfDecoder.StartsWithPayloadHeader.
- Zo ja, lees het met NrbfDecoder.Decode, serialiseer deze met een nieuwe serialisatiefunctie en overschrijf de gegevens in de opslag.
- Als dat niet het is, gebruikt u de nieuwe serializer om de gegevens te deserialiseren.
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-nettoladingen veilig lezen
De NRBF-nettolading bestaat uit serialisatierecords die de geserialiseerde objecten en de bijbehorende metagegevens vertegenwoordigen. Als u de hele nettolading wilt lezen en het hoofdobject wilt ophalen, moet u de Decode methode aanroepen.
De Decode methode retourneert een SerializationRecord exemplaar. SerializationRecord is een abstracte klasse die de serialisatierecord vertegenwoordigt en drie zelfbeschrijfbare eigenschappen biedt: Id, RecordTypeen TypeName.
Notitie
Een aanvaller kan een payload maken met cycli (bijvoorbeeld: klasse of een array van objecten met een verwijzing naar zichzelf). De Id retourneert een exemplaar van SerializationRecordId dat IEquatable<T> implementeert en onder andere kan worden gebruikt om cycli in gedecodeerde records te detecteren.
SerializationRecord stelt één methode bloot, TypeNameMatches, waarmee de typenaam die wordt gelezen uit de gegevensbelasting (en beschikbaar gemaakt via de eigenschap TypeName) wordt vergeleken met het opgegeven type. Deze methode negeert assemblynamen, zodat gebruikers zich geen zorgen hoeven te maken over het doorsturen van typen en het versiebeheer van assembly's. Er wordt ook geen rekening gehouden met namen van leden of hun typen (omdat het ophalen van deze informatie het laden van het type vereist).
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}`.");
}
}
Er zijn meer dan tien verschillende serialisatie recordtypen. Deze bibliotheek biedt een reeks abstracties, dus u hoeft er slechts enkele te leren:
-
PrimitiveTypeRecord<T>: beschrijft alle primitieve typen die systeemeigen worden ondersteund door de NRBF (
string
,bool
, ,byte
sbyte
char
,short
ushort
int
uint
long
ulong
float
double
decimal
, en ).TimeSpan
DateTime
- Hiermee wordt de waarde via de
Value
eigenschap weergegeven. -
PrimitiveTypeRecord<T> is afgeleid van het niet-algemene PrimitiveTypeRecord, dat ook een Value eigenschap beschikbaar maakt. Maar op de basisklasse wordt de waarde geretourneerd als
object
(waarmee boksen voor waardetypen wordt geïntroduceerd).
- Hiermee wordt de waarde via de
-
ClassRecord: beschrijft alle
class
enstruct
naast de bovengenoemde primitieve typen. - ArrayRecord: beschrijft alle matrixrecords, inclusief onregelmatige en multidimensionale matrices.
-
SZArrayRecord<T>: beschrijft enkelvoudige, nul-geïndexeerde matrixrecords, waarbij
T
dit een primitief type of een 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.");
}
Naast Decodede NrbfDecoder wordt een DecodeClassRecord methode weergegeven die retourneert ClassRecord (of genereert).
ClassRecord
Het belangrijkste type waaruit is SerializationRecord afgeleid, is ClassRecord, dat alle class
exemplaren struct
naast matrices en systeemeigen ondersteunde primitieve typen vertegenwoordigt. Hiermee kunt u alle ledennamen en -waarden lezen. Als u wilt weten wat lid is, raadpleegt u de BinaryFormatter.
De API die het biedt:
- MemberNames eigenschap die de namen van geserialiseerde leden ophaalt.
- HasMember methode waarmee wordt gecontroleerd of lid van de opgegeven naam aanwezig was in de nettolading. Het is ontworpen voor het afhandelen van versiebeheerscenario's waarbij het opgegeven lid de naam van een lid kan hebben gewijzigd.
- Een set toegewezen methoden voor het ophalen van primitieve waarden van de opgegeven lidnaam: GetString, , GetBooleanGetByte, GetSByte, , , GetChar, GetInt16, GetUInt16GetInt32GetUInt32GetInt64GetUInt64, , GetSingle, , GetDouble, , GetDecimalen . GetTimeSpanGetDateTime
- GetClassRecord een exemplaar van [ClassRecord] ophaalt. In het geval van een cyclus is dit hetzelfde exemplaar van de huidige [ClassRecord] met dezelfde Id.
- GetArrayRecord een exemplaar van [ArrayRecord] ophaalt.
- GetSerializationRecord om een serialisatierecord op te halen en GetRawValue om een serialisatierecord of onbewerkte primitieve waarde op te halen.
Het volgende codefragment wordt in actie weergegeven 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 definieert het kerngedrag voor NRBF-matrixrecords en biedt een basis voor afgeleide klassen. Het biedt twee eigenschappen:
- Rank, die de rang van de array verkrijgt.
- Lengths, dat een buffer ontvangt van gehele getallen die het aantal elementen in elke dimensie vertegenwoordigen. Het is raadzaam om de totale lengte van de opgegeven matrixrecord te controleren voordat u GetArrayaanroept.
Het biedt ook één methode: GetArray. Wanneer deze voor het eerst wordt gebruikt, wijst deze een matrix toe en vult deze met de gegevens in de geserialiseerde records (in het geval van de systeemeigen ondersteunde primitieve typen zoals string
of int
) of de geserialiseerde records zelf (in het geval van matrices van complexe typen).
GetArray vereist een verplicht argument dat het type van de verwachte matrix aangeeft. Als de record bijvoorbeeld een 2D-matrix met gehele getallen moet zijn, moet deze expectedArrayType
worden opgegeven als typeof(int[,])
en is de geretourneerde matrix ook 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[,]));
Als er een type niet overeenkomt (bijvoorbeeld: de aanvaller heeft een nettolading met een matrix van twee miljard tekenreeksen geleverd), wordt de methode gegooid InvalidOperationException.
Voorzichtigheid
Helaas maakt de NRBF-indeling het voor een aanvaller gemakkelijk om een groot aantal null-matrixitems te comprimeren. Daarom is het raadzaam om altijd de totale lengte van de matrix te controleren voordat u GetArrayaanroept. Bovendien accepteert GetArray een optioneel allowNulls
Booleaanse argument, dat, indien ingesteld op false
, null-waarden genereert.
NrbfDecoder laadt of instantiëren geen aangepaste typen, dus in het geval van matrices van complexe typen retourneert het een matrix van 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 ondersteunde niet-nul geïndexeerde matrices binnen NRBF-nettoladingen, maar deze ondersteuning is nooit overgezet naar .NET (Core). NrbfDecoder biedt daarom geen ondersteuning voor het decoderen van niet-nul geïndexeerde matrices.
SZArrayRecord
SZArrayRecord<T>
definieert het kerngedrag voor NRBF-enkelvoudige, met nul geïndexeerde matrixrecords en biedt een basis voor afgeleide klassen. De T
kan een van de systeemeigen ondersteunde primitieve typen of SerializationRecord.
Het biedt een Length eigenschap en een GetArray overbelasting die retourneert 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()
};