Partilhar via


Leitura BinaryFormatter de cargas úteis (NRBF)

BinaryFormatter usou o .NET Remoting: Binary Format para serialization. Este formato é conhecido pela sua abreviatura de MS-NRBF ou apenas NRBF. Um desafio comum envolvido na migração é lidar com cargas úteis persistentes para armazenamento conforme a leitura dessas cargas úteis exigida BinaryFormatter BinaryFormatteranteriormente. Alguns sistemas precisam manter a capacidade de ler essas cargas úteis para migrações graduais para novos serializadores, evitando uma referência a BinaryFormatter si mesmo.

Como parte do .NET 9, uma nova classe NrbfDecoder foi introduzida para decodificar cargas úteis NRBF sem executar a desserialização da carga útil. Essa API pode ser usada com segurança para decodificar cargas úteis confiáveis ou não confiáveis sem nenhum dos riscos que BinaryFormatter a desserialização acarreta. No entanto, NrbfDecoder meramente decodifica os dados em estruturas que um aplicativo pode processar. Deve-se ter cuidado ao usar o NrbfDecoder para carregar os dados com segurança nas instâncias apropriadas.

Você pode pensar NrbfDecoder como sendo o equivalente a usar um leitor JSON/XML sem o desserializador.

NrbfDecoder

NrbfDecoder faz parte do novo pacote System.Formats.Nrbf NuGet. Ele tem como alvo não apenas o .NET 9, mas também apelidos mais antigos, como .NET Standard 2.0 e .NET Framework. Essa multiplataforma possibilita que todos que usam uma versão suportada do .NET migrem BinaryFormatterdo . NrbfDecoder pode ler cargas que foram serializadas com BinaryFormatter o uso ( FormatterTypeStyle.TypesAlways o padrão).

NrbfDecoder é projetado para tratar todas as entradas como não confiáveis. Como tal, tem os seguintes princípios:

  • Nenhum tipo de carregamento de qualquer tipo (para evitar riscos como a execução remota de código).
  • Nenhuma recursão de qualquer tipo (para evitar recursão não vinculada, estouro de pilha e negação de serviço).
  • Nenhuma pré-alocação de buffer com base no tamanho fornecido na carga útil, se a carga for muito pequena para conter os dados prometidos (para evitar falta de memória e negação de serviço).
  • Decodifice cada parte da entrada apenas uma vez (para executar a mesma quantidade de trabalho que o invasor potencial que criou a carga).
  • Use hashing aleatório resistente a colisões para armazenar registros referenciados por outros registros (para evitar ficar sem memória para dicionário apoiado por uma matriz cujo tamanho depende do número de colisões de código hash).
  • Apenas os tipos primitivos podem ser instanciados de forma implícita. Os arrays podem ser instanciados sob demanda. Outros tipos nunca são instanciados.

Ao usar o NrbfDecoder, é importante não reintroduzir esses recursos no código de uso geral, pois isso negaria essas salvaguardas.

Desserializar um conjunto fechado de tipos

NrbfDecoder é útil somente quando a lista de tipos serializados é um conjunto conhecido e fechado. Dito de outra forma, você precisa saber antecipadamente o que deseja ler, porque também precisa criar instâncias desses tipos e preenchê-las com dados que foram lidos da carga útil. Considere dois exemplos opostos:

  • Todos os [Serializable] tipos de Quartz.NET que podem ser persistidos pela própria biblioteca são sealed. Portanto, não há tipos personalizados que os usuários possam criar, e a carga útil pode conter apenas tipos conhecidos. Os tipos também fornecem construtores públicos, portanto, é possível recriar esses tipos com base nas informações lidas da carga útil.
  • O SettingsPropertyValue tipo expõe a propriedade PropertyValue do tipo object que pode ser usada BinaryFormatter internamente para serializar e desserializar qualquer objeto que foi armazenado no arquivo de configuração. Ele pode ser usado para armazenar um inteiro, um tipo personalizado, um dicionário ou literalmente qualquer coisa. Por isso, é impossível migrar essa biblioteca sem introduzir alterações significativas na API.

Identificar cargas úteis NRBF

NrbfDecoder fornece dois StartsWithPayloadHeader métodos que permitem verificar se um determinado fluxo ou buffer começa com o cabeçalho NRBF. É recomendável usar estes métodos ao migrar cargas úteis persistidas para BinaryFormatter um serializador diferente:

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

Leia com segurança as cargas úteis da NRBF

A carga útil NRBF consiste em serialization registros que representam os objetos serializados e seus metadados. Para ler toda a carga útil e obter o objeto raiz, você precisa chamar o Decode método.

O Decode método retorna uma SerializationRecord instância. SerializationRecord é uma classe abstrata que representa o serialization registro e fornece três propriedades autodescritivas: Id, RecordType, e TypeName. Ele expõe um método, TypeNameMatches, que compara o nome do tipo lido da carga útil (e exposto via TypeName propriedade) com o tipo especificado. Esse método ignora nomes de assembly, para que os usuários não precisem se preocupar com o encaminhamento de tipo e o controle de versão de assembly. Ele também não considera nomes de membros ou seus tipos (porque obter essas informações exigiria o carregamento de tipos).

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

Existem mais de uma dúzia de tipos de registos diferentes.serialization Esta biblioteca fornece um conjunto de abstrações, então você só precisa aprender algumas delas:

  • PrimitiveTypeRecord<T>: descreve todos os tipos primitivos suportados nativamente pela NRBF (string, bool, byte, sbyte, char, ushortshort, , int, ulongfloatuintdoubledecimallongTimeSpane ).DateTime
    • Expõe o valor através da Value propriedade.
    • PrimitiveTypeRecord<T> deriva do não-genérico PrimitiveTypeRecord, que também expõe uma Value propriedade. Mas na classe base, o valor é retornado como object (o que introduz boxing para tipos de valor).
  • ClassRecord: descreve todos class e struct além dos tipos primitivos acima mencionados.
  • ArrayRecord: descreve todos os registros de matriz, incluindo matrizes irregulares e multidimensionais.
  • SZArrayRecord<T>: descreve registros de matriz unidimensionais e indexados a zero, onde T pode ser um tipo primitivo ou um ClassRecordarquivo .
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.");
}

Ao lado Decodede , o NrbfDecoder expõe um DecodeClassRecord método que retorna ClassRecord (ou lança).

ClassRecord

O tipo mais importante que deriva é , que representa todas e struct instâncias class ao lado de matrizes e tipos primitivos suportados SerializationRecord ClassRecordnativamente. Ele permite que você leia todos os nomes e valores dos membros. Para entender o que é membro, consulte a referência de BinaryFormatter funcionalidade.

A API fornece:

O trecho de código a seguir é exibido ClassRecord em ação:

[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 define o comportamento principal para registros de matriz NRBF e fornece uma base para classes derivadas. Ele fornece duas propriedades:

  • Rank que obtém a classificação da matriz.
  • Lengths que obtêm um buffer de inteiros que representam o número de elementos em cada dimensão.

Ele também fornece um método: GetArray. Quando usado pela primeira vez, ele aloca uma matriz e a preenche com os dados fornecidos nos registros serializados (no caso dos tipos primitivos suportados nativamente como string ou int) ou nos próprios registros serializados (no caso de matrizes de tipos complexos).

GetArray requer um argumento obrigatório que especifica o tipo da matriz esperada. Por exemplo, se o registro deve ser uma matriz 2D de inteiros, o expectedArrayType deve ser fornecido como typeof(int[,]) e a matriz retornada também int[,]é :

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

Se houver uma incompatibilidade de tipo (exemplo: o invasor forneceu uma carga útil com uma matriz de dois bilhões de strings), o método lança InvalidOperationException.

NrbfDecoder não carrega ou instancia nenhum tipo personalizado, portanto, no caso de matrizes de tipos complexos, ele retorna uma matriz de 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();

O .NET Framework suportava matrizes indexadas diferentes de zero em cargas úteis NRBF, mas esse suporte nunca foi portado para .NET (Core). NrbfDecoder , portanto, não suporta a decodificação de matrizes indexadas diferentes de zero.

SZArrayRecord

SZArrayRecord<T> define o comportamento central para registros de matriz unidimensionais e indexados zero da NRBF e fornece uma base para classes derivadas. O T pode ser um dos tipos primitivos suportados nativamente ou SerializationRecord.

Ele fornece uma Length propriedade e uma GetArray sobrecarga que retorna 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()
};