Dela via


Läsnyttolaster BinaryFormatter (NRBF)

BinaryFormatteranvände .NET-fjärrkommunikation: Binärt format för serialization. Det här formatet är känt genom dess förkortning av MS-NRBF eller bara NRBF. En vanlig utmaning vid migrering från BinaryFormatter är att hantera nyttolaster som sparats till lagringen eftersom det tidigare krävdes BinaryFormatteratt läsa dessa nyttolaster. Vissa system måste behålla möjligheten att läsa dessa nyttolaster för gradvisa migreringar till nya serialiserare samtidigt som man undviker en referens till BinaryFormatter sig själv.

Som en del av .NET 9 introducerades en ny NrbfDecoder-klass för att avkoda NRBF-nyttolaster utan att utföra deserialisering av nyttolasten. Det här API:et kan på ett säkert sätt användas för att avkoda betrodda eller ej betrodda nyttolaster utan några av de risker som BinaryFormatter deserialisering medför. NrbfDecoder avkodar dock bara data till strukturer som ett program kan bearbeta ytterligare. Du måste vara försiktig när du använder NrbfDecoder för att på ett säkert sätt läsa in data i lämpliga instanser.

Du kan tänka NrbfDecoder dig att vara motsvarigheten till att använda en JSON/XML-läsare utan deserialiseraren.

NrbfDecoder

NrbfDecoder är en del av det nya NuGet-paketet System.Formats.Nrbf . Den riktar sig inte bara till .NET 9, utan även äldre monikers som .NET Standard 2.0 och .NET Framework. Den multi-targeting gör det möjligt för alla som använder en version av .NET som stöds att migrera bort från BinaryFormatter. NrbfDecoder kan läsa nyttolaster som serialiserades med BinaryFormatter hjälp av FormatterTypeStyle.TypesAlways (standard).

NrbfDecoder är utformat för att behandla alla indata som ej betrodda. Som sådan har den följande principer:

  • Ingen typinläsning av något slag (för att undvika risker som fjärrkörning av kod).
  • Ingen rekursion av något slag (för att undvika obundna rekursioner, stackspill och överbelastning).
  • Ingen buffertförallokering baserat på den storlek som anges i nyttolasten, om nyttolasten är för liten för att innehålla de utlovade data (för att undvika att minnet och överbelastningen börjar ta slut).
  • Avkoda varje del av indata bara en gång (för att utföra samma mängd arbete som den potentiella angriparen som skapade nyttolasten).
  • Använd kollisionsresistent randomiserad hashning för att lagra poster som refereras av andra poster (för att undvika att minnet börjar ta slut för ordlistor som backas upp av en matris vars storlek beror på antalet hash-kodkollisioner).
  • Endast primitiva typer kan instansieras på ett implicit sätt. Matriser kan instansieras på begäran. Andra typer instansieras aldrig.

När du använder NrbfDecoder är det viktigt att inte återinföra dessa funktioner i allmän kod, eftersom detta skulle förhindra dessa skyddsåtgärder.

Deserialisera en sluten uppsättning typer

NrbfDecoder är bara användbart när listan över serialiserade typer är en känd, stängd uppsättning. För att uttrycka det på ett annat sätt måste du veta i förväg vad du vill läsa, eftersom du också måste skapa instanser av dessa typer och fylla dem med data som lästes från nyttolasten. Överväg två motsatta exempel:

  • Alla [Serializable] typer från Quartz.NET som kan bevaras av själva biblioteket är sealed. Det finns därför inga anpassade typer som användarna kan skapa, och nyttolasten kan bara innehålla kända typer. Typerna tillhandahåller också offentliga konstruktorer, så det är möjligt att återskapa dessa typer baserat på den information som läses från nyttolasten.
  • Typen SettingsPropertyValue exponerar egenskapen PropertyValue av typen object som kan användas BinaryFormatter internt för att serialisera och deserialisera alla objekt som lagrats i konfigurationsfilen. Den kan användas för att lagra ett heltal, en anpassad typ, en ordlista eller bokstavligen vad som helst. Därför är det omöjligt att migrera det här biblioteket utan att införa icke-bakåtkompatibla ändringar i API:et.

Identifiera NRBF-nyttolaster

NrbfDecoder innehåller två StartsWithPayloadHeader metoder som gör att du kan kontrollera om en viss ström eller buffert börjar med NRBF-huvudet. Vi rekommenderar att du använder dessa metoder när du migrerar nyttolaster som sparats med BinaryFormatter till en annan serialiserare:

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

Läsa NRBF-nyttolaster på ett säkert sätt

NRBF-nyttolasten består av serialization poster som representerar serialiserade objekt och deras metadata. Om du vill läsa hela nyttolasten och hämta rotobjektet måste du anropa Decode metoden.

Metoden Decode returnerar en SerializationRecord instans. SerializationRecord är en abstrakt klass som representerar serialization posten och innehåller tre självbeskrivande egenskaper: Id, RecordTypeoch TypeName. Den exponerar en metod, TypeNameMatches, som jämför typnamnet som lästs från nyttolasten (och exponeras via TypeName egenskapen) med den angivna typen. Den här metoden ignorerar sammansättningsnamn, så användarna behöver inte bekymra sig om vidarebefordran av typ och versionshantering för sammansättning. Den tar inte heller hänsyn till medlemsnamn eller deras typer (eftersom det skulle krävas typinläsning för att hämta den här informationen).

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}`."
    }
}

Det finns mer än ett dussin olika serializationposttyper. Det här biblioteket innehåller en uppsättning abstraktioner, så du behöver bara lära dig några av dem:

  • PrimitiveTypeRecord<T>: beskriver alla primitiva typer som stöds internt av NRBF (string, , bool, bytesbyte, char, short, ushort, int, uint, longulong, , float, , double, decimal, och TimeSpanDateTime).
    • Exponerar värdet via egenskapen Value .
    • PrimitiveTypeRecord<T> härleds från den icke-generiska PrimitiveTypeRecord, som också exponerar en Value egenskap. Men i basklassen returneras värdet som object (vilket introducerar boxning för värdetyper).
  • ClassRecord: beskriver alla class och struct förutom de ovan nämnda primitiva typerna.
  • ArrayRecord: beskriver alla matrisposter, inklusive ojämna och flerdimensionella matriser.
  • SZArrayRecord<T>: beskriver endimensionella, nollindexerade matrisposter, där T kan vara antingen en primitiv typ eller en 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.");
}

Bredvid Decodeexponerar NrbfDecoder en DecodeClassRecord metod som returnerar ClassRecord (eller genererar).

ClassRecord

Den viktigaste typen som härleds från SerializationRecord är ClassRecord, som representerar alla class instanser och struct instanser bredvid matriser och primitiva typer som stöds internt. Det gör att du kan läsa alla medlemsnamn och värden. Information om vad medlem är finns i funktionsreferensenBinaryFormatter.

API:et innehåller:

Följande kodfragment visas ClassRecord i praktiken:

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

ArrayRecord

ArrayRecord definierar kärnbeteendet för NRBF-matrisposter och ger en bas för härledda klasser. Den innehåller två egenskaper:

  • Rank som hämtar matrisens rangordning.
  • Lengths som får en buffert med heltal som representerar antalet element i varje dimension.

Den innehåller också en metod: GetArray. När den används för första gången allokerar den en matris och fyller den med data som tillhandahålls i de serialiserade posterna (om de ursprungliga primitiva typerna som stöds som string eller int) eller själva serialiserade posterna (om det gäller matriser av komplexa typer).

GetArray kräver ett obligatoriskt argument som anger typen av förväntad matris. Om posten till exempel ska vara en 2D-matris med heltal måste anges expectedArrayType som typeof(int[,]) och den returnerade matrisen är också int[,]:

ArrayRecord arrayRecord = (ArrayRecord)NrbfDecoder.Decode(stream);
int[,] array2d = (int[,])arrayRecord.GetArray(typeof(int[,]));

Om det finns en typmatchningsfel (exempel: angriparen har tillhandahållit en nyttolast med en matris med två miljarder strängar) genererar InvalidOperationExceptionmetoden .

NrbfDecoder läser inte in eller instansierar några anpassade typer, så om det gäller matriser av komplexa typer returneras en matris med 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();

.NET Framework har stöd för indexerade matriser som inte är noll i NRBF-nyttolaster, men det här stödet portades aldrig till .NET (Core). NrbfDecoder stöder därför inte avkodning av indexerade matriser som inte är noll.

SZArrayRecord

SZArrayRecord<T> definierar kärnbeteendet för nrbf-poster med en enda dimensionell, nollindexerad matris och ger en bas för härledda klasser. T Kan vara en av de primitiva typer som stöds internt eller SerializationRecord.

Den innehåller en Length egenskap och en GetArray överlagring som returnerar 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()
};