讀取 BinaryFormatter (NRBF) 負載
BinaryFormatter 使用了 .NET Remoting:二進位格式進行 serialization。 這種格式以 MS-NRBF 或簡稱 NRBF 來識別。 從 BinaryFormatter 移轉時的一個常見挑戰是處理持久化到儲存中的負載,因為讀取這些負載之前需要使用 BinaryFormatter。 一些系統需要保留讀取這些負載的能力,以便逐步移轉到新的序列化程式,同時避免直接參考 BinaryFormatter。
再 .NET 9 中,引入了新的 NrbfDecoder 類別,用於解碼 NRBF 負載,而不進行負載的還原序列化。 此 API 可以安全地用於解碼受信任或不受信任的負載,而不會承擔 BinaryFormatter 還原序列化所帶來的風險。 然而,NrbfDecoder 只會將資料解碼為應用程式可以進一步處理的結構。 使用 NrbfDecoder 時,必須小心將資料安全地載入到適當的執行個體中。
您可以將 NrbfDecoder 視為相當於使用 JSON/XML 讀取器,但不進行還原序列化。
NrbfDecoder
NrbfDecoder 是新 System.Formats.Nrbf NuGet 套件的一部分。 它不僅支援 .NET 9,還支援舊版本,如 .NET Standard 2.0 和 .NET Framework。 這種多目標支援使得所有使用支援版本 .NET 的人能夠從 BinaryFormatter 進行移轉。 NrbfDecoder 可以讀取使用 BinaryFormatter 和 FormatterTypeStyle.TypesAlways (預設值) 序列化的負載。
NrbfDecoder 被設計為將所有輸入視為不受信任。 因此,它遵循以下原則:
- 不進行任何類型載入 (以避免如遠端程式碼執行等風險)。
- 不進行任何形式的遞迴 (以避免無界遞迴、堆疊溢位和阻斷服務)。
- 不根據負載中提供的大小進行緩衝區的預先分配,如果負載過小而無法容納承諾的資料 (以避免記憶體耗盡和阻斷服務)。
- 輸入的每個部分只解碼一次 (以執行與建立負載的潛在攻擊者相同的工作量)。
- 使用抗碰撞的隨機化雜湊來儲存由其他記錄參考的記錄 (以避免耗盡由陣列支援字典的記憶體,該陣列的大小由雜湊碼碰撞數量決定)。
- 只有基本類型可以用隱含方式具現化。 陣列可以在需要時進行具現化。 其他類型則永遠不進行具現化。
使用 NrbfDecoder 時,重要的是不要在通用程式碼中重新引入這些功能,因為這樣會使這些防護措施失效。
還原序列化一組封閉的類型
NrbfDecoder 只有在序列化類型的列表是已知且封閉的集合時才有用。 換句話說,您需要事先知道您想要讀取的內容,因為您還需要建立這些類型的執行個體並用從負載中讀取的資料填充它們。 請考慮兩個相反的範例:
- Quartz.NET 中所有可以由函式庫本身持久化的
[Serializable]
類型都是sealed
。 因此,使用者無法建立任何自訂類型,並且負載中只能包含已知類型。 這些類型也提供了公共建構函式,因此可以根據從負載中讀取的資訊重新建立這些類型。 - SettingsPropertyValue 類型會公開類型為
object
的屬性 PropertyValue,該屬性可能會內部使用 BinaryFormatter 來序列化和還原序列化任何儲存在設定檔中的物件。 它可以用來儲存整數、自訂類型、字典或任何其他物件。 因此,若要移轉這個資源庫,就無法避免對 API 進行破壞性變更。
識別 NRBF 負載
NrbfDecoder 提供了兩個 StartsWithPayloadHeader 方法,讓您可以檢查指定的串流或緩衝區是否以 NRBF 標頭開始。 建議在將使用 BinaryFormatter 持久化的負載移轉到其他序列化程式時使用這些方法:
- 使用 NrbfDecoder.StartsWithPayloadHeader 檢查從存放區中讀取的負載是否為 NRBF 負載。
- 如果是這樣,請使用 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 負載由 serialization 記錄組成,這些記錄代表了已序列化的物件及其中繼資料。 要讀取整個負載並獲取根物件,您需要呼叫 Decode 方法。
Decode 方法會傳回 SerializationRecord 執行個體。 SerializationRecord 是一個抽象類別,代表 serialization 記錄並提供三個自我描述屬性:Id、RecordType 和 TypeName。 它公開了一個方法 TypeNameMatches,該方法會比較從負載中讀取的類型名稱 (透過 TypeName 屬性公開) 與指定的類型。 這個方法忽略了組件名稱,因此使用者不需要擔心類型轉發和組件版本控制。 它也不考慮成員名稱或其類型 (因為獲取這些資訊需要載入類型)。
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}`."
}
}
有十多種不同的serialization記錄類型。 這個資源庫提供了一組抽象概念,因此您只需要了解其中幾個:
- PrimitiveTypeRecord<T>:描述 NRBF 原生支援的所有基本類型 (
string
、bool
、byte
、sbyte
、char
、short
、ushort
、int
、uint
、long
、ulong
、float
、double
、decimal
、TimeSpan
和DateTime
)。- 透過
Value
屬性公開值。 - PrimitiveTypeRecord<T> 衍生自非泛型的 PrimitiveTypeRecord,該類別也公開了一個 Value 屬性。 但在基底類別中,該值會以
object
的形式傳回 (這會對值類型進行裝箱)。
- 透過
- ClassRecord:描述了除上述基本類型外的所有
class
和struct
。 - ArrayRecord:描述所有陣列記錄,包括不規則陣列和多維陣列。
- SZArrayRecord<T>:描述單維度、零基索引的陣列記錄,其中
T
可以是基本類型或 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.");
}
除了 Decode,NrbfDecoder 還公開了一個 DecodeClassRecord 方法,該方法會傳回 (或擲回) 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 和 GetArrayRecord 方法,用於檢索指定記錄類型的執行個體。
- 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
ClassInstance = new()
{
Text = rootRecord
.GetClassRecord(nameof(Sample.ClassInstance))!
.GetString(nameof(Sample.Text))
}
};
ArrayRecord
ArrayRecord 定義了 NRBF 陣列記錄的核心行為,並為衍生類別提供了基礎。 它提供兩個屬性:
它還提供一種方法:GetArray。 第一次使用時,它會分配一個陣列,並用序列化記錄中提供的資料進行填寫 (如果是像 string
或 int
這樣的原生支援基本類型),或填寫序列化記錄本身 (如果是複雜類型的陣列)。
GetArray 需要一個強制參數,該參數指定了預期陣列的類型。 例如,如果記錄應該是一個整數的 2D 陣列,則 expectedArrayType
必須作為 typeof(int[,])
提供,並且傳回的陣列也是 int[,]
:
ArrayRecord arrayRecord = (ArrayRecord)NrbfDecoder.Decode(stream);
int[,] array2d = (int[,])arrayRecord.GetArray(typeof(int[,]));
如果發生類型不相符 (例如:攻擊者提供了一個包含二十億個字串的負載),該方法會擲回 InvalidOperationException。
NrbfDecoder 不會載入或建立任何自訂類型,因此在處理複雜類型的陣列時,它會傳回 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 支援 NRBF 負載中的非零索引陣列,但這項支援從未移植到 .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()
};