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ãosealed
. 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:
- Verifique se a carga útil lida do armazenamento é uma carga útil NRBF com NrbfDecoder.StartsWithPayloadHeader.
- Em caso afirmativo, 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;
}
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
,ushort
short
, ,int
,ulong
float
uint
double
decimal
long
TimeSpan
e ).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).
- Expõe o valor através da
- ClassRecord: descreve todos
class
estruct
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:
- MemberNames que obtém os nomes dos membros serializados.
- HasMember método que verifica se o membro do nome próprio estava presente na carga. 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 de membro fornecido: GetString, GetBoolean, GetByte, GetSByte, GetChar, GetInt16, GetInt32GetInt64GetUInt64GetUInt32GetUInt16GetDoubleGetDecimalGetSingleGetTimeSpane .GetDateTime
- GetClassRecord e GetArrayRecord métodos para recuperar instâncias de determinados tipos de registro.
- GetSerializationRecord para recuperar qualquer serialization registro e GetRawValue para recuperar qualquer serialization registro ou um valor primitivo bruto.
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()
};