읽기 BinaryFormatter (NRBF) 페이로드
BinaryFormatter은 serialization을 위해 .NET Remoting: 이진 형식를 사용했습니다. NRBF 또는 MS-NRBF의 약어로 이 형식이 알려져 있습니다. 이전에 BinaryFormatter이(가) 필요한 이러한 페이로드를 읽으면서 스토리지에 유지되는 페이로드를 처리하는 것이 BinaryFormatter(으)로부터의 마이그레이션과 관련된 일반적인 과제입니다. 일부 시스템은 이러한 페이로드를 읽는 기능을 유지함으로써 BinaryFormatter 자체에 대한 참조를 피하면서 새 직렬 변환기로 점진적으로 마이그레이션해야 합니다.
.NET 9의 일부로 페이로드의 역직렬화를 수행하지 않고 NRBF 페이로드를 디코딩하는 새로운 NrbfDecoder 클래스가 도입되었습니다. 이 API는 신뢰할 수 있거나 신뢰할 수 없는 페이로드를 BinaryFormatter 역직렬화에 수반되는 위험 없이 디코딩하는 데 안전하게 사용할 수 있습니다. 하지만 NrbfDecoder는 데이터를 애플리케이션이 추가로 처리할 수 있는 구조로 디코딩하기만 하면 됩니다. 데이터를 적절한 인스턴스에 안전하게 로드하기 위해 NrbfDecoder를 사용할 때는 주의해야 합니다.
주의
NrbfDecoderNRBF 판독기의 구현이지만 해당 동작은 BinaryFormatter구현을 엄격하게 따르지 않습니다. 따라서 NrbfDecoder 출력을 사용하여 BinaryFormatter 호출이 안전한지 여부를 결정해서는 안 됩니다.
NrbfDecoder이(가) JSON/XML 판독기를 역직렬 변환기 없이 사용하는 것과 같다고 생각할 수 있습니다.
NrbfDecoder
NrbfDecoder는 새 System.Formats.Nrbf NuGet 패키지의 일부입니다. .NET 9뿐만 아니라 .NET Framework 및 .NET Standard 2.0과 같은 이전 모니커도 대상으로 합니다. 이러한 다중 대상 지정을 사용하면 지원되는 .NET 버전을 사용하는 모든 사용자가 BinaryFormatter에서 마이그레이션할 수 있습니다. NrbfDecoder는 BinaryFormatter(기본값)을(를) 사용하여 FormatterTypeStyle.TypesAlways(으)로 직렬화된 페이로드를 읽을 수 있습니다.
NrbfDecoder는 모든 입력을 신뢰할 수 없는 것으로 처리하도록 설계되었습니다. 그러므로 다음과 같은 원칙이 있습니다.
- (원격 코드 실행과 같은 위험을 방지하기 위해) 어떤 종류의 형식도 로드하지 않습니다.
- (언바운드 재귀, 스택 오버플로 및 서비스 거부를 방지하기 위해) 어떤 종류의 재귀도 없습니다.
- 페이로드가 너무 작아서 약속된 데이터를 포함할 수 없는 경우, (메모리 부족 및 서비스 거부를 방지하기 위해) 페이로드에 제공된 크기에 따라 버퍼 사전 할당이 수행되지 않습니다.
- (페이로드를 만든 잠재적인 공격자와 동일한 양의 작업을 수행하려면) 입력의 모든 부분을 한 번만 디코딩합니다.
- (해시 코드 충돌 수에 따라 크기가 달라지는 배열에서 지원되는 사전의 메모리 부족을 방지하기 위해) 충돌 방지 임의 해시를 사용하여 다른 레코드에서 참조하는 레코드를 저장합니다.
- 기본 형식만 암시적 방식으로 인스턴스화할 수 있습니다. 요청에 따라 배열을 인스턴스화할 수 있습니다. 다른 형식은 인스턴스화되지 않습니다.
닫힌 형식 집합 역직렬화
직렬화된 형식 목록이 알려진 닫힌 집합인 경우에만 NrbfDecoder가 유용합니다. 다른 방법으로 설정하려면 읽을 내용을 미리 알고 있어야 하며, 이는 이러한 형식의 인스턴스를 만들고 페이로드에서 읽은 데이터로 채워야 하기 때문입니다. 반대 예제 두 가지를 고려합니다.
- 라이브러리 자체에서 유지할 수 있는
[Serializable]
에서의 모든 형식은sealed
입니다. 그러므로 페이로드에는 알려진 형식만 포함될 수 있으며, 사용자가 만들 수 있는 사용자 지정 형식은 없습니다. 또한 이러한 형식을 페이로드에서 읽은 정보에 따라 다시 만들 수 있으며, 이는 형식은 공용 생성자를 제공하기 때문입니다. -
SettingsPropertyValue 형식은 구성 파일에 저장된 개체를 직렬화 및 역직렬화하는 데 PropertyValue을(를) 내부적으로 사용할 수 있는 형식
object
의 속성 BinaryFormatter을(를) 노출합니다. 문자 그대로 모든 항목을 저장하는 데 사용할 수 있으며, 정수, 사용자 지정 형식, 사전 등이 포함됩니다. 그렇기 때문에, 호환성이 손상되는 변경 내용을 API에 도입하지 않고는 이 라이브러리를 마이그레이션할 수 없습니다.
NRBF 페이로드 식별
NrbfDecoder는 NRBF 헤더로 지정된 스트림 또는 버퍼가 시작하는지 여부를 확인할 수 있는 두 가지 StartsWithPayloadHeader 메서드를 제공합니다. BinaryFormatter로 (으)로 지속된 페이로드를 마이그레이션할 때는 이러한 메서드를 사용하는 것이 권장됩니다.
- 스토리지에서 읽은 페이로드가 을(를) 포함하는 NrbfDecoder.StartsWithPayloadHeader 페이로드인지 여부를 확인합니다.
- 그렇다면 이를 NrbfDecoder.Decode(으)로 읽고, 새 직렬 변환기를 사용하여 다시 직렬화하고, 스토리지의 데이터를 덮어씁니다.
- 그렇지 않다면, 데이터를 역직렬화하기 위해 새 직렬 변환기를 사용합니다.
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 페이로드 읽기
NRBF 페이로드는 직렬화된 개체와 해당 메타데이터를 나타내는 직렬화 레코드로 구성됩니다. Decode 메서드를 호출해야 전체 페이로드를 읽고 루트 개체를 얻을 수 있습니다.
Decode 메서드는 SerializationRecord 인스턴스를 반환합니다. SerializationRecord serialization 레코드를 나타내는 추상 클래스이며 Id, RecordType및 TypeName세 가지 자체 설명 속성을 제공합니다.
메모
공격자는 주기를 사용하여 페이로드를 만들 수 있습니다(예: 클래스 또는 자체에 대한 참조가 있는 개체 배열). Id IEquatable<T> 구현하는 SerializationRecordId 인스턴스를 반환하며, 무엇보다도 디코딩된 레코드에서 주기를 검색하는 데 사용할 수 있습니다.
SerializationRecord 페이로드에서 읽은 형식 이름(및 TypeName 속성을 통해 노출됨)과 지정된 형식을 비교하는 메서드 TypeNameMatches하나를 노출합니다. 사용자는 형식 전달 및 어셈블리 버전 관리와 관련된 걱정을 할 필요가 없으며, 이는 이 메서드는 어셈블리 이름을 무시하기 때문입니다. 또한 (이러한 정보를 가져오려면 형식 로드가 필요하기 때문에) 멤버 이름 또는 해당 형식을 고려하지 않습니다.
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}`.");
}
}
서로 다른 serialization 레코드 형식이 12가지 이상있습니다. 이 라이브러리는 다음 중 몇 가지만 학습하면 되며, 라이브러리가 추상화 집합을 제공하기 때문입니다.
-
PrimitiveTypeRecord<T>: NRBF에서 기본적으로 지원되는 모든 기본 형식을 설명합니다(
string
,bool
,byte
,sbyte
,char
,short
,ushort
,int
,uint
,long
,ulong
,float
,double
,decimal
,TimeSpan
및DateTime
).-
Value
속성을 통해 값을 노출합니다. -
PrimitiveTypeRecord<T>은(는) PrimitiveTypeRecord 속성을 노출하는 제네릭이 아닌 Value 항목에서 파생됩니다. 하지만 기본 클래스에서는 (값 형식에 boxing을 도입하여) 값이
object
(으)로 반환됩니다.
-
-
ClassRecord: 앞서 언급한 기본 형식 외에 모든
class
및struct
형식을 설명합니다. - ArrayRecord: 다차원 배열 및 가변 배열을 포함하는 모든 배열 레코드를 설명합니다.
-
SZArrayRecord<T>:
T
이(가) 기본 형식이거나 SerializationRecord일 수 있는 인덱싱되지 않은 1차원 배열 레코드를 설명합니다.
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.");
}
Decode 외에도, NrbfDecoder는 DecodeClassRecord을(를) 반환(또는 throw)하는 ClassRecord 메서드를 노출합니다.
ClassRecord
SerializationRecord은(는) ClassRecord(으)로부터 파생되는 가장 중요한 형식으로, 배열 및 고유하게 지원되는 기본 형식 외의 모든 class
및 struct
인스턴스를 나타내는 형식입니다. 모든 멤버 이름 및 값을 읽을 수 있습니다.
멤버가 무엇인지 이해하려면 BinaryFormatter 기능 참조를 참조하세요.
제공하는 API:
- MemberNames 속성은 직렬화된 멤버의 이름을 가져옵니다.
- HasMember 메서드는 페이로드에 지정된 이름의 멤버가 있는지 여부를 확인합니다. 이는 지정된 멤버의 이름을 바꿀 수 있는 버전 관리 시나리오를 처리하도록 설계되었습니다.
- 다음은 제공된 멤버 이름의 기본값을 검색하기 위한 전용 메서드 집합입니다. GetString, GetBoolean, GetByte, GetSByte, GetChar, GetInt16, GetUInt16, GetInt32, GetUInt32, GetInt64, GetUInt64, GetSingle, GetDouble, GetDecimal, GetTimeSpan 및 GetDateTime.
- GetClassRecord [ClassRecord]의 인스턴스를 검색합니다. 주기의 경우 동일한 Id있는 현재 [ClassRecord]의 동일한 인스턴스입니다.
- GetArrayRecord [ArrayRecord]의 인스턴스를 검색합니다.
- GetSerializationRecord은 serialization 레코드를 검색하고, GetRawValue은 serialization 레코드나 원시 기본 값을 검색합니다.
다음과 같은 코드 조각은 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은(는) NRBF 배열 레코드의 핵심 동작을 정의하며 파생 클래스에 대한 기반을 제공합니다. 이는 다음과 같은 두 가지 속성을 제공합니다.
- Rank- 배열의 순위를 가져옵니다.
- Lengths는 모든 차원의 요소 수를 나타내는 정수 버퍼를 획득합니다. GetArray호출하기 전에 제공된 배열 레코드 총 길이를 확인하는 것이 좋습니다.
또한 다음의 한 가지 메서드를 제공합니다. GetArray. 처음으로 사용되는 경우 배열을 할당하고 직렬화된 레코드에 제공된 데이터(고유하게 지원되는 string
또는 int
등의 기본 형식의 경우) 또는 직렬화된 레코드 자체(복합 형식 배열의 경우)로 채웁니다.
GetArray은(는) 예상된 배열의 형식을 지정하는 필수 인수를 필요로 합니다. 예를 들어, 레코드가 정수의 2D 배열이어야 하는 경우 expectedArrayType
와(과) 같이 typeof(int[,])
이(가) 제공되어야 하며, 반환된 배열 또한 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[,]));
형식이 일치하지 않는 경우(예를 들어, 공격자가 두 개의 십억 문자열 배열로 페이로드를 제공하는 경우) 메서드가 InvalidOperationException을(를) throw합니다.
주의
아쉽게도 NRBF 형식을 사용하면 공격자가 많은 수의 null 배열 항목을 쉽게 압축할 수 있습니다. 따라서 GetArray호출하기 전에 항상 배열의 총 길이를 확인하는 것이 좋습니다. 또한 GetArray은 선택적 allowNulls
부울 인수를 허용하며, 해당 값을 false
로 설정하면 null에 대해 예외를 발생시킵니다.
NrbfDecoder는 복합 형식 배열의 경우 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();
NRBF 페이로드 내에서 인덱싱되지 않은 배열을 .NET Framework가 지원했지만 .NET(Core)으로 이 지원이 이식되지 않았습니다. 그러므로 NrbfDecoder는 인덱싱되지 않은 배열의 디코딩을 지원하지 않습니다.
SZArrayRecord
SZArrayRecord<T>
은(는) NRBF 단일 차원, 인덱싱되지 않은 배열 레코드의 핵심 동작을 정의하고 파생 클래스에 대한 기반을 제공합니다.
T
은(는) 기본적으로 지원되는 기본 형식 또는 SerializationRecord 중 하나일 수 있습니다.
이는 Length이(가) 반환하는 GetArray 속성 및 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()
};
.NET