Delen via


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, zijn sealed. 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:

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, , bytesbytechar, shortushortintuintlongulongfloatdoubledecimal, en ).TimeSpanDateTime
    • 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).
  • ClassRecord: beschrijft alle class en struct 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:

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()
};