Leitura de cargas de BinaryFormatter (NRBF)
BinaryFormatter usou o .NET Remoting: Formato Binário para serialization. Esse formato é conhecido por sua abreviação de MS-NRBF ou apenas NRBF. Um desafio comum envolvido na migração é BinaryFormatter lidar com cargas persistentes para o armazenamento, pois a leitura dessas cargas antes exigiam BinaryFormatter. Alguns sistemas precisam manter a capacidade de ler essas cargas para migrações graduais para novos serializadores, evitando uma referência ao próprio BinaryFormatter.
Como parte do .NET 9, uma nova classe NrbfDecoder foi introduzida para decodificar cargas NRBF sem executar a desserialização da carga. Essa API pode ser usada com segurança para decodificar cargas confiáveis ou não confiáveis sem nenhum dos riscos que a desserialização de BinaryFormatter acarreta. No entanto, o NrbfDecoder apenas decodifica os dados em estruturas que um aplicativo pode processar posteriormente. Deve-se ter cuidado ao usar o NrbfDecoder para carregar com segurança os dados nas instâncias apropriadas.
Você pode pensar em NrbfDecoder como sendo o equivalente a usar um leitor JSON/XML sem o desserializador.
NrbfDecoder
NrbfDecoder faz parte do novo pacote NuGet System.Formats.Nrbf. Ele tem como destino não apenas o .NET 9, mas também os nomes mais antigos, como .NET Standard 2.0 e .NET Framework. Esse direcionamento múltiplo possibilita que todos que usam uma versão com suporte do .NET migrem para longe de BinaryFormatter. NrbfDecoder pode ler cargas que foram serializadas com BinaryFormatter using FormatterTypeStyle.TypesAlways (o padrão).
O NrbfDecoder foi projetado para tratar todas as entradas como não confiáveis. Como tal, ele tem estes princípios:
- Nenhum carregamento de tipo (para evitar riscos, como execução remota de código).
- Nenhuma recursão de qualquer tipo (para evitar recursão desassociada, excedente de pilha e negação de serviço).
- Nenhuma pré-alocação de buffer com base no tamanho fornecido na carga, se a carga for muito pequena para conter os dados prometidos (para evitar ficar sem memória e negação de serviço).
- Decodificar cada parte da entrada apenas uma vez (para executar a mesma quantidade de trabalho que o invasor potencial que criou a carga).
- Use hash randomizado resistente a colisões para armazenar registros referenciados por outros registros (para evitar ficar sem memória para um dicionário apoiado por uma matriz cujo tamanho depende do número de colisões de código hash).
- Somente tipos primitivos podem ser instanciados de forma implícita. As matrizes podem ser instanciadas 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 proteções.
Desserializar um conjunto fechado de tipos
O NrbfDecoder é útil somente quando a lista de tipos serializados é um conjunto conhecido e fechado. Em outras palavras, você precisa saber antecipadamente o que deseja ler, pois também precisa criar instâncias desses tipos e preenchê-las com dados lidos da carga. Considere dois exemplos opostos:
- Todos os tipos de
[Serializable]
de Quartz.NET que podem ser persistidos pela própria biblioteca sãosealed
. Portanto, não há tipos personalizados que os usuários possam criar, e a carga 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. - O SettingsPropertyValue tipo expõe a propriedade PropertyValue do tipo
object
que pode ser usada BinaryFormatter internamente para serializar e desserializar qualquer objeto 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 interruptivas na API.
Identificar cargas NRBF
O 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 esses métodos ao migrar cargas persistidas com BinaryFormatter para um serializador diferente:
- Verifique se a carga lida do armazenamento é uma carga NRBF com NrbfDecoder.StartsWithPayloadHeader.
- Nesse caso, leia-o com NrbfDecoder.Decode, serialize-o novamente com um novo serializador e substitua os dados no armazenamento.
- Caso contrário, use o novo serializador para desserializar os dados.
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;
}
Ler cargas NRBF com segurança
A carga NRBF consiste em serialization registros que representam os objetos serializados e seus metadados. Para ler toda a carga e obter o objeto raiz, você precisa chamar o método Decode.
O método Decode retorna uma instância de SerializationRecord. 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 (e exposto por meio da propriedade TypeName) 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 tipos e o controle de versão do assembly. Ele também não considera nomes de membros ou seus tipos (porque obter essas informações exigiria o carregamento de tipo).
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}`."
}
}
Há mais de uma dúzia de tipos serializationde registro diferentes. Esta biblioteca fornece um conjunto de abstrações, portanto, você só precisa aprender algumas delas:
- PrimitiveTypeRecord<T>: descreve todos os tipos primitivos com suporte nativo pelo NRBF (
string
,bool
,byte
,sbyte
,char
,short
,ushort
,int
,uint
,long
,ulong
,float
,double
,decimal
,TimeSpan
eDateTime
).- Expõe o valor por meio da propriedade
Value
. - PrimitiveTypeRecord<T> deriva do PrimitiveTypeRecordnão genérico, que também expõe uma propriedade Value. No entanto, na classe base, o valor é retornado como
object
(que introduz o boxing para tipos de valor).
- Expõe o valor por meio da propriedade
- ClassRecord: descreve todos os
class
estruct
além dos tipos primitivos mencionados anteriormente. - ArrayRecord: descreve todos os registros de matriz, incluindo matrizes irregulares e multidimensionais.
- SZArrayRecord<T>: descreve registros de matriz unidimensional, com indexação zero, em que
T
pode ser um tipo primitivo ou um 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.");
}
Além disso Decode, o NrbfDecoder expõe um DecodeClassRecord método que retorna ClassRecord (ou lança).
ClassRecord
O tipo mais importante que deriva de SerializationRecord é ClassRecord, que representa todas as instâncias class
e struct
além de matrizes e tipos primitivos com suporte nativo. Ele permite que você leia todos os nomes e valores de membro. Para entender o que é membro, consulte a BinaryFormatter referência de funcionalidade.
A API que ela fornece:
- A propriedade MemberNames que obtém os nomes de membros serializados.
- O método HasMember que verifica se o membro de determinado nome estava presente no conteúdo. Ele foi projetado para lidar com cenários de controle de versão em que determinado membro poderia ter sido renomeado.
- Um conjunto de métodos dedicados para recuperar valores primitivos do nome do membro fornecido: GetString, GetBoolean, GetByte, GetSByte, GetChar, GetInt16, GetUInt16, GetInt32, GetUInt32, GetInt64, GetUInt64, GetSingle, GetDouble, GetDecimal, GetTimeSpan e GetDateTime
- Métodos GetClassRecord e GetArrayRecord para recuperar a instância de determinados tipos de registro.
- GetSerializationRecord para recuperar qualquer registro serialization e GetRawValue para recuperar qualquer registro serialization ou um valor primitivo bruto.
O snippet de código a seguir mostra 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 com suporte nativo, como string
ou int
) ou os 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 tipos incompatíveis (exemplo: o invasor forneceu uma carga com uma matriz de dois bilhões de cadeias de caracteres), o método gerará InvalidOperationException.
O 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 dá suporte a matrizes com indexação não zero em cargas NRBF, mas esse suporte nunca foi portado para o .NET (Core). O NrbfDecoder, portanto, não dá suporte à decodificação de matrizes indexadas diferentes de zero.
SZArrayRecord
SZArrayRecord<T>
define o comportamento principal para registros de matriz NRBF unidimensional, com indexação zero, e fornece uma base para classes derivadas. O T
pode ser um dos tipos primitivos com suporte nativo ou SerializationRecord.
Ele fornece uma propriedade Length e uma sobrecarga GetArray 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()
};