BinaryFormatter PayloadReader API #102014

Description

@adamsitnik

Background and Motivation

BinaryFormatter is getting removed in .NET 9, but our customers need to be able to read the payloads that:

  • were serialized with BF (using previous .NET versions) and persisted (to disk/db etc)
  • are being generated by software they have no control over (example: 3rd party clients calling an existing web service with public API that allows for BF).

Our primary goal is to allow the users to read BF payloads in a secure manner from untrusted input. The principles:

  • Treating every input as potentially hostile.
  • No type loading of any kind (to avoid remote code execution).
  • No recursion of any kind (to avoid unbound recursion, stack overflow and denial of service).
  • No buffer pre-allocation based on size provided in payload (to avoid running out of memory and denial of service).
  • Using collision-resistant dictionary to store records referenced by other records.
  • Only primitive types can be instantiated in implicit way. Arrays can be instantiated on demand (with a default max size limit). Other types are never instantiated.

We also want to make the APIs easy to use, to avoid the customers using the OOB package with the copy of BinaryFormatter (and remaining vulnerable to various attacks). That is why currently the public API surface is very narrow. We could expose more information, but we don't want to confuse the users or need them to become familiar with BF specification to get simple tasks done. Example: null can be represented using three different serialization records (ObjectNull, ObjectNullMultiple and ObjectNullMultiple256). The public APIs just return null, rather than a record that represents it.

The new APIs need to be shipped in a new OOB package that supports older monikers, as we have first party customers running on Full Framework that are going to use it.

Proposed API

namespaceSystem.Runtime.Serialization.BinaryFormat;publicstaticclassNrbfReader{/// <summary>/// Checks if given buffer starts with <see href="https://learn.microsoft.com/openspecs/windows_protocols/ms-nrbf/a7e578d3-400a-4249-9424-7529d10d1b3c">NRBF payload header</see>./// </summary>/// <param name="bytes">The buffer to inspect.</param>/// <returns><see langword="true" /> if it starts with NRBF payload header; otherwise, <see langword="false" />.</returns>publicstaticboolStartsWithPayloadHeader(byte[]bytes);/// <summary>/// Checks if given stream starts with <see href="https://learn.microsoft.com/openspecs/windows_protocols/ms-nrbf/a7e578d3-400a-4249-9424-7529d10d1b3c">NRBF payload header</see>./// </summary>/// <param name="stream">The stream to inspect. The stream must be both readable and seekable.</param>/// <returns><see langword="true" /> if it starts with NRBF payload header; otherwise, <see langword="false" />.</returns>/// <exception cref="ArgumentNullException"><paramref name="stream" /> is <see langword="null" />.</exception>/// <exception cref="NotSupportedException">The stream does not support reading or seeking.</exception>/// <exception cref="ObjectDisposedException">The stream was closed.</exception>/// <remarks><para>When this method returns, <paramref name="stream" /> will be restored to its original position.</para></remarks>publicstaticboolStartsWithPayloadHeader(Streamstream);/// <summary>/// Reads the provided NRBF payload./// </summary>/// <param name="payload">The NRBF payload.</param>/// <param name="options">Options to control behavior during parsing.</param>/// <param name="leaveOpen">/// <see langword="true" /> to leave <paramref name="payload"/> payload open/// after the reading is finished; otherwise, <see langword="false" />./// </param>/// <returns>A <see cref="SerializationRecord"/> that represents the root object./// It can be either <see cref="PrimitiveTypeRecord{T}"/>,/// a <see cref="ClassRecord"/> or an <see cref="ArrayRecord"/>.</returns>/// <exception cref="ArgumentNullException"><paramref name="payload"/> is <see langword="null" />.</exception>/// <exception cref="ArgumentException"><paramref name="payload"/> does not support reading or is already closed.</exception>/// <exception cref="SerializationException">Reading from <paramref name="payload"/> encounters invalid NRBF data.</exception>/// <exception cref="DecoderFallbackException">Reading from <paramref name="payload"/>/// encounters an invalid UTF8 sequence.</exception>publicstaticSerializationRecordRead(Streampayload,PayloadOptions?options=default,boolleaveOpen=false);/// <param name="recordMap">/// When this method returns, contains a mapping of <see cref="SerializationRecord.ObjectId" /> to the associated serialization record./// This parameter is treated as uninitialized./// </param>publicstaticSerializationRecordRead(Streampayload,outIReadOnlyDictionary<int,SerializationRecord>recordMap,PayloadOptions?options=default,boolleaveOpen=false);/// <summary>/// Reads the provided Binary Format payload that is expected to contain an instance of any class (or struct) that is not an <seealso cref="Array"/> or a primitive type./// </summary>/// <returns>A <seealso cref="ClassRecord"/> that represents the root object.</returns>publicstaticClassRecordReadClassRecord(Streampayload,PayloadOptions?options=default,boolleaveOpen=false);}publicsealedclassPayloadOptions{publicPayloadOptions(){}publicTypeNameParseOptions?TypeNameParseOptions{get;set;}/// <summary>/// Gets or sets a value that indicates whether type name truncation is undone./// </summary>/// <value><see langword="true" /> if truncated type names should be reassembled; otherwise, <see langword="false" />.</value>/// <remarks>/// Example:/// TypeName: "Namespace.TypeName`1[[Namespace.GenericArgName"/// LibraryName: "AssemblyName]]"/// Is combined into "Namespace.TypeName`1[[Namespace.GenericArgName, AssemblyName]]"/// </remarks>publicboolUndoTruncatedTypeNames{get;set;}}/// <summary>/// Abstract class that represents the serialization record./// </summary>/// <remarks>/// Every instance returned to the end user can be either <seealso cref="PrimitiveTypeRecord{T}"/>,/// a <seealso cref="ClassRecord"/> or an <seealso cref="ArrayRecord"/>./// </remarks>publicabstractclassSerializationRecord{internalSerializationRecord();// others can't derive from this type/// <summary>/// Gets the type of the record./// </summary>/// <value>The type of the record.</value>publicabstractRecordTypeRecordType{get;}/// <summary>/// Gets the ID of the record./// </summary>/// <value>The ID of the record.</value>publicabstractintObjectId{get;}/// <summary>/// Compares the type and assembly name read from the payload against the specified type./// </summary>/// <remarks>/// <para>This method takes type forwarding into account.</para>/// <para>This method does NOT take into account member names or their types.</para>/// </remarks>/// <param name="type">The type to compare against.</param>/// <returns><see langword="true" /> if the serialized type and assembly name match provided type; otherwise, <see langword="false" />.</returns>publicvirtualboolIsTypeNameMatching(Typetype);}/// <summary>/// Record type./// </summary>/// <remarks>/// <para>/// The enumeration does not contain all values supported by the <see href="https://learn.microsoft.com/openspecs/windows_protocols/ms-nrbf/954a0657-b901-4813-9398-4ec732fe8b32">/// [MS-NRBF] 2.1.2.1</see>, but only those supported by the <see cref="PayloadReader"/>./// </para>/// </remarks>publicenumRecordType:byte{SerializedStreamHeader,ClassWithId,// SystemClassWithMembers and ClassWithMembers are not supported by design (require type loading) and not includedSystemClassWithMembersAndTypes=4,ClassWithMembersAndTypes,BinaryObjectString,BinaryArray,MemberPrimitiveTyped,MemberReference,ObjectNull,MessageEnd,BinaryLibrary,ObjectNullMultiple256,ObjectNullMultiple,ArraySinglePrimitive,ArraySingleObject,ArraySingleString}/// <summary>/// Represents a record that itself represents the primitive value of <typeparamref name="T"/> type./// </summary>/// <typeparam name="T">The type of the primitive value.</typeparam>/// <remarks>/// <para>/// The NRBF specification considers the following types to be primitive:/// <see cref="string"/>, <see cref="bool"/>, <see cref="byte"/>, <see cref="sbyte"/>/// <see cref="char"/>, <see cref="short"/>, <see cref="ushort"/>,/// <see cref="int"/>, <see cref="uint"/>, <see cref="long"/>, <see cref="ulong"/>,/// <see cref="float"/>, <see cref="double"/>, <see cref="decimal"/>,/// <see cref="DateTime"/> and <see cref="TimeSpan"/>./// </para>/// <para>Other serialization records are represented with <see cref="ClassRecord"/> or <see cref="ArrayRecord"/>.</para>/// </remarks>publicabstractclassPrimitiveTypeRecord<T>:SerializationRecord{privateprotectedPrimitiveTypeRecord(Tvalue);publicTValue{get;}}/// <summary>/// Defines the core behavior for NRBF class records and provides a base for derived classes./// </summary>publicabstractclassClassRecord:SerializationRecord{privateprotectedClassRecord(ClassInfoclassInfo);publicTypeNameTypeName{get;}publicIEnumerable<string>MemberNames{get;}/// <summary>/// Checks if member of given name was present in the payload./// </summary>/// <param name="memberName">The name of the member.</param>/// <returns><see langword="true" /> if it was present, otherwise <see langword="false" />.</returns>/// <remarks>/// <para>/// It's recommended to use this method when dealing with payload that may contain/// different versions of the same type./// </para>/// </remarks>publicboolHasMember(stringmemberName);publicstring?GetString(stringmemberName);publicboolGetBoolean(stringmemberName);publicbyteGetByte(stringmemberName);publicsbyteGetSByte(stringmemberName);publicshortGetInt16(stringmemberName);publicushortGetUInt16(stringmemberName);publiccharGetChar(stringmemberName);publicintGetInt32(stringmemberName);publicuintGetUInt32(stringmemberName);publicfloatGetSingle(stringmemberName);publiclongGetInt64(stringmemberName);publiculongGetUInt64(stringmemberName);publicdoubleGetDouble(stringmemberName);publicdecimalGetDecimal(stringmemberName);publicTimeSpanGetTimeSpan(stringmemberName);publicDateTimeGetDateTime(stringmemberName);/// <summary>/// Retrieves an array for the provided <paramref name="memberName"/>./// </summary>/// <param name="memberName">The name of the field.</param>/// <param name="allowNulls">Specifies whether null values are allowed.</param>/// <returns>The array itself or null.</returns>/// <exception cref="KeyNotFoundException">Member of such name does not exist.</exception>/// <exception cref="InvalidOperationException">Member of such name has value of a different type.</exception>publicT?[]?GetArrayOfPrimitiveType<T>(stringmemberName,boolallowNulls=true);/// <summary>/// Retrieves the <see cref="SerializationRecord" /> of the provided <paramref name="memberName"/>./// </summary>/// <param name="memberName">The name of the field.</param>/// <returns>The serialization record, which can be any of <see cref="PrimitiveTypeRecord{T}"/>,/// <see cref="ClassRecord"/>, <see cref="ArrayRecord"/> or <see langword="null" />./// </returns>/// <exception cref="KeyNotFoundException"><paramref name="memberName" /> does not refer to a known member. You can use <see cref="HasMember(string)"/> to check if given member exists.</exception>/// <exception cref="InvalidOperationException">The specified member is not a <see cref="SerializationRecord"/>, but just a raw primitive value.</exception>publicSerializationRecord?GetSerializationRecord(stringmemberName);/// <summary>/// Retrieves the value of the provided <paramref name="memberName"/>./// </summary>/// <param name="memberName">The name of the member.</param>/// <returns>The value.</returns>/// <exception cref="KeyNotFoundException"><paramref name="memberName" /> does not refer to a known member. You can use <see cref="HasMember(string)"/> to check if given member exists.</exception>/// <exception cref="InvalidOperationException">Member of such name has value of a different type.</exception>publicClassRecord?GetClassRecord(stringmemberName);/// <returns>/// <para>For primitive types like <see cref="int"/>, <see langword="string"/> or <see cref="DateTime"/> returns their value.</para>/// <para>For nulls, returns a null.</para>/// <para>For other types that are not arrays, returns an instance of <see cref="ClassRecord"/>.</para>/// <para>For single-dimensional arrays returns <see cref="ArrayRecord{T}"/> where the generic type is the primitive type or <see cref="ClassRecord"/>.</para>/// <para>For jagged and multi-dimensional arrays, returns an instance of <see cref="ArrayRecord"/>.</para>/// </returns>publicobject?GetRawValue(stringmemberName);}/// <summary>/// Defines the core behavior for NRBF array records and provides a base for derived classes./// </summary>publicabstractclassArrayRecord:SerializationRecord{privateprotectedArrayRecord(ArrayInfoarrayInfo);/// <summary>/// When overridden in a derived class, gets a buffer of integers that represent the number of elements in every dimension./// </summary>/// <value>A buffer of integers that represent the number of elements in every dimension.</value>publicabstractReadOnlySpan<int>Lengths{get;}/// <summary>/// Gets the rank of the array./// </summary>/// <value>The rank of the array.</value>publicintRank{get;}/// <summary>/// Gets the type of the array./// </summary>/// <value>The type of the array.</value>publicBinaryArrayTypeArrayType{get;}/// <summary>/// Gets the name of the array element type./// </summary>/// <value>The name of the array element type.</value>publicabstractTypeNameElementTypeName{get;}/// <summary>/// Allocates an array and fills it with the data provided in the serialized records (in case of primitive types like <see cref="string"/> or <see cref="int"/>) or the serialized records themselves./// </summary>/// <param name="expectedArrayType">Expected array type.</param>/// <param name="allowNulls">/// <see langword="true" /> to permit <see langword="null" /> values within the array;/// otherwise, <see langword="false" />./// </param>/// <returns>An array filled with the data provided in the serialized records.</returns>/// <exception cref="InvalidOperationException"><paramref name="expectedArrayType" /> does not match the data from the payload.</exception>publicArrayGetArray(TypeexpectedArrayType,boolallowNulls=true);}/// <summary>/// Binary array type./// </summary>/// <remarks>/// BinaryArrayType enumeration is described in <see href="https://learn.microsoft.com/openspecs/windows_protocols/ms-nrbf/4dbbf3a8-6bc4-4dfc-aa7e-36a35be6ff58">[MS-NRBF] 2.4.1.1</see>./// </remarks>publicenumBinaryArrayType:byte{/// <summary>/// A single-dimensional array./// </summary>Single=0,/// <summary>/// An array whose elements are arrays. The elements of a jagged array can be of different dimensions and sizes./// </summary>Jagged=1,/// <summary>/// A multi-dimensional rectangular array./// </summary>Rectangular=2,/// <summary>/// A single-dimensional array where the lower bound index is greater than 0./// </summary>SingleOffset=3,/// <summary>/// A jagged array where the lower bound index is greater than 0./// </summary>JaggedOffset=4,/// <summary>/// Multi-dimensional arrays where the lower bound index of at least one of the dimensions is greater than 0./// </summary>RectangularOffset=5}/// <summary>/// Defines the core behavior for NRBF single dimensional, zero-indexed array records and provides a base for derived classes./// </summary>publicabstractclassArrayRecord<T>:ArrayRecord{privateprotectedArrayRecord(ArrayInfoarrayInfo);/// <summary>/// Gets the length of the array./// </summary>/// <value>The length of the array.</value>publicintLength{get;}/// <summary>/// When overridden in a derived class, allocates an array of <typeparamref name="T"/> and fills it with the data provided in the serialized records (in case of primitive types like <see cref="string"/> or <see cref="int"/>) or the serialized records themselves./// </summary>/// <param name="allowNulls">/// <see langword="true" /> to permit <see langword="null" /> values within the array;/// otherwise, <see langword="false" />./// </param>/// <returns>An array filled with the data provided in the serialized records.</returns>publicabstractT?[]GetArray(boolallowNulls=true);}

Usage Examples

The implementation with no dependency to dotnet/runtime can be found here.

Reading a class serialized with BF to a file

ClassRecordrootRecord=NrbfReader.ReadClassRecord(File.OpenRead("peristedPayload.bf"));Sampleoutput=new(){// using the dedicated methods to read primitive valuesInteger=rootRecord.GetInt32(nameof(Sample.Integer)),Text=rootRecord.GetString(nameof(Sample.Text)),// using dedicated method to read an array of bytesArrayOfBytes=rootRecord.GetArrayOfPrimitiveType<byte>(nameof(Sample.ArrayOfBytes)),// using GetClassRecord to read a class recordClassInstance=new(){Text=rootRecord.GetClassRecord(nameof(Sample.ClassInstance))!.GetString(nameof(Sample.Text))}};[Serializable]publicclassSample{publicintInteger;publicstring?Text;publicbyte[]?ArrayOfBytes;publicSample?ClassInstance;}

Checking if Stream contains BF payload

The users need to be able to check if given Stream contains BF data, as they might want to migrate the data on demand to new serialization format:

staticTPseudocode<T>(Streampayload,NewSerializernewSerializer){if(NrbfReader.StartsWithPayloadHeader(payload)){TfromPayload=UseThePayloadReaderToReadTheData<T>(payload);payload.Seek(0,SeekOrigin.Begin);newSerializer.Serialize(payload,fromPayload);payload.Flush();}else{returnnewSerializer.Deserialize<T>(payload)}}

SzArrays

Single dimension, zero-indexed arrays are expected to be the most frequently used arrays.

SerializationRecordrootObject=NrbfReader.Read(File.OpenRead("peristedPayload.bf"));if(rootObjectisArrayRecord<string>arrayOfStrings){string?[]strings=arrayOfStrings.GetArray();}

Other arrays

BF supports:

  • jagged arrays
  • multi-dimensional array
  • non-zero indexed arrays

They are all represented by internal types that derive from ArrayRecord. The users can use the API to instantiate such arrays, but they need to provide the expected array type. By doing that we make this advanced scenario possible and safe (the library is not loading any types, if there is a type mismatch it throws).

publicabstractclassArrayRecord:SerializationRecrd{publicArrayGetArray(TypeexpectedArrayType,boolallowNulls=true);}
ArrayRecordarrayRecord=(ArrayRecord)NrbfReader.Read(File.OpenRead("peristedPayload.bf"));if(arrayRecord.ArrayType==ArrayType.Jagged){int[][][]array=(int[][][])arrayRecord.GetArray(expectedArrayType:typeof(int[][][]));}

For more usages of this API please refer to JaggedArraysTests.cs, RectangularArraysTests.cs and CustomOffsetArrays.cs.

Arrays of non-primitive types

Arrays of non-primitive types are represented as ArrayRecord<ClassRecord> or just ArrayRecord.

ArrayRecord<ClassRecord>rootRecord=(ArrayRecord<ClassRecord>)NrbfReader.Read(File.OpenRead("peristedPayload.bf"));ClassRecord[]classRecords=rootRecord.GetArray(allowNulls:false)!;Sample[]output=classRecords.Select(classRecord =>newSample(){Integer=classRecord.GetInt32(nameof(Sample.Integer)),Text=classRecord.GetString(nameof(Sample.Text))}).ToArray();

Risks

If the new APIs are not easy to use, some of the users might choose the new OOB package with a copy of BF and remain vulnerable to all attacks. This defeats the purpose of our initiative and must be avoided.

Metadata

Metadata

Assignees

Labels

api-approvedAPI was approved in API review, it can be implementedarea-System.Formats.Nrbfbinaryformatter-migrationIssues related to the removal of BinaryFormatter and migrations away from itblockingMarks issues that we want to fast track in order to unblock other important work

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
     blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
    }
    } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
    })();
    (function(){
    try {
    var __m = "github.com";
    var __re = new RegExp('^' + "github\\.com" + '
    
    Skip to content

    BinaryFormatter PayloadReader API #102014

    Description

    @adamsitnik

    Background and Motivation

    BinaryFormatter is getting removed in .NET 9, but our customers need to be able to read the payloads that:

    • were serialized with BF (using previous .NET versions) and persisted (to disk/db etc)
    • are being generated by software they have no control over (example: 3rd party clients calling an existing web service with public API that allows for BF).

    Our primary goal is to allow the users to read BF payloads in a secure manner from untrusted input. The principles:

    • Treating every input as potentially hostile.
    • No type loading of any kind (to avoid remote code execution).
    • No recursion of any kind (to avoid unbound recursion, stack overflow and denial of service).
    • No buffer pre-allocation based on size provided in payload (to avoid running out of memory and denial of service).
    • Using collision-resistant dictionary to store records referenced by other records.
    • Only primitive types can be instantiated in implicit way. Arrays can be instantiated on demand (with a default max size limit). Other types are never instantiated.

    We also want to make the APIs easy to use, to avoid the customers using the OOB package with the copy of BinaryFormatter (and remaining vulnerable to various attacks). That is why currently the public API surface is very narrow. We could expose more information, but we don't want to confuse the users or need them to become familiar with BF specification to get simple tasks done. Example: null can be represented using three different serialization records (ObjectNull, ObjectNullMultiple and ObjectNullMultiple256). The public APIs just return null, rather than a record that represents it.

    The new APIs need to be shipped in a new OOB package that supports older monikers, as we have first party customers running on Full Framework that are going to use it.

    Proposed API

    namespaceSystem.Runtime.Serialization.BinaryFormat;publicstaticclassNrbfReader{/// <summary>/// Checks if given buffer starts with <see href="https://learn.microsoft.com/openspecs/windows_protocols/ms-nrbf/a7e578d3-400a-4249-9424-7529d10d1b3c">NRBF payload header</see>./// </summary>/// <param name="bytes">The buffer to inspect.</param>/// <returns><see langword="true" /> if it starts with NRBF payload header; otherwise, <see langword="false" />.</returns>publicstaticboolStartsWithPayloadHeader(byte[]bytes);/// <summary>/// Checks if given stream starts with <see href="https://learn.microsoft.com/openspecs/windows_protocols/ms-nrbf/a7e578d3-400a-4249-9424-7529d10d1b3c">NRBF payload header</see>./// </summary>/// <param name="stream">The stream to inspect. The stream must be both readable and seekable.</param>/// <returns><see langword="true" /> if it starts with NRBF payload header; otherwise, <see langword="false" />.</returns>/// <exception cref="ArgumentNullException"><paramref name="stream" /> is <see langword="null" />.</exception>/// <exception cref="NotSupportedException">The stream does not support reading or seeking.</exception>/// <exception cref="ObjectDisposedException">The stream was closed.</exception>/// <remarks><para>When this method returns, <paramref name="stream" /> will be restored to its original position.</para></remarks>publicstaticboolStartsWithPayloadHeader(Streamstream);/// <summary>/// Reads the provided NRBF payload./// </summary>/// <param name="payload">The NRBF payload.</param>/// <param name="options">Options to control behavior during parsing.</param>/// <param name="leaveOpen">/// <see langword="true" /> to leave <paramref name="payload"/> payload open/// after the reading is finished; otherwise, <see langword="false" />./// </param>/// <returns>A <see cref="SerializationRecord"/> that represents the root object./// It can be either <see cref="PrimitiveTypeRecord{T}"/>,/// a <see cref="ClassRecord"/> or an <see cref="ArrayRecord"/>.</returns>/// <exception cref="ArgumentNullException"><paramref name="payload"/> is <see langword="null" />.</exception>/// <exception cref="ArgumentException"><paramref name="payload"/> does not support reading or is already closed.</exception>/// <exception cref="SerializationException">Reading from <paramref name="payload"/> encounters invalid NRBF data.</exception>/// <exception cref="DecoderFallbackException">Reading from <paramref name="payload"/>/// encounters an invalid UTF8 sequence.</exception>publicstaticSerializationRecordRead(Streampayload,PayloadOptions?options=default,boolleaveOpen=false);/// <param name="recordMap">/// When this method returns, contains a mapping of <see cref="SerializationRecord.ObjectId" /> to the associated serialization record./// This parameter is treated as uninitialized./// </param>publicstaticSerializationRecordRead(Streampayload,outIReadOnlyDictionary<int,SerializationRecord>recordMap,PayloadOptions?options=default,boolleaveOpen=false);/// <summary>/// Reads the provided Binary Format payload that is expected to contain an instance of any class (or struct) that is not an <seealso cref="Array"/> or a primitive type./// </summary>/// <returns>A <seealso cref="ClassRecord"/> that represents the root object.</returns>publicstaticClassRecordReadClassRecord(Streampayload,PayloadOptions?options=default,boolleaveOpen=false);}publicsealedclassPayloadOptions{publicPayloadOptions(){}publicTypeNameParseOptions?TypeNameParseOptions{get;set;}/// <summary>/// Gets or sets a value that indicates whether type name truncation is undone./// </summary>/// <value><see langword="true" /> if truncated type names should be reassembled; otherwise, <see langword="false" />.</value>/// <remarks>/// Example:/// TypeName: "Namespace.TypeName`1[[Namespace.GenericArgName"/// LibraryName: "AssemblyName]]"/// Is combined into "Namespace.TypeName`1[[Namespace.GenericArgName, AssemblyName]]"/// </remarks>publicboolUndoTruncatedTypeNames{get;set;}}/// <summary>/// Abstract class that represents the serialization record./// </summary>/// <remarks>/// Every instance returned to the end user can be either <seealso cref="PrimitiveTypeRecord{T}"/>,/// a <seealso cref="ClassRecord"/> or an <seealso cref="ArrayRecord"/>./// </remarks>publicabstractclassSerializationRecord{internalSerializationRecord();// others can't derive from this type/// <summary>/// Gets the type of the record./// </summary>/// <value>The type of the record.</value>publicabstractRecordTypeRecordType{get;}/// <summary>/// Gets the ID of the record./// </summary>/// <value>The ID of the record.</value>publicabstractintObjectId{get;}/// <summary>/// Compares the type and assembly name read from the payload against the specified type./// </summary>/// <remarks>/// <para>This method takes type forwarding into account.</para>/// <para>This method does NOT take into account member names or their types.</para>/// </remarks>/// <param name="type">The type to compare against.</param>/// <returns><see langword="true" /> if the serialized type and assembly name match provided type; otherwise, <see langword="false" />.</returns>publicvirtualboolIsTypeNameMatching(Typetype);}/// <summary>/// Record type./// </summary>/// <remarks>/// <para>/// The enumeration does not contain all values supported by the <see href="https://learn.microsoft.com/openspecs/windows_protocols/ms-nrbf/954a0657-b901-4813-9398-4ec732fe8b32">/// [MS-NRBF] 2.1.2.1</see>, but only those supported by the <see cref="PayloadReader"/>./// </para>/// </remarks>publicenumRecordType:byte{SerializedStreamHeader,ClassWithId,// SystemClassWithMembers and ClassWithMembers are not supported by design (require type loading) and not includedSystemClassWithMembersAndTypes=4,ClassWithMembersAndTypes,BinaryObjectString,BinaryArray,MemberPrimitiveTyped,MemberReference,ObjectNull,MessageEnd,BinaryLibrary,ObjectNullMultiple256,ObjectNullMultiple,ArraySinglePrimitive,ArraySingleObject,ArraySingleString}/// <summary>/// Represents a record that itself represents the primitive value of <typeparamref name="T"/> type./// </summary>/// <typeparam name="T">The type of the primitive value.</typeparam>/// <remarks>/// <para>/// The NRBF specification considers the following types to be primitive:/// <see cref="string"/>, <see cref="bool"/>, <see cref="byte"/>, <see cref="sbyte"/>/// <see cref="char"/>, <see cref="short"/>, <see cref="ushort"/>,/// <see cref="int"/>, <see cref="uint"/>, <see cref="long"/>, <see cref="ulong"/>,/// <see cref="float"/>, <see cref="double"/>, <see cref="decimal"/>,/// <see cref="DateTime"/> and <see cref="TimeSpan"/>./// </para>/// <para>Other serialization records are represented with <see cref="ClassRecord"/> or <see cref="ArrayRecord"/>.</para>/// </remarks>publicabstractclassPrimitiveTypeRecord<T>:SerializationRecord{privateprotectedPrimitiveTypeRecord(Tvalue);publicTValue{get;}}/// <summary>/// Defines the core behavior for NRBF class records and provides a base for derived classes./// </summary>publicabstractclassClassRecord:SerializationRecord{privateprotectedClassRecord(ClassInfoclassInfo);publicTypeNameTypeName{get;}publicIEnumerable<string>MemberNames{get;}/// <summary>/// Checks if member of given name was present in the payload./// </summary>/// <param name="memberName">The name of the member.</param>/// <returns><see langword="true" /> if it was present, otherwise <see langword="false" />.</returns>/// <remarks>/// <para>/// It's recommended to use this method when dealing with payload that may contain/// different versions of the same type./// </para>/// </remarks>publicboolHasMember(stringmemberName);publicstring?GetString(stringmemberName);publicboolGetBoolean(stringmemberName);publicbyteGetByte(stringmemberName);publicsbyteGetSByte(stringmemberName);publicshortGetInt16(stringmemberName);publicushortGetUInt16(stringmemberName);publiccharGetChar(stringmemberName);publicintGetInt32(stringmemberName);publicuintGetUInt32(stringmemberName);publicfloatGetSingle(stringmemberName);publiclongGetInt64(stringmemberName);publiculongGetUInt64(stringmemberName);publicdoubleGetDouble(stringmemberName);publicdecimalGetDecimal(stringmemberName);publicTimeSpanGetTimeSpan(stringmemberName);publicDateTimeGetDateTime(stringmemberName);/// <summary>/// Retrieves an array for the provided <paramref name="memberName"/>./// </summary>/// <param name="memberName">The name of the field.</param>/// <param name="allowNulls">Specifies whether null values are allowed.</param>/// <returns>The array itself or null.</returns>/// <exception cref="KeyNotFoundException">Member of such name does not exist.</exception>/// <exception cref="InvalidOperationException">Member of such name has value of a different type.</exception>publicT?[]?GetArrayOfPrimitiveType<T>(stringmemberName,boolallowNulls=true);/// <summary>/// Retrieves the <see cref="SerializationRecord" /> of the provided <paramref name="memberName"/>./// </summary>/// <param name="memberName">The name of the field.</param>/// <returns>The serialization record, which can be any of <see cref="PrimitiveTypeRecord{T}"/>,/// <see cref="ClassRecord"/>, <see cref="ArrayRecord"/> or <see langword="null" />./// </returns>/// <exception cref="KeyNotFoundException"><paramref name="memberName" /> does not refer to a known member. You can use <see cref="HasMember(string)"/> to check if given member exists.</exception>/// <exception cref="InvalidOperationException">The specified member is not a <see cref="SerializationRecord"/>, but just a raw primitive value.</exception>publicSerializationRecord?GetSerializationRecord(stringmemberName);/// <summary>/// Retrieves the value of the provided <paramref name="memberName"/>./// </summary>/// <param name="memberName">The name of the member.</param>/// <returns>The value.</returns>/// <exception cref="KeyNotFoundException"><paramref name="memberName" /> does not refer to a known member. You can use <see cref="HasMember(string)"/> to check if given member exists.</exception>/// <exception cref="InvalidOperationException">Member of such name has value of a different type.</exception>publicClassRecord?GetClassRecord(stringmemberName);/// <returns>/// <para>For primitive types like <see cref="int"/>, <see langword="string"/> or <see cref="DateTime"/> returns their value.</para>/// <para>For nulls, returns a null.</para>/// <para>For other types that are not arrays, returns an instance of <see cref="ClassRecord"/>.</para>/// <para>For single-dimensional arrays returns <see cref="ArrayRecord{T}"/> where the generic type is the primitive type or <see cref="ClassRecord"/>.</para>/// <para>For jagged and multi-dimensional arrays, returns an instance of <see cref="ArrayRecord"/>.</para>/// </returns>publicobject?GetRawValue(stringmemberName);}/// <summary>/// Defines the core behavior for NRBF array records and provides a base for derived classes./// </summary>publicabstractclassArrayRecord:SerializationRecord{privateprotectedArrayRecord(ArrayInfoarrayInfo);/// <summary>/// When overridden in a derived class, gets a buffer of integers that represent the number of elements in every dimension./// </summary>/// <value>A buffer of integers that represent the number of elements in every dimension.</value>publicabstractReadOnlySpan<int>Lengths{get;}/// <summary>/// Gets the rank of the array./// </summary>/// <value>The rank of the array.</value>publicintRank{get;}/// <summary>/// Gets the type of the array./// </summary>/// <value>The type of the array.</value>publicBinaryArrayTypeArrayType{get;}/// <summary>/// Gets the name of the array element type./// </summary>/// <value>The name of the array element type.</value>publicabstractTypeNameElementTypeName{get;}/// <summary>/// Allocates an array and fills it with the data provided in the serialized records (in case of primitive types like <see cref="string"/> or <see cref="int"/>) or the serialized records themselves./// </summary>/// <param name="expectedArrayType">Expected array type.</param>/// <param name="allowNulls">/// <see langword="true" /> to permit <see langword="null" /> values within the array;/// otherwise, <see langword="false" />./// </param>/// <returns>An array filled with the data provided in the serialized records.</returns>/// <exception cref="InvalidOperationException"><paramref name="expectedArrayType" /> does not match the data from the payload.</exception>publicArrayGetArray(TypeexpectedArrayType,boolallowNulls=true);}/// <summary>/// Binary array type./// </summary>/// <remarks>/// BinaryArrayType enumeration is described in <see href="https://learn.microsoft.com/openspecs/windows_protocols/ms-nrbf/4dbbf3a8-6bc4-4dfc-aa7e-36a35be6ff58">[MS-NRBF] 2.4.1.1</see>./// </remarks>publicenumBinaryArrayType:byte{/// <summary>/// A single-dimensional array./// </summary>Single=0,/// <summary>/// An array whose elements are arrays. The elements of a jagged array can be of different dimensions and sizes./// </summary>Jagged=1,/// <summary>/// A multi-dimensional rectangular array./// </summary>Rectangular=2,/// <summary>/// A single-dimensional array where the lower bound index is greater than 0./// </summary>SingleOffset=3,/// <summary>/// A jagged array where the lower bound index is greater than 0./// </summary>JaggedOffset=4,/// <summary>/// Multi-dimensional arrays where the lower bound index of at least one of the dimensions is greater than 0./// </summary>RectangularOffset=5}/// <summary>/// Defines the core behavior for NRBF single dimensional, zero-indexed array records and provides a base for derived classes./// </summary>publicabstractclassArrayRecord<T>:ArrayRecord{privateprotectedArrayRecord(ArrayInfoarrayInfo);/// <summary>/// Gets the length of the array./// </summary>/// <value>The length of the array.</value>publicintLength{get;}/// <summary>/// When overridden in a derived class, allocates an array of <typeparamref name="T"/> and fills it with the data provided in the serialized records (in case of primitive types like <see cref="string"/> or <see cref="int"/>) or the serialized records themselves./// </summary>/// <param name="allowNulls">/// <see langword="true" /> to permit <see langword="null" /> values within the array;/// otherwise, <see langword="false" />./// </param>/// <returns>An array filled with the data provided in the serialized records.</returns>publicabstractT?[]GetArray(boolallowNulls=true);}

    Usage Examples

    The implementation with no dependency to dotnet/runtime can be found here.

    Reading a class serialized with BF to a file

    ClassRecordrootRecord=NrbfReader.ReadClassRecord(File.OpenRead("peristedPayload.bf"));Sampleoutput=new(){// using the dedicated methods to read primitive valuesInteger=rootRecord.GetInt32(nameof(Sample.Integer)),Text=rootRecord.GetString(nameof(Sample.Text)),// using dedicated method to read an array of bytesArrayOfBytes=rootRecord.GetArrayOfPrimitiveType<byte>(nameof(Sample.ArrayOfBytes)),// using GetClassRecord to read a class recordClassInstance=new(){Text=rootRecord.GetClassRecord(nameof(Sample.ClassInstance))!.GetString(nameof(Sample.Text))}};[Serializable]publicclassSample{publicintInteger;publicstring?Text;publicbyte[]?ArrayOfBytes;publicSample?ClassInstance;}

    Checking if Stream contains BF payload

    The users need to be able to check if given Stream contains BF data, as they might want to migrate the data on demand to new serialization format:

    staticTPseudocode<T>(Streampayload,NewSerializernewSerializer){if(NrbfReader.StartsWithPayloadHeader(payload)){TfromPayload=UseThePayloadReaderToReadTheData<T>(payload);payload.Seek(0,SeekOrigin.Begin);newSerializer.Serialize(payload,fromPayload);payload.Flush();}else{returnnewSerializer.Deserialize<T>(payload)}}

    SzArrays

    Single dimension, zero-indexed arrays are expected to be the most frequently used arrays.

    SerializationRecordrootObject=NrbfReader.Read(File.OpenRead("peristedPayload.bf"));if(rootObjectisArrayRecord<string>arrayOfStrings){string?[]strings=arrayOfStrings.GetArray();}

    Other arrays

    BF supports:

    • jagged arrays
    • multi-dimensional array
    • non-zero indexed arrays

    They are all represented by internal types that derive from ArrayRecord. The users can use the API to instantiate such arrays, but they need to provide the expected array type. By doing that we make this advanced scenario possible and safe (the library is not loading any types, if there is a type mismatch it throws).

    publicabstractclassArrayRecord:SerializationRecrd{publicArrayGetArray(TypeexpectedArrayType,boolallowNulls=true);}
    ArrayRecordarrayRecord=(ArrayRecord)NrbfReader.Read(File.OpenRead("peristedPayload.bf"));if(arrayRecord.ArrayType==ArrayType.Jagged){int[][][]array=(int[][][])arrayRecord.GetArray(expectedArrayType:typeof(int[][][]));}

    For more usages of this API please refer to JaggedArraysTests.cs, RectangularArraysTests.cs and CustomOffsetArrays.cs.

    Arrays of non-primitive types

    Arrays of non-primitive types are represented as ArrayRecord<ClassRecord> or just ArrayRecord.

    ArrayRecord<ClassRecord>rootRecord=(ArrayRecord<ClassRecord>)NrbfReader.Read(File.OpenRead("peristedPayload.bf"));ClassRecord[]classRecords=rootRecord.GetArray(allowNulls:false)!;Sample[]output=classRecords.Select(classRecord =>newSample(){Integer=classRecord.GetInt32(nameof(Sample.Integer)),Text=classRecord.GetString(nameof(Sample.Text))}).ToArray();

    Risks

    If the new APIs are not easy to use, some of the users might choose the new OOB package with a copy of BF and remain vulnerable to all attacks. This defeats the purpose of our initiative and must be avoided.

    Metadata

    Metadata

    Assignees

    Labels

    api-approvedAPI was approved in API review, it can be implementedarea-System.Formats.Nrbfbinaryformatter-migrationIssues related to the removal of BinaryFormatter and migrations away from itblockingMarks issues that we want to fast track in order to unblock other important work

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
      Skip to content

      BinaryFormatter PayloadReader API #102014

      Description

      @adamsitnik

      Background and Motivation

      BinaryFormatter is getting removed in .NET 9, but our customers need to be able to read the payloads that:

      • were serialized with BF (using previous .NET versions) and persisted (to disk/db etc)
      • are being generated by software they have no control over (example: 3rd party clients calling an existing web service with public API that allows for BF).

      Our primary goal is to allow the users to read BF payloads in a secure manner from untrusted input. The principles:

      • Treating every input as potentially hostile.
      • No type loading of any kind (to avoid remote code execution).
      • No recursion of any kind (to avoid unbound recursion, stack overflow and denial of service).
      • No buffer pre-allocation based on size provided in payload (to avoid running out of memory and denial of service).
      • Using collision-resistant dictionary to store records referenced by other records.
      • Only primitive types can be instantiated in implicit way. Arrays can be instantiated on demand (with a default max size limit). Other types are never instantiated.

      We also want to make the APIs easy to use, to avoid the customers using the OOB package with the copy of BinaryFormatter (and remaining vulnerable to various attacks). That is why currently the public API surface is very narrow. We could expose more information, but we don't want to confuse the users or need them to become familiar with BF specification to get simple tasks done. Example: null can be represented using three different serialization records (ObjectNull, ObjectNullMultiple and ObjectNullMultiple256). The public APIs just return null, rather than a record that represents it.

      The new APIs need to be shipped in a new OOB package that supports older monikers, as we have first party customers running on Full Framework that are going to use it.

      Proposed API

      namespaceSystem.Runtime.Serialization.BinaryFormat;publicstaticclassNrbfReader{/// <summary>/// Checks if given buffer starts with <see href="https://learn.microsoft.com/openspecs/windows_protocols/ms-nrbf/a7e578d3-400a-4249-9424-7529d10d1b3c">NRBF payload header</see>./// </summary>/// <param name="bytes">The buffer to inspect.</param>/// <returns><see langword="true" /> if it starts with NRBF payload header; otherwise, <see langword="false" />.</returns>publicstaticboolStartsWithPayloadHeader(byte[]bytes);/// <summary>/// Checks if given stream starts with <see href="https://learn.microsoft.com/openspecs/windows_protocols/ms-nrbf/a7e578d3-400a-4249-9424-7529d10d1b3c">NRBF payload header</see>./// </summary>/// <param name="stream">The stream to inspect. The stream must be both readable and seekable.</param>/// <returns><see langword="true" /> if it starts with NRBF payload header; otherwise, <see langword="false" />.</returns>/// <exception cref="ArgumentNullException"><paramref name="stream" /> is <see langword="null" />.</exception>/// <exception cref="NotSupportedException">The stream does not support reading or seeking.</exception>/// <exception cref="ObjectDisposedException">The stream was closed.</exception>/// <remarks><para>When this method returns, <paramref name="stream" /> will be restored to its original position.</para></remarks>publicstaticboolStartsWithPayloadHeader(Streamstream);/// <summary>/// Reads the provided NRBF payload./// </summary>/// <param name="payload">The NRBF payload.</param>/// <param name="options">Options to control behavior during parsing.</param>/// <param name="leaveOpen">/// <see langword="true" /> to leave <paramref name="payload"/> payload open/// after the reading is finished; otherwise, <see langword="false" />./// </param>/// <returns>A <see cref="SerializationRecord"/> that represents the root object./// It can be either <see cref="PrimitiveTypeRecord{T}"/>,/// a <see cref="ClassRecord"/> or an <see cref="ArrayRecord"/>.</returns>/// <exception cref="ArgumentNullException"><paramref name="payload"/> is <see langword="null" />.</exception>/// <exception cref="ArgumentException"><paramref name="payload"/> does not support reading or is already closed.</exception>/// <exception cref="SerializationException">Reading from <paramref name="payload"/> encounters invalid NRBF data.</exception>/// <exception cref="DecoderFallbackException">Reading from <paramref name="payload"/>/// encounters an invalid UTF8 sequence.</exception>publicstaticSerializationRecordRead(Streampayload,PayloadOptions?options=default,boolleaveOpen=false);/// <param name="recordMap">/// When this method returns, contains a mapping of <see cref="SerializationRecord.ObjectId" /> to the associated serialization record./// This parameter is treated as uninitialized./// </param>publicstaticSerializationRecordRead(Streampayload,outIReadOnlyDictionary<int,SerializationRecord>recordMap,PayloadOptions?options=default,boolleaveOpen=false);/// <summary>/// Reads the provided Binary Format payload that is expected to contain an instance of any class (or struct) that is not an <seealso cref="Array"/> or a primitive type./// </summary>/// <returns>A <seealso cref="ClassRecord"/> that represents the root object.</returns>publicstaticClassRecordReadClassRecord(Streampayload,PayloadOptions?options=default,boolleaveOpen=false);}publicsealedclassPayloadOptions{publicPayloadOptions(){}publicTypeNameParseOptions?TypeNameParseOptions{get;set;}/// <summary>/// Gets or sets a value that indicates whether type name truncation is undone./// </summary>/// <value><see langword="true" /> if truncated type names should be reassembled; otherwise, <see langword="false" />.</value>/// <remarks>/// Example:/// TypeName: "Namespace.TypeName`1[[Namespace.GenericArgName"/// LibraryName: "AssemblyName]]"/// Is combined into "Namespace.TypeName`1[[Namespace.GenericArgName, AssemblyName]]"/// </remarks>publicboolUndoTruncatedTypeNames{get;set;}}/// <summary>/// Abstract class that represents the serialization record./// </summary>/// <remarks>/// Every instance returned to the end user can be either <seealso cref="PrimitiveTypeRecord{T}"/>,/// a <seealso cref="ClassRecord"/> or an <seealso cref="ArrayRecord"/>./// </remarks>publicabstractclassSerializationRecord{internalSerializationRecord();// others can't derive from this type/// <summary>/// Gets the type of the record./// </summary>/// <value>The type of the record.</value>publicabstractRecordTypeRecordType{get;}/// <summary>/// Gets the ID of the record./// </summary>/// <value>The ID of the record.</value>publicabstractintObjectId{get;}/// <summary>/// Compares the type and assembly name read from the payload against the specified type./// </summary>/// <remarks>/// <para>This method takes type forwarding into account.</para>/// <para>This method does NOT take into account member names or their types.</para>/// </remarks>/// <param name="type">The type to compare against.</param>/// <returns><see langword="true" /> if the serialized type and assembly name match provided type; otherwise, <see langword="false" />.</returns>publicvirtualboolIsTypeNameMatching(Typetype);}/// <summary>/// Record type./// </summary>/// <remarks>/// <para>/// The enumeration does not contain all values supported by the <see href="https://learn.microsoft.com/openspecs/windows_protocols/ms-nrbf/954a0657-b901-4813-9398-4ec732fe8b32">/// [MS-NRBF] 2.1.2.1</see>, but only those supported by the <see cref="PayloadReader"/>./// </para>/// </remarks>publicenumRecordType:byte{SerializedStreamHeader,ClassWithId,// SystemClassWithMembers and ClassWithMembers are not supported by design (require type loading) and not includedSystemClassWithMembersAndTypes=4,ClassWithMembersAndTypes,BinaryObjectString,BinaryArray,MemberPrimitiveTyped,MemberReference,ObjectNull,MessageEnd,BinaryLibrary,ObjectNullMultiple256,ObjectNullMultiple,ArraySinglePrimitive,ArraySingleObject,ArraySingleString}/// <summary>/// Represents a record that itself represents the primitive value of <typeparamref name="T"/> type./// </summary>/// <typeparam name="T">The type of the primitive value.</typeparam>/// <remarks>/// <para>/// The NRBF specification considers the following types to be primitive:/// <see cref="string"/>, <see cref="bool"/>, <see cref="byte"/>, <see cref="sbyte"/>/// <see cref="char"/>, <see cref="short"/>, <see cref="ushort"/>,/// <see cref="int"/>, <see cref="uint"/>, <see cref="long"/>, <see cref="ulong"/>,/// <see cref="float"/>, <see cref="double"/>, <see cref="decimal"/>,/// <see cref="DateTime"/> and <see cref="TimeSpan"/>./// </para>/// <para>Other serialization records are represented with <see cref="ClassRecord"/> or <see cref="ArrayRecord"/>.</para>/// </remarks>publicabstractclassPrimitiveTypeRecord<T>:SerializationRecord{privateprotectedPrimitiveTypeRecord(Tvalue);publicTValue{get;}}/// <summary>/// Defines the core behavior for NRBF class records and provides a base for derived classes./// </summary>publicabstractclassClassRecord:SerializationRecord{privateprotectedClassRecord(ClassInfoclassInfo);publicTypeNameTypeName{get;}publicIEnumerable<string>MemberNames{get;}/// <summary>/// Checks if member of given name was present in the payload./// </summary>/// <param name="memberName">The name of the member.</param>/// <returns><see langword="true" /> if it was present, otherwise <see langword="false" />.</returns>/// <remarks>/// <para>/// It's recommended to use this method when dealing with payload that may contain/// different versions of the same type./// </para>/// </remarks>publicboolHasMember(stringmemberName);publicstring?GetString(stringmemberName);publicboolGetBoolean(stringmemberName);publicbyteGetByte(stringmemberName);publicsbyteGetSByte(stringmemberName);publicshortGetInt16(stringmemberName);publicushortGetUInt16(stringmemberName);publiccharGetChar(stringmemberName);publicintGetInt32(stringmemberName);publicuintGetUInt32(stringmemberName);publicfloatGetSingle(stringmemberName);publiclongGetInt64(stringmemberName);publiculongGetUInt64(stringmemberName);publicdoubleGetDouble(stringmemberName);publicdecimalGetDecimal(stringmemberName);publicTimeSpanGetTimeSpan(stringmemberName);publicDateTimeGetDateTime(stringmemberName);/// <summary>/// Retrieves an array for the provided <paramref name="memberName"/>./// </summary>/// <param name="memberName">The name of the field.</param>/// <param name="allowNulls">Specifies whether null values are allowed.</param>/// <returns>The array itself or null.</returns>/// <exception cref="KeyNotFoundException">Member of such name does not exist.</exception>/// <exception cref="InvalidOperationException">Member of such name has value of a different type.</exception>publicT?[]?GetArrayOfPrimitiveType<T>(stringmemberName,boolallowNulls=true);/// <summary>/// Retrieves the <see cref="SerializationRecord" /> of the provided <paramref name="memberName"/>./// </summary>/// <param name="memberName">The name of the field.</param>/// <returns>The serialization record, which can be any of <see cref="PrimitiveTypeRecord{T}"/>,/// <see cref="ClassRecord"/>, <see cref="ArrayRecord"/> or <see langword="null" />./// </returns>/// <exception cref="KeyNotFoundException"><paramref name="memberName" /> does not refer to a known member. You can use <see cref="HasMember(string)"/> to check if given member exists.</exception>/// <exception cref="InvalidOperationException">The specified member is not a <see cref="SerializationRecord"/>, but just a raw primitive value.</exception>publicSerializationRecord?GetSerializationRecord(stringmemberName);/// <summary>/// Retrieves the value of the provided <paramref name="memberName"/>./// </summary>/// <param name="memberName">The name of the member.</param>/// <returns>The value.</returns>/// <exception cref="KeyNotFoundException"><paramref name="memberName" /> does not refer to a known member. You can use <see cref="HasMember(string)"/> to check if given member exists.</exception>/// <exception cref="InvalidOperationException">Member of such name has value of a different type.</exception>publicClassRecord?GetClassRecord(stringmemberName);/// <returns>/// <para>For primitive types like <see cref="int"/>, <see langword="string"/> or <see cref="DateTime"/> returns their value.</para>/// <para>For nulls, returns a null.</para>/// <para>For other types that are not arrays, returns an instance of <see cref="ClassRecord"/>.</para>/// <para>For single-dimensional arrays returns <see cref="ArrayRecord{T}"/> where the generic type is the primitive type or <see cref="ClassRecord"/>.</para>/// <para>For jagged and multi-dimensional arrays, returns an instance of <see cref="ArrayRecord"/>.</para>/// </returns>publicobject?GetRawValue(stringmemberName);}/// <summary>/// Defines the core behavior for NRBF array records and provides a base for derived classes./// </summary>publicabstractclassArrayRecord:SerializationRecord{privateprotectedArrayRecord(ArrayInfoarrayInfo);/// <summary>/// When overridden in a derived class, gets a buffer of integers that represent the number of elements in every dimension./// </summary>/// <value>A buffer of integers that represent the number of elements in every dimension.</value>publicabstractReadOnlySpan<int>Lengths{get;}/// <summary>/// Gets the rank of the array./// </summary>/// <value>The rank of the array.</value>publicintRank{get;}/// <summary>/// Gets the type of the array./// </summary>/// <value>The type of the array.</value>publicBinaryArrayTypeArrayType{get;}/// <summary>/// Gets the name of the array element type./// </summary>/// <value>The name of the array element type.</value>publicabstractTypeNameElementTypeName{get;}/// <summary>/// Allocates an array and fills it with the data provided in the serialized records (in case of primitive types like <see cref="string"/> or <see cref="int"/>) or the serialized records themselves./// </summary>/// <param name="expectedArrayType">Expected array type.</param>/// <param name="allowNulls">/// <see langword="true" /> to permit <see langword="null" /> values within the array;/// otherwise, <see langword="false" />./// </param>/// <returns>An array filled with the data provided in the serialized records.</returns>/// <exception cref="InvalidOperationException"><paramref name="expectedArrayType" /> does not match the data from the payload.</exception>publicArrayGetArray(TypeexpectedArrayType,boolallowNulls=true);}/// <summary>/// Binary array type./// </summary>/// <remarks>/// BinaryArrayType enumeration is described in <see href="https://learn.microsoft.com/openspecs/windows_protocols/ms-nrbf/4dbbf3a8-6bc4-4dfc-aa7e-36a35be6ff58">[MS-NRBF] 2.4.1.1</see>./// </remarks>publicenumBinaryArrayType:byte{/// <summary>/// A single-dimensional array./// </summary>Single=0,/// <summary>/// An array whose elements are arrays. The elements of a jagged array can be of different dimensions and sizes./// </summary>Jagged=1,/// <summary>/// A multi-dimensional rectangular array./// </summary>Rectangular=2,/// <summary>/// A single-dimensional array where the lower bound index is greater than 0./// </summary>SingleOffset=3,/// <summary>/// A jagged array where the lower bound index is greater than 0./// </summary>JaggedOffset=4,/// <summary>/// Multi-dimensional arrays where the lower bound index of at least one of the dimensions is greater than 0./// </summary>RectangularOffset=5}/// <summary>/// Defines the core behavior for NRBF single dimensional, zero-indexed array records and provides a base for derived classes./// </summary>publicabstractclassArrayRecord<T>:ArrayRecord{privateprotectedArrayRecord(ArrayInfoarrayInfo);/// <summary>/// Gets the length of the array./// </summary>/// <value>The length of the array.</value>publicintLength{get;}/// <summary>/// When overridden in a derived class, allocates an array of <typeparamref name="T"/> and fills it with the data provided in the serialized records (in case of primitive types like <see cref="string"/> or <see cref="int"/>) or the serialized records themselves./// </summary>/// <param name="allowNulls">/// <see langword="true" /> to permit <see langword="null" /> values within the array;/// otherwise, <see langword="false" />./// </param>/// <returns>An array filled with the data provided in the serialized records.</returns>publicabstractT?[]GetArray(boolallowNulls=true);}

      Usage Examples

      The implementation with no dependency to dotnet/runtime can be found here.

      Reading a class serialized with BF to a file

      ClassRecordrootRecord=NrbfReader.ReadClassRecord(File.OpenRead("peristedPayload.bf"));Sampleoutput=new(){// using the dedicated methods to read primitive valuesInteger=rootRecord.GetInt32(nameof(Sample.Integer)),Text=rootRecord.GetString(nameof(Sample.Text)),// using dedicated method to read an array of bytesArrayOfBytes=rootRecord.GetArrayOfPrimitiveType<byte>(nameof(Sample.ArrayOfBytes)),// using GetClassRecord to read a class recordClassInstance=new(){Text=rootRecord.GetClassRecord(nameof(Sample.ClassInstance))!.GetString(nameof(Sample.Text))}};[Serializable]publicclassSample{publicintInteger;publicstring?Text;publicbyte[]?ArrayOfBytes;publicSample?ClassInstance;}

      Checking if Stream contains BF payload

      The users need to be able to check if given Stream contains BF data, as they might want to migrate the data on demand to new serialization format:

      staticTPseudocode<T>(Streampayload,NewSerializernewSerializer){if(NrbfReader.StartsWithPayloadHeader(payload)){TfromPayload=UseThePayloadReaderToReadTheData<T>(payload);payload.Seek(0,SeekOrigin.Begin);newSerializer.Serialize(payload,fromPayload);payload.Flush();}else{returnnewSerializer.Deserialize<T>(payload)}}

      SzArrays

      Single dimension, zero-indexed arrays are expected to be the most frequently used arrays.

      SerializationRecordrootObject=NrbfReader.Read(File.OpenRead("peristedPayload.bf"));if(rootObjectisArrayRecord<string>arrayOfStrings){string?[]strings=arrayOfStrings.GetArray();}

      Other arrays

      BF supports:

      • jagged arrays
      • multi-dimensional array
      • non-zero indexed arrays

      They are all represented by internal types that derive from ArrayRecord. The users can use the API to instantiate such arrays, but they need to provide the expected array type. By doing that we make this advanced scenario possible and safe (the library is not loading any types, if there is a type mismatch it throws).

      publicabstractclassArrayRecord:SerializationRecrd{publicArrayGetArray(TypeexpectedArrayType,boolallowNulls=true);}
      ArrayRecordarrayRecord=(ArrayRecord)NrbfReader.Read(File.OpenRead("peristedPayload.bf"));if(arrayRecord.ArrayType==ArrayType.Jagged){int[][][]array=(int[][][])arrayRecord.GetArray(expectedArrayType:typeof(int[][][]));}

      For more usages of this API please refer to JaggedArraysTests.cs, RectangularArraysTests.cs and CustomOffsetArrays.cs.

      Arrays of non-primitive types

      Arrays of non-primitive types are represented as ArrayRecord<ClassRecord> or just ArrayRecord.

      ArrayRecord<ClassRecord>rootRecord=(ArrayRecord<ClassRecord>)NrbfReader.Read(File.OpenRead("peristedPayload.bf"));ClassRecord[]classRecords=rootRecord.GetArray(allowNulls:false)!;Sample[]output=classRecords.Select(classRecord =>newSample(){Integer=classRecord.GetInt32(nameof(Sample.Integer)),Text=classRecord.GetString(nameof(Sample.Text))}).ToArray();

      Risks

      If the new APIs are not easy to use, some of the users might choose the new OOB package with a copy of BF and remain vulnerable to all attacks. This defeats the purpose of our initiative and must be avoided.

      Metadata

      Metadata

      Assignees

      Labels

      api-approvedAPI was approved in API review, it can be implementedarea-System.Formats.Nrbfbinaryformatter-migrationIssues related to the removal of BinaryFormatter and migrations away from itblockingMarks issues that we want to fast track in order to unblock other important work

      Type

      No type

      Projects

      No projects

        Milestone

        Relationships

        None yet

        Development

        No branches or pull requests

        Issue actions

        , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
        Skip to content

        BinaryFormatter PayloadReader API #102014

        Description

        @adamsitnik

        Background and Motivation

        BinaryFormatter is getting removed in .NET 9, but our customers need to be able to read the payloads that:

        • were serialized with BF (using previous .NET versions) and persisted (to disk/db etc)
        • are being generated by software they have no control over (example: 3rd party clients calling an existing web service with public API that allows for BF).

        Our primary goal is to allow the users to read BF payloads in a secure manner from untrusted input. The principles:

        • Treating every input as potentially hostile.
        • No type loading of any kind (to avoid remote code execution).
        • No recursion of any kind (to avoid unbound recursion, stack overflow and denial of service).
        • No buffer pre-allocation based on size provided in payload (to avoid running out of memory and denial of service).
        • Using collision-resistant dictionary to store records referenced by other records.
        • Only primitive types can be instantiated in implicit way. Arrays can be instantiated on demand (with a default max size limit). Other types are never instantiated.

        We also want to make the APIs easy to use, to avoid the customers using the OOB package with the copy of BinaryFormatter (and remaining vulnerable to various attacks). That is why currently the public API surface is very narrow. We could expose more information, but we don't want to confuse the users or need them to become familiar with BF specification to get simple tasks done. Example: null can be represented using three different serialization records (ObjectNull, ObjectNullMultiple and ObjectNullMultiple256). The public APIs just return null, rather than a record that represents it.

        The new APIs need to be shipped in a new OOB package that supports older monikers, as we have first party customers running on Full Framework that are going to use it.

        Proposed API

        namespaceSystem.Runtime.Serialization.BinaryFormat;publicstaticclassNrbfReader{/// <summary>/// Checks if given buffer starts with <see href="https://learn.microsoft.com/openspecs/windows_protocols/ms-nrbf/a7e578d3-400a-4249-9424-7529d10d1b3c">NRBF payload header</see>./// </summary>/// <param name="bytes">The buffer to inspect.</param>/// <returns><see langword="true" /> if it starts with NRBF payload header; otherwise, <see langword="false" />.</returns>publicstaticboolStartsWithPayloadHeader(byte[]bytes);/// <summary>/// Checks if given stream starts with <see href="https://learn.microsoft.com/openspecs/windows_protocols/ms-nrbf/a7e578d3-400a-4249-9424-7529d10d1b3c">NRBF payload header</see>./// </summary>/// <param name="stream">The stream to inspect. The stream must be both readable and seekable.</param>/// <returns><see langword="true" /> if it starts with NRBF payload header; otherwise, <see langword="false" />.</returns>/// <exception cref="ArgumentNullException"><paramref name="stream" /> is <see langword="null" />.</exception>/// <exception cref="NotSupportedException">The stream does not support reading or seeking.</exception>/// <exception cref="ObjectDisposedException">The stream was closed.</exception>/// <remarks><para>When this method returns, <paramref name="stream" /> will be restored to its original position.</para></remarks>publicstaticboolStartsWithPayloadHeader(Streamstream);/// <summary>/// Reads the provided NRBF payload./// </summary>/// <param name="payload">The NRBF payload.</param>/// <param name="options">Options to control behavior during parsing.</param>/// <param name="leaveOpen">/// <see langword="true" /> to leave <paramref name="payload"/> payload open/// after the reading is finished; otherwise, <see langword="false" />./// </param>/// <returns>A <see cref="SerializationRecord"/> that represents the root object./// It can be either <see cref="PrimitiveTypeRecord{T}"/>,/// a <see cref="ClassRecord"/> or an <see cref="ArrayRecord"/>.</returns>/// <exception cref="ArgumentNullException"><paramref name="payload"/> is <see langword="null" />.</exception>/// <exception cref="ArgumentException"><paramref name="payload"/> does not support reading or is already closed.</exception>/// <exception cref="SerializationException">Reading from <paramref name="payload"/> encounters invalid NRBF data.</exception>/// <exception cref="DecoderFallbackException">Reading from <paramref name="payload"/>/// encounters an invalid UTF8 sequence.</exception>publicstaticSerializationRecordRead(Streampayload,PayloadOptions?options=default,boolleaveOpen=false);/// <param name="recordMap">/// When this method returns, contains a mapping of <see cref="SerializationRecord.ObjectId" /> to the associated serialization record./// This parameter is treated as uninitialized./// </param>publicstaticSerializationRecordRead(Streampayload,outIReadOnlyDictionary<int,SerializationRecord>recordMap,PayloadOptions?options=default,boolleaveOpen=false);/// <summary>/// Reads the provided Binary Format payload that is expected to contain an instance of any class (or struct) that is not an <seealso cref="Array"/> or a primitive type./// </summary>/// <returns>A <seealso cref="ClassRecord"/> that represents the root object.</returns>publicstaticClassRecordReadClassRecord(Streampayload,PayloadOptions?options=default,boolleaveOpen=false);}publicsealedclassPayloadOptions{publicPayloadOptions(){}publicTypeNameParseOptions?TypeNameParseOptions{get;set;}/// <summary>/// Gets or sets a value that indicates whether type name truncation is undone./// </summary>/// <value><see langword="true" /> if truncated type names should be reassembled; otherwise, <see langword="false" />.</value>/// <remarks>/// Example:/// TypeName: "Namespace.TypeName`1[[Namespace.GenericArgName"/// LibraryName: "AssemblyName]]"/// Is combined into "Namespace.TypeName`1[[Namespace.GenericArgName, AssemblyName]]"/// </remarks>publicboolUndoTruncatedTypeNames{get;set;}}/// <summary>/// Abstract class that represents the serialization record./// </summary>/// <remarks>/// Every instance returned to the end user can be either <seealso cref="PrimitiveTypeRecord{T}"/>,/// a <seealso cref="ClassRecord"/> or an <seealso cref="ArrayRecord"/>./// </remarks>publicabstractclassSerializationRecord{internalSerializationRecord();// others can't derive from this type/// <summary>/// Gets the type of the record./// </summary>/// <value>The type of the record.</value>publicabstractRecordTypeRecordType{get;}/// <summary>/// Gets the ID of the record./// </summary>/// <value>The ID of the record.</value>publicabstractintObjectId{get;}/// <summary>/// Compares the type and assembly name read from the payload against the specified type./// </summary>/// <remarks>/// <para>This method takes type forwarding into account.</para>/// <para>This method does NOT take into account member names or their types.</para>/// </remarks>/// <param name="type">The type to compare against.</param>/// <returns><see langword="true" /> if the serialized type and assembly name match provided type; otherwise, <see langword="false" />.</returns>publicvirtualboolIsTypeNameMatching(Typetype);}/// <summary>/// Record type./// </summary>/// <remarks>/// <para>/// The enumeration does not contain all values supported by the <see href="https://learn.microsoft.com/openspecs/windows_protocols/ms-nrbf/954a0657-b901-4813-9398-4ec732fe8b32">/// [MS-NRBF] 2.1.2.1</see>, but only those supported by the <see cref="PayloadReader"/>./// </para>/// </remarks>publicenumRecordType:byte{SerializedStreamHeader,ClassWithId,// SystemClassWithMembers and ClassWithMembers are not supported by design (require type loading) and not includedSystemClassWithMembersAndTypes=4,ClassWithMembersAndTypes,BinaryObjectString,BinaryArray,MemberPrimitiveTyped,MemberReference,ObjectNull,MessageEnd,BinaryLibrary,ObjectNullMultiple256,ObjectNullMultiple,ArraySinglePrimitive,ArraySingleObject,ArraySingleString}/// <summary>/// Represents a record that itself represents the primitive value of <typeparamref name="T"/> type./// </summary>/// <typeparam name="T">The type of the primitive value.</typeparam>/// <remarks>/// <para>/// The NRBF specification considers the following types to be primitive:/// <see cref="string"/>, <see cref="bool"/>, <see cref="byte"/>, <see cref="sbyte"/>/// <see cref="char"/>, <see cref="short"/>, <see cref="ushort"/>,/// <see cref="int"/>, <see cref="uint"/>, <see cref="long"/>, <see cref="ulong"/>,/// <see cref="float"/>, <see cref="double"/>, <see cref="decimal"/>,/// <see cref="DateTime"/> and <see cref="TimeSpan"/>./// </para>/// <para>Other serialization records are represented with <see cref="ClassRecord"/> or <see cref="ArrayRecord"/>.</para>/// </remarks>publicabstractclassPrimitiveTypeRecord<T>:SerializationRecord{privateprotectedPrimitiveTypeRecord(Tvalue);publicTValue{get;}}/// <summary>/// Defines the core behavior for NRBF class records and provides a base for derived classes./// </summary>publicabstractclassClassRecord:SerializationRecord{privateprotectedClassRecord(ClassInfoclassInfo);publicTypeNameTypeName{get;}publicIEnumerable<string>MemberNames{get;}/// <summary>/// Checks if member of given name was present in the payload./// </summary>/// <param name="memberName">The name of the member.</param>/// <returns><see langword="true" /> if it was present, otherwise <see langword="false" />.</returns>/// <remarks>/// <para>/// It's recommended to use this method when dealing with payload that may contain/// different versions of the same type./// </para>/// </remarks>publicboolHasMember(stringmemberName);publicstring?GetString(stringmemberName);publicboolGetBoolean(stringmemberName);publicbyteGetByte(stringmemberName);publicsbyteGetSByte(stringmemberName);publicshortGetInt16(stringmemberName);publicushortGetUInt16(stringmemberName);publiccharGetChar(stringmemberName);publicintGetInt32(stringmemberName);publicuintGetUInt32(stringmemberName);publicfloatGetSingle(stringmemberName);publiclongGetInt64(stringmemberName);publiculongGetUInt64(stringmemberName);publicdoubleGetDouble(stringmemberName);publicdecimalGetDecimal(stringmemberName);publicTimeSpanGetTimeSpan(stringmemberName);publicDateTimeGetDateTime(stringmemberName);/// <summary>/// Retrieves an array for the provided <paramref name="memberName"/>./// </summary>/// <param name="memberName">The name of the field.</param>/// <param name="allowNulls">Specifies whether null values are allowed.</param>/// <returns>The array itself or null.</returns>/// <exception cref="KeyNotFoundException">Member of such name does not exist.</exception>/// <exception cref="InvalidOperationException">Member of such name has value of a different type.</exception>publicT?[]?GetArrayOfPrimitiveType<T>(stringmemberName,boolallowNulls=true);/// <summary>/// Retrieves the <see cref="SerializationRecord" /> of the provided <paramref name="memberName"/>./// </summary>/// <param name="memberName">The name of the field.</param>/// <returns>The serialization record, which can be any of <see cref="PrimitiveTypeRecord{T}"/>,/// <see cref="ClassRecord"/>, <see cref="ArrayRecord"/> or <see langword="null" />./// </returns>/// <exception cref="KeyNotFoundException"><paramref name="memberName" /> does not refer to a known member. You can use <see cref="HasMember(string)"/> to check if given member exists.</exception>/// <exception cref="InvalidOperationException">The specified member is not a <see cref="SerializationRecord"/>, but just a raw primitive value.</exception>publicSerializationRecord?GetSerializationRecord(stringmemberName);/// <summary>/// Retrieves the value of the provided <paramref name="memberName"/>./// </summary>/// <param name="memberName">The name of the member.</param>/// <returns>The value.</returns>/// <exception cref="KeyNotFoundException"><paramref name="memberName" /> does not refer to a known member. You can use <see cref="HasMember(string)"/> to check if given member exists.</exception>/// <exception cref="InvalidOperationException">Member of such name has value of a different type.</exception>publicClassRecord?GetClassRecord(stringmemberName);/// <returns>/// <para>For primitive types like <see cref="int"/>, <see langword="string"/> or <see cref="DateTime"/> returns their value.</para>/// <para>For nulls, returns a null.</para>/// <para>For other types that are not arrays, returns an instance of <see cref="ClassRecord"/>.</para>/// <para>For single-dimensional arrays returns <see cref="ArrayRecord{T}"/> where the generic type is the primitive type or <see cref="ClassRecord"/>.</para>/// <para>For jagged and multi-dimensional arrays, returns an instance of <see cref="ArrayRecord"/>.</para>/// </returns>publicobject?GetRawValue(stringmemberName);}/// <summary>/// Defines the core behavior for NRBF array records and provides a base for derived classes./// </summary>publicabstractclassArrayRecord:SerializationRecord{privateprotectedArrayRecord(ArrayInfoarrayInfo);/// <summary>/// When overridden in a derived class, gets a buffer of integers that represent the number of elements in every dimension./// </summary>/// <value>A buffer of integers that represent the number of elements in every dimension.</value>publicabstractReadOnlySpan<int>Lengths{get;}/// <summary>/// Gets the rank of the array./// </summary>/// <value>The rank of the array.</value>publicintRank{get;}/// <summary>/// Gets the type of the array./// </summary>/// <value>The type of the array.</value>publicBinaryArrayTypeArrayType{get;}/// <summary>/// Gets the name of the array element type./// </summary>/// <value>The name of the array element type.</value>publicabstractTypeNameElementTypeName{get;}/// <summary>/// Allocates an array and fills it with the data provided in the serialized records (in case of primitive types like <see cref="string"/> or <see cref="int"/>) or the serialized records themselves./// </summary>/// <param name="expectedArrayType">Expected array type.</param>/// <param name="allowNulls">/// <see langword="true" /> to permit <see langword="null" /> values within the array;/// otherwise, <see langword="false" />./// </param>/// <returns>An array filled with the data provided in the serialized records.</returns>/// <exception cref="InvalidOperationException"><paramref name="expectedArrayType" /> does not match the data from the payload.</exception>publicArrayGetArray(TypeexpectedArrayType,boolallowNulls=true);}/// <summary>/// Binary array type./// </summary>/// <remarks>/// BinaryArrayType enumeration is described in <see href="https://learn.microsoft.com/openspecs/windows_protocols/ms-nrbf/4dbbf3a8-6bc4-4dfc-aa7e-36a35be6ff58">[MS-NRBF] 2.4.1.1</see>./// </remarks>publicenumBinaryArrayType:byte{/// <summary>/// A single-dimensional array./// </summary>Single=0,/// <summary>/// An array whose elements are arrays. The elements of a jagged array can be of different dimensions and sizes./// </summary>Jagged=1,/// <summary>/// A multi-dimensional rectangular array./// </summary>Rectangular=2,/// <summary>/// A single-dimensional array where the lower bound index is greater than 0./// </summary>SingleOffset=3,/// <summary>/// A jagged array where the lower bound index is greater than 0./// </summary>JaggedOffset=4,/// <summary>/// Multi-dimensional arrays where the lower bound index of at least one of the dimensions is greater than 0./// </summary>RectangularOffset=5}/// <summary>/// Defines the core behavior for NRBF single dimensional, zero-indexed array records and provides a base for derived classes./// </summary>publicabstractclassArrayRecord<T>:ArrayRecord{privateprotectedArrayRecord(ArrayInfoarrayInfo);/// <summary>/// Gets the length of the array./// </summary>/// <value>The length of the array.</value>publicintLength{get;}/// <summary>/// When overridden in a derived class, allocates an array of <typeparamref name="T"/> and fills it with the data provided in the serialized records (in case of primitive types like <see cref="string"/> or <see cref="int"/>) or the serialized records themselves./// </summary>/// <param name="allowNulls">/// <see langword="true" /> to permit <see langword="null" /> values within the array;/// otherwise, <see langword="false" />./// </param>/// <returns>An array filled with the data provided in the serialized records.</returns>publicabstractT?[]GetArray(boolallowNulls=true);}

        Usage Examples

        The implementation with no dependency to dotnet/runtime can be found here.

        Reading a class serialized with BF to a file

        ClassRecordrootRecord=NrbfReader.ReadClassRecord(File.OpenRead("peristedPayload.bf"));Sampleoutput=new(){// using the dedicated methods to read primitive valuesInteger=rootRecord.GetInt32(nameof(Sample.Integer)),Text=rootRecord.GetString(nameof(Sample.Text)),// using dedicated method to read an array of bytesArrayOfBytes=rootRecord.GetArrayOfPrimitiveType<byte>(nameof(Sample.ArrayOfBytes)),// using GetClassRecord to read a class recordClassInstance=new(){Text=rootRecord.GetClassRecord(nameof(Sample.ClassInstance))!.GetString(nameof(Sample.Text))}};[Serializable]publicclassSample{publicintInteger;publicstring?Text;publicbyte[]?ArrayOfBytes;publicSample?ClassInstance;}

        Checking if Stream contains BF payload

        The users need to be able to check if given Stream contains BF data, as they might want to migrate the data on demand to new serialization format:

        staticTPseudocode<T>(Streampayload,NewSerializernewSerializer){if(NrbfReader.StartsWithPayloadHeader(payload)){TfromPayload=UseThePayloadReaderToReadTheData<T>(payload);payload.Seek(0,SeekOrigin.Begin);newSerializer.Serialize(payload,fromPayload);payload.Flush();}else{returnnewSerializer.Deserialize<T>(payload)}}

        SzArrays

        Single dimension, zero-indexed arrays are expected to be the most frequently used arrays.

        SerializationRecordrootObject=NrbfReader.Read(File.OpenRead("peristedPayload.bf"));if(rootObjectisArrayRecord<string>arrayOfStrings){string?[]strings=arrayOfStrings.GetArray();}

        Other arrays

        BF supports:

        • jagged arrays
        • multi-dimensional array
        • non-zero indexed arrays

        They are all represented by internal types that derive from ArrayRecord. The users can use the API to instantiate such arrays, but they need to provide the expected array type. By doing that we make this advanced scenario possible and safe (the library is not loading any types, if there is a type mismatch it throws).

        publicabstractclassArrayRecord:SerializationRecrd{publicArrayGetArray(TypeexpectedArrayType,boolallowNulls=true);}
        ArrayRecordarrayRecord=(ArrayRecord)NrbfReader.Read(File.OpenRead("peristedPayload.bf"));if(arrayRecord.ArrayType==ArrayType.Jagged){int[][][]array=(int[][][])arrayRecord.GetArray(expectedArrayType:typeof(int[][][]));}

        For more usages of this API please refer to JaggedArraysTests.cs, RectangularArraysTests.cs and CustomOffsetArrays.cs.

        Arrays of non-primitive types

        Arrays of non-primitive types are represented as ArrayRecord<ClassRecord> or just ArrayRecord.

        ArrayRecord<ClassRecord>rootRecord=(ArrayRecord<ClassRecord>)NrbfReader.Read(File.OpenRead("peristedPayload.bf"));ClassRecord[]classRecords=rootRecord.GetArray(allowNulls:false)!;Sample[]output=classRecords.Select(classRecord =>newSample(){Integer=classRecord.GetInt32(nameof(Sample.Integer)),Text=classRecord.GetString(nameof(Sample.Text))}).ToArray();

        Risks

        If the new APIs are not easy to use, some of the users might choose the new OOB package with a copy of BF and remain vulnerable to all attacks. This defeats the purpose of our initiative and must be avoided.

        Metadata

        Metadata

        Assignees

        Labels

        api-approvedAPI was approved in API review, it can be implementedarea-System.Formats.Nrbfbinaryformatter-migrationIssues related to the removal of BinaryFormatter and migrations away from itblockingMarks issues that we want to fast track in order to unblock other important work

        Type

        No type

        Projects

        No projects

          Milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
          Skip to content

          BinaryFormatter PayloadReader API #102014

          Description

          @adamsitnik

          Background and Motivation

          BinaryFormatter is getting removed in .NET 9, but our customers need to be able to read the payloads that:

          • were serialized with BF (using previous .NET versions) and persisted (to disk/db etc)
          • are being generated by software they have no control over (example: 3rd party clients calling an existing web service with public API that allows for BF).

          Our primary goal is to allow the users to read BF payloads in a secure manner from untrusted input. The principles:

          • Treating every input as potentially hostile.
          • No type loading of any kind (to avoid remote code execution).
          • No recursion of any kind (to avoid unbound recursion, stack overflow and denial of service).
          • No buffer pre-allocation based on size provided in payload (to avoid running out of memory and denial of service).
          • Using collision-resistant dictionary to store records referenced by other records.
          • Only primitive types can be instantiated in implicit way. Arrays can be instantiated on demand (with a default max size limit). Other types are never instantiated.

          We also want to make the APIs easy to use, to avoid the customers using the OOB package with the copy of BinaryFormatter (and remaining vulnerable to various attacks). That is why currently the public API surface is very narrow. We could expose more information, but we don't want to confuse the users or need them to become familiar with BF specification to get simple tasks done. Example: null can be represented using three different serialization records (ObjectNull, ObjectNullMultiple and ObjectNullMultiple256). The public APIs just return null, rather than a record that represents it.

          The new APIs need to be shipped in a new OOB package that supports older monikers, as we have first party customers running on Full Framework that are going to use it.

          Proposed API

          namespaceSystem.Runtime.Serialization.BinaryFormat;publicstaticclassNrbfReader{/// <summary>/// Checks if given buffer starts with <see href="https://learn.microsoft.com/openspecs/windows_protocols/ms-nrbf/a7e578d3-400a-4249-9424-7529d10d1b3c">NRBF payload header</see>./// </summary>/// <param name="bytes">The buffer to inspect.</param>/// <returns><see langword="true" /> if it starts with NRBF payload header; otherwise, <see langword="false" />.</returns>publicstaticboolStartsWithPayloadHeader(byte[]bytes);/// <summary>/// Checks if given stream starts with <see href="https://learn.microsoft.com/openspecs/windows_protocols/ms-nrbf/a7e578d3-400a-4249-9424-7529d10d1b3c">NRBF payload header</see>./// </summary>/// <param name="stream">The stream to inspect. The stream must be both readable and seekable.</param>/// <returns><see langword="true" /> if it starts with NRBF payload header; otherwise, <see langword="false" />.</returns>/// <exception cref="ArgumentNullException"><paramref name="stream" /> is <see langword="null" />.</exception>/// <exception cref="NotSupportedException">The stream does not support reading or seeking.</exception>/// <exception cref="ObjectDisposedException">The stream was closed.</exception>/// <remarks><para>When this method returns, <paramref name="stream" /> will be restored to its original position.</para></remarks>publicstaticboolStartsWithPayloadHeader(Streamstream);/// <summary>/// Reads the provided NRBF payload./// </summary>/// <param name="payload">The NRBF payload.</param>/// <param name="options">Options to control behavior during parsing.</param>/// <param name="leaveOpen">/// <see langword="true" /> to leave <paramref name="payload"/> payload open/// after the reading is finished; otherwise, <see langword="false" />./// </param>/// <returns>A <see cref="SerializationRecord"/> that represents the root object./// It can be either <see cref="PrimitiveTypeRecord{T}"/>,/// a <see cref="ClassRecord"/> or an <see cref="ArrayRecord"/>.</returns>/// <exception cref="ArgumentNullException"><paramref name="payload"/> is <see langword="null" />.</exception>/// <exception cref="ArgumentException"><paramref name="payload"/> does not support reading or is already closed.</exception>/// <exception cref="SerializationException">Reading from <paramref name="payload"/> encounters invalid NRBF data.</exception>/// <exception cref="DecoderFallbackException">Reading from <paramref name="payload"/>/// encounters an invalid UTF8 sequence.</exception>publicstaticSerializationRecordRead(Streampayload,PayloadOptions?options=default,boolleaveOpen=false);/// <param name="recordMap">/// When this method returns, contains a mapping of <see cref="SerializationRecord.ObjectId" /> to the associated serialization record./// This parameter is treated as uninitialized./// </param>publicstaticSerializationRecordRead(Streampayload,outIReadOnlyDictionary<int,SerializationRecord>recordMap,PayloadOptions?options=default,boolleaveOpen=false);/// <summary>/// Reads the provided Binary Format payload that is expected to contain an instance of any class (or struct) that is not an <seealso cref="Array"/> or a primitive type./// </summary>/// <returns>A <seealso cref="ClassRecord"/> that represents the root object.</returns>publicstaticClassRecordReadClassRecord(Streampayload,PayloadOptions?options=default,boolleaveOpen=false);}publicsealedclassPayloadOptions{publicPayloadOptions(){}publicTypeNameParseOptions?TypeNameParseOptions{get;set;}/// <summary>/// Gets or sets a value that indicates whether type name truncation is undone./// </summary>/// <value><see langword="true" /> if truncated type names should be reassembled; otherwise, <see langword="false" />.</value>/// <remarks>/// Example:/// TypeName: "Namespace.TypeName`1[[Namespace.GenericArgName"/// LibraryName: "AssemblyName]]"/// Is combined into "Namespace.TypeName`1[[Namespace.GenericArgName, AssemblyName]]"/// </remarks>publicboolUndoTruncatedTypeNames{get;set;}}/// <summary>/// Abstract class that represents the serialization record./// </summary>/// <remarks>/// Every instance returned to the end user can be either <seealso cref="PrimitiveTypeRecord{T}"/>,/// a <seealso cref="ClassRecord"/> or an <seealso cref="ArrayRecord"/>./// </remarks>publicabstractclassSerializationRecord{internalSerializationRecord();// others can't derive from this type/// <summary>/// Gets the type of the record./// </summary>/// <value>The type of the record.</value>publicabstractRecordTypeRecordType{get;}/// <summary>/// Gets the ID of the record./// </summary>/// <value>The ID of the record.</value>publicabstractintObjectId{get;}/// <summary>/// Compares the type and assembly name read from the payload against the specified type./// </summary>/// <remarks>/// <para>This method takes type forwarding into account.</para>/// <para>This method does NOT take into account member names or their types.</para>/// </remarks>/// <param name="type">The type to compare against.</param>/// <returns><see langword="true" /> if the serialized type and assembly name match provided type; otherwise, <see langword="false" />.</returns>publicvirtualboolIsTypeNameMatching(Typetype);}/// <summary>/// Record type./// </summary>/// <remarks>/// <para>/// The enumeration does not contain all values supported by the <see href="https://learn.microsoft.com/openspecs/windows_protocols/ms-nrbf/954a0657-b901-4813-9398-4ec732fe8b32">/// [MS-NRBF] 2.1.2.1</see>, but only those supported by the <see cref="PayloadReader"/>./// </para>/// </remarks>publicenumRecordType:byte{SerializedStreamHeader,ClassWithId,// SystemClassWithMembers and ClassWithMembers are not supported by design (require type loading) and not includedSystemClassWithMembersAndTypes=4,ClassWithMembersAndTypes,BinaryObjectString,BinaryArray,MemberPrimitiveTyped,MemberReference,ObjectNull,MessageEnd,BinaryLibrary,ObjectNullMultiple256,ObjectNullMultiple,ArraySinglePrimitive,ArraySingleObject,ArraySingleString}/// <summary>/// Represents a record that itself represents the primitive value of <typeparamref name="T"/> type./// </summary>/// <typeparam name="T">The type of the primitive value.</typeparam>/// <remarks>/// <para>/// The NRBF specification considers the following types to be primitive:/// <see cref="string"/>, <see cref="bool"/>, <see cref="byte"/>, <see cref="sbyte"/>/// <see cref="char"/>, <see cref="short"/>, <see cref="ushort"/>,/// <see cref="int"/>, <see cref="uint"/>, <see cref="long"/>, <see cref="ulong"/>,/// <see cref="float"/>, <see cref="double"/>, <see cref="decimal"/>,/// <see cref="DateTime"/> and <see cref="TimeSpan"/>./// </para>/// <para>Other serialization records are represented with <see cref="ClassRecord"/> or <see cref="ArrayRecord"/>.</para>/// </remarks>publicabstractclassPrimitiveTypeRecord<T>:SerializationRecord{privateprotectedPrimitiveTypeRecord(Tvalue);publicTValue{get;}}/// <summary>/// Defines the core behavior for NRBF class records and provides a base for derived classes./// </summary>publicabstractclassClassRecord:SerializationRecord{privateprotectedClassRecord(ClassInfoclassInfo);publicTypeNameTypeName{get;}publicIEnumerable<string>MemberNames{get;}/// <summary>/// Checks if member of given name was present in the payload./// </summary>/// <param name="memberName">The name of the member.</param>/// <returns><see langword="true" /> if it was present, otherwise <see langword="false" />.</returns>/// <remarks>/// <para>/// It's recommended to use this method when dealing with payload that may contain/// different versions of the same type./// </para>/// </remarks>publicboolHasMember(stringmemberName);publicstring?GetString(stringmemberName);publicboolGetBoolean(stringmemberName);publicbyteGetByte(stringmemberName);publicsbyteGetSByte(stringmemberName);publicshortGetInt16(stringmemberName);publicushortGetUInt16(stringmemberName);publiccharGetChar(stringmemberName);publicintGetInt32(stringmemberName);publicuintGetUInt32(stringmemberName);publicfloatGetSingle(stringmemberName);publiclongGetInt64(stringmemberName);publiculongGetUInt64(stringmemberName);publicdoubleGetDouble(stringmemberName);publicdecimalGetDecimal(stringmemberName);publicTimeSpanGetTimeSpan(stringmemberName);publicDateTimeGetDateTime(stringmemberName);/// <summary>/// Retrieves an array for the provided <paramref name="memberName"/>./// </summary>/// <param name="memberName">The name of the field.</param>/// <param name="allowNulls">Specifies whether null values are allowed.</param>/// <returns>The array itself or null.</returns>/// <exception cref="KeyNotFoundException">Member of such name does not exist.</exception>/// <exception cref="InvalidOperationException">Member of such name has value of a different type.</exception>publicT?[]?GetArrayOfPrimitiveType<T>(stringmemberName,boolallowNulls=true);/// <summary>/// Retrieves the <see cref="SerializationRecord" /> of the provided <paramref name="memberName"/>./// </summary>/// <param name="memberName">The name of the field.</param>/// <returns>The serialization record, which can be any of <see cref="PrimitiveTypeRecord{T}"/>,/// <see cref="ClassRecord"/>, <see cref="ArrayRecord"/> or <see langword="null" />./// </returns>/// <exception cref="KeyNotFoundException"><paramref name="memberName" /> does not refer to a known member. You can use <see cref="HasMember(string)"/> to check if given member exists.</exception>/// <exception cref="InvalidOperationException">The specified member is not a <see cref="SerializationRecord"/>, but just a raw primitive value.</exception>publicSerializationRecord?GetSerializationRecord(stringmemberName);/// <summary>/// Retrieves the value of the provided <paramref name="memberName"/>./// </summary>/// <param name="memberName">The name of the member.</param>/// <returns>The value.</returns>/// <exception cref="KeyNotFoundException"><paramref name="memberName" /> does not refer to a known member. You can use <see cref="HasMember(string)"/> to check if given member exists.</exception>/// <exception cref="InvalidOperationException">Member of such name has value of a different type.</exception>publicClassRecord?GetClassRecord(stringmemberName);/// <returns>/// <para>For primitive types like <see cref="int"/>, <see langword="string"/> or <see cref="DateTime"/> returns their value.</para>/// <para>For nulls, returns a null.</para>/// <para>For other types that are not arrays, returns an instance of <see cref="ClassRecord"/>.</para>/// <para>For single-dimensional arrays returns <see cref="ArrayRecord{T}"/> where the generic type is the primitive type or <see cref="ClassRecord"/>.</para>/// <para>For jagged and multi-dimensional arrays, returns an instance of <see cref="ArrayRecord"/>.</para>/// </returns>publicobject?GetRawValue(stringmemberName);}/// <summary>/// Defines the core behavior for NRBF array records and provides a base for derived classes./// </summary>publicabstractclassArrayRecord:SerializationRecord{privateprotectedArrayRecord(ArrayInfoarrayInfo);/// <summary>/// When overridden in a derived class, gets a buffer of integers that represent the number of elements in every dimension./// </summary>/// <value>A buffer of integers that represent the number of elements in every dimension.</value>publicabstractReadOnlySpan<int>Lengths{get;}/// <summary>/// Gets the rank of the array./// </summary>/// <value>The rank of the array.</value>publicintRank{get;}/// <summary>/// Gets the type of the array./// </summary>/// <value>The type of the array.</value>publicBinaryArrayTypeArrayType{get;}/// <summary>/// Gets the name of the array element type./// </summary>/// <value>The name of the array element type.</value>publicabstractTypeNameElementTypeName{get;}/// <summary>/// Allocates an array and fills it with the data provided in the serialized records (in case of primitive types like <see cref="string"/> or <see cref="int"/>) or the serialized records themselves./// </summary>/// <param name="expectedArrayType">Expected array type.</param>/// <param name="allowNulls">/// <see langword="true" /> to permit <see langword="null" /> values within the array;/// otherwise, <see langword="false" />./// </param>/// <returns>An array filled with the data provided in the serialized records.</returns>/// <exception cref="InvalidOperationException"><paramref name="expectedArrayType" /> does not match the data from the payload.</exception>publicArrayGetArray(TypeexpectedArrayType,boolallowNulls=true);}/// <summary>/// Binary array type./// </summary>/// <remarks>/// BinaryArrayType enumeration is described in <see href="https://learn.microsoft.com/openspecs/windows_protocols/ms-nrbf/4dbbf3a8-6bc4-4dfc-aa7e-36a35be6ff58">[MS-NRBF] 2.4.1.1</see>./// </remarks>publicenumBinaryArrayType:byte{/// <summary>/// A single-dimensional array./// </summary>Single=0,/// <summary>/// An array whose elements are arrays. The elements of a jagged array can be of different dimensions and sizes./// </summary>Jagged=1,/// <summary>/// A multi-dimensional rectangular array./// </summary>Rectangular=2,/// <summary>/// A single-dimensional array where the lower bound index is greater than 0./// </summary>SingleOffset=3,/// <summary>/// A jagged array where the lower bound index is greater than 0./// </summary>JaggedOffset=4,/// <summary>/// Multi-dimensional arrays where the lower bound index of at least one of the dimensions is greater than 0./// </summary>RectangularOffset=5}/// <summary>/// Defines the core behavior for NRBF single dimensional, zero-indexed array records and provides a base for derived classes./// </summary>publicabstractclassArrayRecord<T>:ArrayRecord{privateprotectedArrayRecord(ArrayInfoarrayInfo);/// <summary>/// Gets the length of the array./// </summary>/// <value>The length of the array.</value>publicintLength{get;}/// <summary>/// When overridden in a derived class, allocates an array of <typeparamref name="T"/> and fills it with the data provided in the serialized records (in case of primitive types like <see cref="string"/> or <see cref="int"/>) or the serialized records themselves./// </summary>/// <param name="allowNulls">/// <see langword="true" /> to permit <see langword="null" /> values within the array;/// otherwise, <see langword="false" />./// </param>/// <returns>An array filled with the data provided in the serialized records.</returns>publicabstractT?[]GetArray(boolallowNulls=true);}

          Usage Examples

          The implementation with no dependency to dotnet/runtime can be found here.

          Reading a class serialized with BF to a file

          ClassRecordrootRecord=NrbfReader.ReadClassRecord(File.OpenRead("peristedPayload.bf"));Sampleoutput=new(){// using the dedicated methods to read primitive valuesInteger=rootRecord.GetInt32(nameof(Sample.Integer)),Text=rootRecord.GetString(nameof(Sample.Text)),// using dedicated method to read an array of bytesArrayOfBytes=rootRecord.GetArrayOfPrimitiveType<byte>(nameof(Sample.ArrayOfBytes)),// using GetClassRecord to read a class recordClassInstance=new(){Text=rootRecord.GetClassRecord(nameof(Sample.ClassInstance))!.GetString(nameof(Sample.Text))}};[Serializable]publicclassSample{publicintInteger;publicstring?Text;publicbyte[]?ArrayOfBytes;publicSample?ClassInstance;}

          Checking if Stream contains BF payload

          The users need to be able to check if given Stream contains BF data, as they might want to migrate the data on demand to new serialization format:

          staticTPseudocode<T>(Streampayload,NewSerializernewSerializer){if(NrbfReader.StartsWithPayloadHeader(payload)){TfromPayload=UseThePayloadReaderToReadTheData<T>(payload);payload.Seek(0,SeekOrigin.Begin);newSerializer.Serialize(payload,fromPayload);payload.Flush();}else{returnnewSerializer.Deserialize<T>(payload)}}

          SzArrays

          Single dimension, zero-indexed arrays are expected to be the most frequently used arrays.

          SerializationRecordrootObject=NrbfReader.Read(File.OpenRead("peristedPayload.bf"));if(rootObjectisArrayRecord<string>arrayOfStrings){string?[]strings=arrayOfStrings.GetArray();}

          Other arrays

          BF supports:

          • jagged arrays
          • multi-dimensional array
          • non-zero indexed arrays

          They are all represented by internal types that derive from ArrayRecord. The users can use the API to instantiate such arrays, but they need to provide the expected array type. By doing that we make this advanced scenario possible and safe (the library is not loading any types, if there is a type mismatch it throws).

          publicabstractclassArrayRecord:SerializationRecrd{publicArrayGetArray(TypeexpectedArrayType,boolallowNulls=true);}
          ArrayRecordarrayRecord=(ArrayRecord)NrbfReader.Read(File.OpenRead("peristedPayload.bf"));if(arrayRecord.ArrayType==ArrayType.Jagged){int[][][]array=(int[][][])arrayRecord.GetArray(expectedArrayType:typeof(int[][][]));}

          For more usages of this API please refer to JaggedArraysTests.cs, RectangularArraysTests.cs and CustomOffsetArrays.cs.

          Arrays of non-primitive types

          Arrays of non-primitive types are represented as ArrayRecord<ClassRecord> or just ArrayRecord.

          ArrayRecord<ClassRecord>rootRecord=(ArrayRecord<ClassRecord>)NrbfReader.Read(File.OpenRead("peristedPayload.bf"));ClassRecord[]classRecords=rootRecord.GetArray(allowNulls:false)!;Sample[]output=classRecords.Select(classRecord =>newSample(){Integer=classRecord.GetInt32(nameof(Sample.Integer)),Text=classRecord.GetString(nameof(Sample.Text))}).ToArray();

          Risks

          If the new APIs are not easy to use, some of the users might choose the new OOB package with a copy of BF and remain vulnerable to all attacks. This defeats the purpose of our initiative and must be avoided.

          Metadata

          Metadata

          Assignees

          Labels

          api-approvedAPI was approved in API review, it can be implementedarea-System.Formats.Nrbfbinaryformatter-migrationIssues related to the removal of BinaryFormatter and migrations away from itblockingMarks issues that we want to fast track in order to unblock other important work

          Type

          No type

          Projects

          No projects

            Milestone

            Relationships

            None yet

            Development

            No branches or pull requests

            Issue actions

            , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
            Skip to content

            BinaryFormatter PayloadReader API #102014

            Description

            @adamsitnik

            Background and Motivation

            BinaryFormatter is getting removed in .NET 9, but our customers need to be able to read the payloads that:

            • were serialized with BF (using previous .NET versions) and persisted (to disk/db etc)
            • are being generated by software they have no control over (example: 3rd party clients calling an existing web service with public API that allows for BF).

            Our primary goal is to allow the users to read BF payloads in a secure manner from untrusted input. The principles:

            • Treating every input as potentially hostile.
            • No type loading of any kind (to avoid remote code execution).
            • No recursion of any kind (to avoid unbound recursion, stack overflow and denial of service).
            • No buffer pre-allocation based on size provided in payload (to avoid running out of memory and denial of service).
            • Using collision-resistant dictionary to store records referenced by other records.
            • Only primitive types can be instantiated in implicit way. Arrays can be instantiated on demand (with a default max size limit). Other types are never instantiated.

            We also want to make the APIs easy to use, to avoid the customers using the OOB package with the copy of BinaryFormatter (and remaining vulnerable to various attacks). That is why currently the public API surface is very narrow. We could expose more information, but we don't want to confuse the users or need them to become familiar with BF specification to get simple tasks done. Example: null can be represented using three different serialization records (ObjectNull, ObjectNullMultiple and ObjectNullMultiple256). The public APIs just return null, rather than a record that represents it.

            The new APIs need to be shipped in a new OOB package that supports older monikers, as we have first party customers running on Full Framework that are going to use it.

            Proposed API

            namespaceSystem.Runtime.Serialization.BinaryFormat;publicstaticclassNrbfReader{/// <summary>/// Checks if given buffer starts with <see href="https://learn.microsoft.com/openspecs/windows_protocols/ms-nrbf/a7e578d3-400a-4249-9424-7529d10d1b3c">NRBF payload header</see>./// </summary>/// <param name="bytes">The buffer to inspect.</param>/// <returns><see langword="true" /> if it starts with NRBF payload header; otherwise, <see langword="false" />.</returns>publicstaticboolStartsWithPayloadHeader(byte[]bytes);/// <summary>/// Checks if given stream starts with <see href="https://learn.microsoft.com/openspecs/windows_protocols/ms-nrbf/a7e578d3-400a-4249-9424-7529d10d1b3c">NRBF payload header</see>./// </summary>/// <param name="stream">The stream to inspect. The stream must be both readable and seekable.</param>/// <returns><see langword="true" /> if it starts with NRBF payload header; otherwise, <see langword="false" />.</returns>/// <exception cref="ArgumentNullException"><paramref name="stream" /> is <see langword="null" />.</exception>/// <exception cref="NotSupportedException">The stream does not support reading or seeking.</exception>/// <exception cref="ObjectDisposedException">The stream was closed.</exception>/// <remarks><para>When this method returns, <paramref name="stream" /> will be restored to its original position.</para></remarks>publicstaticboolStartsWithPayloadHeader(Streamstream);/// <summary>/// Reads the provided NRBF payload./// </summary>/// <param name="payload">The NRBF payload.</param>/// <param name="options">Options to control behavior during parsing.</param>/// <param name="leaveOpen">/// <see langword="true" /> to leave <paramref name="payload"/> payload open/// after the reading is finished; otherwise, <see langword="false" />./// </param>/// <returns>A <see cref="SerializationRecord"/> that represents the root object./// It can be either <see cref="PrimitiveTypeRecord{T}"/>,/// a <see cref="ClassRecord"/> or an <see cref="ArrayRecord"/>.</returns>/// <exception cref="ArgumentNullException"><paramref name="payload"/> is <see langword="null" />.</exception>/// <exception cref="ArgumentException"><paramref name="payload"/> does not support reading or is already closed.</exception>/// <exception cref="SerializationException">Reading from <paramref name="payload"/> encounters invalid NRBF data.</exception>/// <exception cref="DecoderFallbackException">Reading from <paramref name="payload"/>/// encounters an invalid UTF8 sequence.</exception>publicstaticSerializationRecordRead(Streampayload,PayloadOptions?options=default,boolleaveOpen=false);/// <param name="recordMap">/// When this method returns, contains a mapping of <see cref="SerializationRecord.ObjectId" /> to the associated serialization record./// This parameter is treated as uninitialized./// </param>publicstaticSerializationRecordRead(Streampayload,outIReadOnlyDictionary<int,SerializationRecord>recordMap,PayloadOptions?options=default,boolleaveOpen=false);/// <summary>/// Reads the provided Binary Format payload that is expected to contain an instance of any class (or struct) that is not an <seealso cref="Array"/> or a primitive type./// </summary>/// <returns>A <seealso cref="ClassRecord"/> that represents the root object.</returns>publicstaticClassRecordReadClassRecord(Streampayload,PayloadOptions?options=default,boolleaveOpen=false);}publicsealedclassPayloadOptions{publicPayloadOptions(){}publicTypeNameParseOptions?TypeNameParseOptions{get;set;}/// <summary>/// Gets or sets a value that indicates whether type name truncation is undone./// </summary>/// <value><see langword="true" /> if truncated type names should be reassembled; otherwise, <see langword="false" />.</value>/// <remarks>/// Example:/// TypeName: "Namespace.TypeName`1[[Namespace.GenericArgName"/// LibraryName: "AssemblyName]]"/// Is combined into "Namespace.TypeName`1[[Namespace.GenericArgName, AssemblyName]]"/// </remarks>publicboolUndoTruncatedTypeNames{get;set;}}/// <summary>/// Abstract class that represents the serialization record./// </summary>/// <remarks>/// Every instance returned to the end user can be either <seealso cref="PrimitiveTypeRecord{T}"/>,/// a <seealso cref="ClassRecord"/> or an <seealso cref="ArrayRecord"/>./// </remarks>publicabstractclassSerializationRecord{internalSerializationRecord();// others can't derive from this type/// <summary>/// Gets the type of the record./// </summary>/// <value>The type of the record.</value>publicabstractRecordTypeRecordType{get;}/// <summary>/// Gets the ID of the record./// </summary>/// <value>The ID of the record.</value>publicabstractintObjectId{get;}/// <summary>/// Compares the type and assembly name read from the payload against the specified type./// </summary>/// <remarks>/// <para>This method takes type forwarding into account.</para>/// <para>This method does NOT take into account member names or their types.</para>/// </remarks>/// <param name="type">The type to compare against.</param>/// <returns><see langword="true" /> if the serialized type and assembly name match provided type; otherwise, <see langword="false" />.</returns>publicvirtualboolIsTypeNameMatching(Typetype);}/// <summary>/// Record type./// </summary>/// <remarks>/// <para>/// The enumeration does not contain all values supported by the <see href="https://learn.microsoft.com/openspecs/windows_protocols/ms-nrbf/954a0657-b901-4813-9398-4ec732fe8b32">/// [MS-NRBF] 2.1.2.1</see>, but only those supported by the <see cref="PayloadReader"/>./// </para>/// </remarks>publicenumRecordType:byte{SerializedStreamHeader,ClassWithId,// SystemClassWithMembers and ClassWithMembers are not supported by design (require type loading) and not includedSystemClassWithMembersAndTypes=4,ClassWithMembersAndTypes,BinaryObjectString,BinaryArray,MemberPrimitiveTyped,MemberReference,ObjectNull,MessageEnd,BinaryLibrary,ObjectNullMultiple256,ObjectNullMultiple,ArraySinglePrimitive,ArraySingleObject,ArraySingleString}/// <summary>/// Represents a record that itself represents the primitive value of <typeparamref name="T"/> type./// </summary>/// <typeparam name="T">The type of the primitive value.</typeparam>/// <remarks>/// <para>/// The NRBF specification considers the following types to be primitive:/// <see cref="string"/>, <see cref="bool"/>, <see cref="byte"/>, <see cref="sbyte"/>/// <see cref="char"/>, <see cref="short"/>, <see cref="ushort"/>,/// <see cref="int"/>, <see cref="uint"/>, <see cref="long"/>, <see cref="ulong"/>,/// <see cref="float"/>, <see cref="double"/>, <see cref="decimal"/>,/// <see cref="DateTime"/> and <see cref="TimeSpan"/>./// </para>/// <para>Other serialization records are represented with <see cref="ClassRecord"/> or <see cref="ArrayRecord"/>.</para>/// </remarks>publicabstractclassPrimitiveTypeRecord<T>:SerializationRecord{privateprotectedPrimitiveTypeRecord(Tvalue);publicTValue{get;}}/// <summary>/// Defines the core behavior for NRBF class records and provides a base for derived classes./// </summary>publicabstractclassClassRecord:SerializationRecord{privateprotectedClassRecord(ClassInfoclassInfo);publicTypeNameTypeName{get;}publicIEnumerable<string>MemberNames{get;}/// <summary>/// Checks if member of given name was present in the payload./// </summary>/// <param name="memberName">The name of the member.</param>/// <returns><see langword="true" /> if it was present, otherwise <see langword="false" />.</returns>/// <remarks>/// <para>/// It's recommended to use this method when dealing with payload that may contain/// different versions of the same type./// </para>/// </remarks>publicboolHasMember(stringmemberName);publicstring?GetString(stringmemberName);publicboolGetBoolean(stringmemberName);publicbyteGetByte(stringmemberName);publicsbyteGetSByte(stringmemberName);publicshortGetInt16(stringmemberName);publicushortGetUInt16(stringmemberName);publiccharGetChar(stringmemberName);publicintGetInt32(stringmemberName);publicuintGetUInt32(stringmemberName);publicfloatGetSingle(stringmemberName);publiclongGetInt64(stringmemberName);publiculongGetUInt64(stringmemberName);publicdoubleGetDouble(stringmemberName);publicdecimalGetDecimal(stringmemberName);publicTimeSpanGetTimeSpan(stringmemberName);publicDateTimeGetDateTime(stringmemberName);/// <summary>/// Retrieves an array for the provided <paramref name="memberName"/>./// </summary>/// <param name="memberName">The name of the field.</param>/// <param name="allowNulls">Specifies whether null values are allowed.</param>/// <returns>The array itself or null.</returns>/// <exception cref="KeyNotFoundException">Member of such name does not exist.</exception>/// <exception cref="InvalidOperationException">Member of such name has value of a different type.</exception>publicT?[]?GetArrayOfPrimitiveType<T>(stringmemberName,boolallowNulls=true);/// <summary>/// Retrieves the <see cref="SerializationRecord" /> of the provided <paramref name="memberName"/>./// </summary>/// <param name="memberName">The name of the field.</param>/// <returns>The serialization record, which can be any of <see cref="PrimitiveTypeRecord{T}"/>,/// <see cref="ClassRecord"/>, <see cref="ArrayRecord"/> or <see langword="null" />./// </returns>/// <exception cref="KeyNotFoundException"><paramref name="memberName" /> does not refer to a known member. You can use <see cref="HasMember(string)"/> to check if given member exists.</exception>/// <exception cref="InvalidOperationException">The specified member is not a <see cref="SerializationRecord"/>, but just a raw primitive value.</exception>publicSerializationRecord?GetSerializationRecord(stringmemberName);/// <summary>/// Retrieves the value of the provided <paramref name="memberName"/>./// </summary>/// <param name="memberName">The name of the member.</param>/// <returns>The value.</returns>/// <exception cref="KeyNotFoundException"><paramref name="memberName" /> does not refer to a known member. You can use <see cref="HasMember(string)"/> to check if given member exists.</exception>/// <exception cref="InvalidOperationException">Member of such name has value of a different type.</exception>publicClassRecord?GetClassRecord(stringmemberName);/// <returns>/// <para>For primitive types like <see cref="int"/>, <see langword="string"/> or <see cref="DateTime"/> returns their value.</para>/// <para>For nulls, returns a null.</para>/// <para>For other types that are not arrays, returns an instance of <see cref="ClassRecord"/>.</para>/// <para>For single-dimensional arrays returns <see cref="ArrayRecord{T}"/> where the generic type is the primitive type or <see cref="ClassRecord"/>.</para>/// <para>For jagged and multi-dimensional arrays, returns an instance of <see cref="ArrayRecord"/>.</para>/// </returns>publicobject?GetRawValue(stringmemberName);}/// <summary>/// Defines the core behavior for NRBF array records and provides a base for derived classes./// </summary>publicabstractclassArrayRecord:SerializationRecord{privateprotectedArrayRecord(ArrayInfoarrayInfo);/// <summary>/// When overridden in a derived class, gets a buffer of integers that represent the number of elements in every dimension./// </summary>/// <value>A buffer of integers that represent the number of elements in every dimension.</value>publicabstractReadOnlySpan<int>Lengths{get;}/// <summary>/// Gets the rank of the array./// </summary>/// <value>The rank of the array.</value>publicintRank{get;}/// <summary>/// Gets the type of the array./// </summary>/// <value>The type of the array.</value>publicBinaryArrayTypeArrayType{get;}/// <summary>/// Gets the name of the array element type./// </summary>/// <value>The name of the array element type.</value>publicabstractTypeNameElementTypeName{get;}/// <summary>/// Allocates an array and fills it with the data provided in the serialized records (in case of primitive types like <see cref="string"/> or <see cref="int"/>) or the serialized records themselves./// </summary>/// <param name="expectedArrayType">Expected array type.</param>/// <param name="allowNulls">/// <see langword="true" /> to permit <see langword="null" /> values within the array;/// otherwise, <see langword="false" />./// </param>/// <returns>An array filled with the data provided in the serialized records.</returns>/// <exception cref="InvalidOperationException"><paramref name="expectedArrayType" /> does not match the data from the payload.</exception>publicArrayGetArray(TypeexpectedArrayType,boolallowNulls=true);}/// <summary>/// Binary array type./// </summary>/// <remarks>/// BinaryArrayType enumeration is described in <see href="https://learn.microsoft.com/openspecs/windows_protocols/ms-nrbf/4dbbf3a8-6bc4-4dfc-aa7e-36a35be6ff58">[MS-NRBF] 2.4.1.1</see>./// </remarks>publicenumBinaryArrayType:byte{/// <summary>/// A single-dimensional array./// </summary>Single=0,/// <summary>/// An array whose elements are arrays. The elements of a jagged array can be of different dimensions and sizes./// </summary>Jagged=1,/// <summary>/// A multi-dimensional rectangular array./// </summary>Rectangular=2,/// <summary>/// A single-dimensional array where the lower bound index is greater than 0./// </summary>SingleOffset=3,/// <summary>/// A jagged array where the lower bound index is greater than 0./// </summary>JaggedOffset=4,/// <summary>/// Multi-dimensional arrays where the lower bound index of at least one of the dimensions is greater than 0./// </summary>RectangularOffset=5}/// <summary>/// Defines the core behavior for NRBF single dimensional, zero-indexed array records and provides a base for derived classes./// </summary>publicabstractclassArrayRecord<T>:ArrayRecord{privateprotectedArrayRecord(ArrayInfoarrayInfo);/// <summary>/// Gets the length of the array./// </summary>/// <value>The length of the array.</value>publicintLength{get;}/// <summary>/// When overridden in a derived class, allocates an array of <typeparamref name="T"/> and fills it with the data provided in the serialized records (in case of primitive types like <see cref="string"/> or <see cref="int"/>) or the serialized records themselves./// </summary>/// <param name="allowNulls">/// <see langword="true" /> to permit <see langword="null" /> values within the array;/// otherwise, <see langword="false" />./// </param>/// <returns>An array filled with the data provided in the serialized records.</returns>publicabstractT?[]GetArray(boolallowNulls=true);}

            Usage Examples

            The implementation with no dependency to dotnet/runtime can be found here.

            Reading a class serialized with BF to a file

            ClassRecordrootRecord=NrbfReader.ReadClassRecord(File.OpenRead("peristedPayload.bf"));Sampleoutput=new(){// using the dedicated methods to read primitive valuesInteger=rootRecord.GetInt32(nameof(Sample.Integer)),Text=rootRecord.GetString(nameof(Sample.Text)),// using dedicated method to read an array of bytesArrayOfBytes=rootRecord.GetArrayOfPrimitiveType<byte>(nameof(Sample.ArrayOfBytes)),// using GetClassRecord to read a class recordClassInstance=new(){Text=rootRecord.GetClassRecord(nameof(Sample.ClassInstance))!.GetString(nameof(Sample.Text))}};[Serializable]publicclassSample{publicintInteger;publicstring?Text;publicbyte[]?ArrayOfBytes;publicSample?ClassInstance;}

            Checking if Stream contains BF payload

            The users need to be able to check if given Stream contains BF data, as they might want to migrate the data on demand to new serialization format:

            staticTPseudocode<T>(Streampayload,NewSerializernewSerializer){if(NrbfReader.StartsWithPayloadHeader(payload)){TfromPayload=UseThePayloadReaderToReadTheData<T>(payload);payload.Seek(0,SeekOrigin.Begin);newSerializer.Serialize(payload,fromPayload);payload.Flush();}else{returnnewSerializer.Deserialize<T>(payload)}}

            SzArrays

            Single dimension, zero-indexed arrays are expected to be the most frequently used arrays.

            SerializationRecordrootObject=NrbfReader.Read(File.OpenRead("peristedPayload.bf"));if(rootObjectisArrayRecord<string>arrayOfStrings){string?[]strings=arrayOfStrings.GetArray();}

            Other arrays

            BF supports:

            • jagged arrays
            • multi-dimensional array
            • non-zero indexed arrays

            They are all represented by internal types that derive from ArrayRecord. The users can use the API to instantiate such arrays, but they need to provide the expected array type. By doing that we make this advanced scenario possible and safe (the library is not loading any types, if there is a type mismatch it throws).

            publicabstractclassArrayRecord:SerializationRecrd{publicArrayGetArray(TypeexpectedArrayType,boolallowNulls=true);}
            ArrayRecordarrayRecord=(ArrayRecord)NrbfReader.Read(File.OpenRead("peristedPayload.bf"));if(arrayRecord.ArrayType==ArrayType.Jagged){int[][][]array=(int[][][])arrayRecord.GetArray(expectedArrayType:typeof(int[][][]));}

            For more usages of this API please refer to JaggedArraysTests.cs, RectangularArraysTests.cs and CustomOffsetArrays.cs.

            Arrays of non-primitive types

            Arrays of non-primitive types are represented as ArrayRecord<ClassRecord> or just ArrayRecord.

            ArrayRecord<ClassRecord>rootRecord=(ArrayRecord<ClassRecord>)NrbfReader.Read(File.OpenRead("peristedPayload.bf"));ClassRecord[]classRecords=rootRecord.GetArray(allowNulls:false)!;Sample[]output=classRecords.Select(classRecord =>newSample(){Integer=classRecord.GetInt32(nameof(Sample.Integer)),Text=classRecord.GetString(nameof(Sample.Text))}).ToArray();

            Risks

            If the new APIs are not easy to use, some of the users might choose the new OOB package with a copy of BF and remain vulnerable to all attacks. This defeats the purpose of our initiative and must be avoided.

            Metadata

            Metadata

            Assignees

            Labels

            api-approvedAPI was approved in API review, it can be implementedarea-System.Formats.Nrbfbinaryformatter-migrationIssues related to the removal of BinaryFormatter and migrations away from itblockingMarks issues that we want to fast track in order to unblock other important work

            Type

            No type

            Projects

            No projects

              Milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              BinaryFormatter PayloadReader API #102014

              Description

              @adamsitnik

              Background and Motivation

              BinaryFormatter is getting removed in .NET 9, but our customers need to be able to read the payloads that:

              • were serialized with BF (using previous .NET versions) and persisted (to disk/db etc)
              • are being generated by software they have no control over (example: 3rd party clients calling an existing web service with public API that allows for BF).

              Our primary goal is to allow the users to read BF payloads in a secure manner from untrusted input. The principles:

              • Treating every input as potentially hostile.
              • No type loading of any kind (to avoid remote code execution).
              • No recursion of any kind (to avoid unbound recursion, stack overflow and denial of service).
              • No buffer pre-allocation based on size provided in payload (to avoid running out of memory and denial of service).
              • Using collision-resistant dictionary to store records referenced by other records.
              • Only primitive types can be instantiated in implicit way. Arrays can be instantiated on demand (with a default max size limit). Other types are never instantiated.

              We also want to make the APIs easy to use, to avoid the customers using the OOB package with the copy of BinaryFormatter (and remaining vulnerable to various attacks). That is why currently the public API surface is very narrow. We could expose more information, but we don't want to confuse the users or need them to become familiar with BF specification to get simple tasks done. Example: null can be represented using three different serialization records (ObjectNull, ObjectNullMultiple and ObjectNullMultiple256). The public APIs just return null, rather than a record that represents it.

              The new APIs need to be shipped in a new OOB package that supports older monikers, as we have first party customers running on Full Framework that are going to use it.

              Proposed API

              namespaceSystem.Runtime.Serialization.BinaryFormat;publicstaticclassNrbfReader{/// <summary>/// Checks if given buffer starts with <see href="https://learn.microsoft.com/openspecs/windows_protocols/ms-nrbf/a7e578d3-400a-4249-9424-7529d10d1b3c">NRBF payload header</see>./// </summary>/// <param name="bytes">The buffer to inspect.</param>/// <returns><see langword="true" /> if it starts with NRBF payload header; otherwise, <see langword="false" />.</returns>publicstaticboolStartsWithPayloadHeader(byte[]bytes);/// <summary>/// Checks if given stream starts with <see href="https://learn.microsoft.com/openspecs/windows_protocols/ms-nrbf/a7e578d3-400a-4249-9424-7529d10d1b3c">NRBF payload header</see>./// </summary>/// <param name="stream">The stream to inspect. The stream must be both readable and seekable.</param>/// <returns><see langword="true" /> if it starts with NRBF payload header; otherwise, <see langword="false" />.</returns>/// <exception cref="ArgumentNullException"><paramref name="stream" /> is <see langword="null" />.</exception>/// <exception cref="NotSupportedException">The stream does not support reading or seeking.</exception>/// <exception cref="ObjectDisposedException">The stream was closed.</exception>/// <remarks><para>When this method returns, <paramref name="stream" /> will be restored to its original position.</para></remarks>publicstaticboolStartsWithPayloadHeader(Streamstream);/// <summary>/// Reads the provided NRBF payload./// </summary>/// <param name="payload">The NRBF payload.</param>/// <param name="options">Options to control behavior during parsing.</param>/// <param name="leaveOpen">/// <see langword="true" /> to leave <paramref name="payload"/> payload open/// after the reading is finished; otherwise, <see langword="false" />./// </param>/// <returns>A <see cref="SerializationRecord"/> that represents the root object./// It can be either <see cref="PrimitiveTypeRecord{T}"/>,/// a <see cref="ClassRecord"/> or an <see cref="ArrayRecord"/>.</returns>/// <exception cref="ArgumentNullException"><paramref name="payload"/> is <see langword="null" />.</exception>/// <exception cref="ArgumentException"><paramref name="payload"/> does not support reading or is already closed.</exception>/// <exception cref="SerializationException">Reading from <paramref name="payload"/> encounters invalid NRBF data.</exception>/// <exception cref="DecoderFallbackException">Reading from <paramref name="payload"/>/// encounters an invalid UTF8 sequence.</exception>publicstaticSerializationRecordRead(Streampayload,PayloadOptions?options=default,boolleaveOpen=false);/// <param name="recordMap">/// When this method returns, contains a mapping of <see cref="SerializationRecord.ObjectId" /> to the associated serialization record./// This parameter is treated as uninitialized./// </param>publicstaticSerializationRecordRead(Streampayload,outIReadOnlyDictionary<int,SerializationRecord>recordMap,PayloadOptions?options=default,boolleaveOpen=false);/// <summary>/// Reads the provided Binary Format payload that is expected to contain an instance of any class (or struct) that is not an <seealso cref="Array"/> or a primitive type./// </summary>/// <returns>A <seealso cref="ClassRecord"/> that represents the root object.</returns>publicstaticClassRecordReadClassRecord(Streampayload,PayloadOptions?options=default,boolleaveOpen=false);}publicsealedclassPayloadOptions{publicPayloadOptions(){}publicTypeNameParseOptions?TypeNameParseOptions{get;set;}/// <summary>/// Gets or sets a value that indicates whether type name truncation is undone./// </summary>/// <value><see langword="true" /> if truncated type names should be reassembled; otherwise, <see langword="false" />.</value>/// <remarks>/// Example:/// TypeName: "Namespace.TypeName`1[[Namespace.GenericArgName"/// LibraryName: "AssemblyName]]"/// Is combined into "Namespace.TypeName`1[[Namespace.GenericArgName, AssemblyName]]"/// </remarks>publicboolUndoTruncatedTypeNames{get;set;}}/// <summary>/// Abstract class that represents the serialization record./// </summary>/// <remarks>/// Every instance returned to the end user can be either <seealso cref="PrimitiveTypeRecord{T}"/>,/// a <seealso cref="ClassRecord"/> or an <seealso cref="ArrayRecord"/>./// </remarks>publicabstractclassSerializationRecord{internalSerializationRecord();// others can't derive from this type/// <summary>/// Gets the type of the record./// </summary>/// <value>The type of the record.</value>publicabstractRecordTypeRecordType{get;}/// <summary>/// Gets the ID of the record./// </summary>/// <value>The ID of the record.</value>publicabstractintObjectId{get;}/// <summary>/// Compares the type and assembly name read from the payload against the specified type./// </summary>/// <remarks>/// <para>This method takes type forwarding into account.</para>/// <para>This method does NOT take into account member names or their types.</para>/// </remarks>/// <param name="type">The type to compare against.</param>/// <returns><see langword="true" /> if the serialized type and assembly name match provided type; otherwise, <see langword="false" />.</returns>publicvirtualboolIsTypeNameMatching(Typetype);}/// <summary>/// Record type./// </summary>/// <remarks>/// <para>/// The enumeration does not contain all values supported by the <see href="https://learn.microsoft.com/openspecs/windows_protocols/ms-nrbf/954a0657-b901-4813-9398-4ec732fe8b32">/// [MS-NRBF] 2.1.2.1</see>, but only those supported by the <see cref="PayloadReader"/>./// </para>/// </remarks>publicenumRecordType:byte{SerializedStreamHeader,ClassWithId,// SystemClassWithMembers and ClassWithMembers are not supported by design (require type loading) and not includedSystemClassWithMembersAndTypes=4,ClassWithMembersAndTypes,BinaryObjectString,BinaryArray,MemberPrimitiveTyped,MemberReference,ObjectNull,MessageEnd,BinaryLibrary,ObjectNullMultiple256,ObjectNullMultiple,ArraySinglePrimitive,ArraySingleObject,ArraySingleString}/// <summary>/// Represents a record that itself represents the primitive value of <typeparamref name="T"/> type./// </summary>/// <typeparam name="T">The type of the primitive value.</typeparam>/// <remarks>/// <para>/// The NRBF specification considers the following types to be primitive:/// <see cref="string"/>, <see cref="bool"/>, <see cref="byte"/>, <see cref="sbyte"/>/// <see cref="char"/>, <see cref="short"/>, <see cref="ushort"/>,/// <see cref="int"/>, <see cref="uint"/>, <see cref="long"/>, <see cref="ulong"/>,/// <see cref="float"/>, <see cref="double"/>, <see cref="decimal"/>,/// <see cref="DateTime"/> and <see cref="TimeSpan"/>./// </para>/// <para>Other serialization records are represented with <see cref="ClassRecord"/> or <see cref="ArrayRecord"/>.</para>/// </remarks>publicabstractclassPrimitiveTypeRecord<T>:SerializationRecord{privateprotectedPrimitiveTypeRecord(Tvalue);publicTValue{get;}}/// <summary>/// Defines the core behavior for NRBF class records and provides a base for derived classes./// </summary>publicabstractclassClassRecord:SerializationRecord{privateprotectedClassRecord(ClassInfoclassInfo);publicTypeNameTypeName{get;}publicIEnumerable<string>MemberNames{get;}/// <summary>/// Checks if member of given name was present in the payload./// </summary>/// <param name="memberName">The name of the member.</param>/// <returns><see langword="true" /> if it was present, otherwise <see langword="false" />.</returns>/// <remarks>/// <para>/// It's recommended to use this method when dealing with payload that may contain/// different versions of the same type./// </para>/// </remarks>publicboolHasMember(stringmemberName);publicstring?GetString(stringmemberName);publicboolGetBoolean(stringmemberName);publicbyteGetByte(stringmemberName);publicsbyteGetSByte(stringmemberName);publicshortGetInt16(stringmemberName);publicushortGetUInt16(stringmemberName);publiccharGetChar(stringmemberName);publicintGetInt32(stringmemberName);publicuintGetUInt32(stringmemberName);publicfloatGetSingle(stringmemberName);publiclongGetInt64(stringmemberName);publiculongGetUInt64(stringmemberName);publicdoubleGetDouble(stringmemberName);publicdecimalGetDecimal(stringmemberName);publicTimeSpanGetTimeSpan(stringmemberName);publicDateTimeGetDateTime(stringmemberName);/// <summary>/// Retrieves an array for the provided <paramref name="memberName"/>./// </summary>/// <param name="memberName">The name of the field.</param>/// <param name="allowNulls">Specifies whether null values are allowed.</param>/// <returns>The array itself or null.</returns>/// <exception cref="KeyNotFoundException">Member of such name does not exist.</exception>/// <exception cref="InvalidOperationException">Member of such name has value of a different type.</exception>publicT?[]?GetArrayOfPrimitiveType<T>(stringmemberName,boolallowNulls=true);/// <summary>/// Retrieves the <see cref="SerializationRecord" /> of the provided <paramref name="memberName"/>./// </summary>/// <param name="memberName">The name of the field.</param>/// <returns>The serialization record, which can be any of <see cref="PrimitiveTypeRecord{T}"/>,/// <see cref="ClassRecord"/>, <see cref="ArrayRecord"/> or <see langword="null" />./// </returns>/// <exception cref="KeyNotFoundException"><paramref name="memberName" /> does not refer to a known member. You can use <see cref="HasMember(string)"/> to check if given member exists.</exception>/// <exception cref="InvalidOperationException">The specified member is not a <see cref="SerializationRecord"/>, but just a raw primitive value.</exception>publicSerializationRecord?GetSerializationRecord(stringmemberName);/// <summary>/// Retrieves the value of the provided <paramref name="memberName"/>./// </summary>/// <param name="memberName">The name of the member.</param>/// <returns>The value.</returns>/// <exception cref="KeyNotFoundException"><paramref name="memberName" /> does not refer to a known member. You can use <see cref="HasMember(string)"/> to check if given member exists.</exception>/// <exception cref="InvalidOperationException">Member of such name has value of a different type.</exception>publicClassRecord?GetClassRecord(stringmemberName);/// <returns>/// <para>For primitive types like <see cref="int"/>, <see langword="string"/> or <see cref="DateTime"/> returns their value.</para>/// <para>For nulls, returns a null.</para>/// <para>For other types that are not arrays, returns an instance of <see cref="ClassRecord"/>.</para>/// <para>For single-dimensional arrays returns <see cref="ArrayRecord{T}"/> where the generic type is the primitive type or <see cref="ClassRecord"/>.</para>/// <para>For jagged and multi-dimensional arrays, returns an instance of <see cref="ArrayRecord"/>.</para>/// </returns>publicobject?GetRawValue(stringmemberName);}/// <summary>/// Defines the core behavior for NRBF array records and provides a base for derived classes./// </summary>publicabstractclassArrayRecord:SerializationRecord{privateprotectedArrayRecord(ArrayInfoarrayInfo);/// <summary>/// When overridden in a derived class, gets a buffer of integers that represent the number of elements in every dimension./// </summary>/// <value>A buffer of integers that represent the number of elements in every dimension.</value>publicabstractReadOnlySpan<int>Lengths{get;}/// <summary>/// Gets the rank of the array./// </summary>/// <value>The rank of the array.</value>publicintRank{get;}/// <summary>/// Gets the type of the array./// </summary>/// <value>The type of the array.</value>publicBinaryArrayTypeArrayType{get;}/// <summary>/// Gets the name of the array element type./// </summary>/// <value>The name of the array element type.</value>publicabstractTypeNameElementTypeName{get;}/// <summary>/// Allocates an array and fills it with the data provided in the serialized records (in case of primitive types like <see cref="string"/> or <see cref="int"/>) or the serialized records themselves./// </summary>/// <param name="expectedArrayType">Expected array type.</param>/// <param name="allowNulls">/// <see langword="true" /> to permit <see langword="null" /> values within the array;/// otherwise, <see langword="false" />./// </param>/// <returns>An array filled with the data provided in the serialized records.</returns>/// <exception cref="InvalidOperationException"><paramref name="expectedArrayType" /> does not match the data from the payload.</exception>publicArrayGetArray(TypeexpectedArrayType,boolallowNulls=true);}/// <summary>/// Binary array type./// </summary>/// <remarks>/// BinaryArrayType enumeration is described in <see href="https://learn.microsoft.com/openspecs/windows_protocols/ms-nrbf/4dbbf3a8-6bc4-4dfc-aa7e-36a35be6ff58">[MS-NRBF] 2.4.1.1</see>./// </remarks>publicenumBinaryArrayType:byte{/// <summary>/// A single-dimensional array./// </summary>Single=0,/// <summary>/// An array whose elements are arrays. The elements of a jagged array can be of different dimensions and sizes./// </summary>Jagged=1,/// <summary>/// A multi-dimensional rectangular array./// </summary>Rectangular=2,/// <summary>/// A single-dimensional array where the lower bound index is greater than 0./// </summary>SingleOffset=3,/// <summary>/// A jagged array where the lower bound index is greater than 0./// </summary>JaggedOffset=4,/// <summary>/// Multi-dimensional arrays where the lower bound index of at least one of the dimensions is greater than 0./// </summary>RectangularOffset=5}/// <summary>/// Defines the core behavior for NRBF single dimensional, zero-indexed array records and provides a base for derived classes./// </summary>publicabstractclassArrayRecord<T>:ArrayRecord{privateprotectedArrayRecord(ArrayInfoarrayInfo);/// <summary>/// Gets the length of the array./// </summary>/// <value>The length of the array.</value>publicintLength{get;}/// <summary>/// When overridden in a derived class, allocates an array of <typeparamref name="T"/> and fills it with the data provided in the serialized records (in case of primitive types like <see cref="string"/> or <see cref="int"/>) or the serialized records themselves./// </summary>/// <param name="allowNulls">/// <see langword="true" /> to permit <see langword="null" /> values within the array;/// otherwise, <see langword="false" />./// </param>/// <returns>An array filled with the data provided in the serialized records.</returns>publicabstractT?[]GetArray(boolallowNulls=true);}

              Usage Examples

              The implementation with no dependency to dotnet/runtime can be found here.

              Reading a class serialized with BF to a file

              ClassRecordrootRecord=NrbfReader.ReadClassRecord(File.OpenRead("peristedPayload.bf"));Sampleoutput=new(){// using the dedicated methods to read primitive valuesInteger=rootRecord.GetInt32(nameof(Sample.Integer)),Text=rootRecord.GetString(nameof(Sample.Text)),// using dedicated method to read an array of bytesArrayOfBytes=rootRecord.GetArrayOfPrimitiveType<byte>(nameof(Sample.ArrayOfBytes)),// using GetClassRecord to read a class recordClassInstance=new(){Text=rootRecord.GetClassRecord(nameof(Sample.ClassInstance))!.GetString(nameof(Sample.Text))}};[Serializable]publicclassSample{publicintInteger;publicstring?Text;publicbyte[]?ArrayOfBytes;publicSample?ClassInstance;}

              Checking if Stream contains BF payload

              The users need to be able to check if given Stream contains BF data, as they might want to migrate the data on demand to new serialization format:

              staticTPseudocode<T>(Streampayload,NewSerializernewSerializer){if(NrbfReader.StartsWithPayloadHeader(payload)){TfromPayload=UseThePayloadReaderToReadTheData<T>(payload);payload.Seek(0,SeekOrigin.Begin);newSerializer.Serialize(payload,fromPayload);payload.Flush();}else{returnnewSerializer.Deserialize<T>(payload)}}

              SzArrays

              Single dimension, zero-indexed arrays are expected to be the most frequently used arrays.

              SerializationRecordrootObject=NrbfReader.Read(File.OpenRead("peristedPayload.bf"));if(rootObjectisArrayRecord<string>arrayOfStrings){string?[]strings=arrayOfStrings.GetArray();}

              Other arrays

              BF supports:

              • jagged arrays
              • multi-dimensional array
              • non-zero indexed arrays

              They are all represented by internal types that derive from ArrayRecord. The users can use the API to instantiate such arrays, but they need to provide the expected array type. By doing that we make this advanced scenario possible and safe (the library is not loading any types, if there is a type mismatch it throws).

              publicabstractclassArrayRecord:SerializationRecrd{publicArrayGetArray(TypeexpectedArrayType,boolallowNulls=true);}
              ArrayRecordarrayRecord=(ArrayRecord)NrbfReader.Read(File.OpenRead("peristedPayload.bf"));if(arrayRecord.ArrayType==ArrayType.Jagged){int[][][]array=(int[][][])arrayRecord.GetArray(expectedArrayType:typeof(int[][][]));}

              For more usages of this API please refer to JaggedArraysTests.cs, RectangularArraysTests.cs and CustomOffsetArrays.cs.

              Arrays of non-primitive types

              Arrays of non-primitive types are represented as ArrayRecord<ClassRecord> or just ArrayRecord.

              ArrayRecord<ClassRecord>rootRecord=(ArrayRecord<ClassRecord>)NrbfReader.Read(File.OpenRead("peristedPayload.bf"));ClassRecord[]classRecords=rootRecord.GetArray(allowNulls:false)!;Sample[]output=classRecords.Select(classRecord =>newSample(){Integer=classRecord.GetInt32(nameof(Sample.Integer)),Text=classRecord.GetString(nameof(Sample.Text))}).ToArray();

              Risks

              If the new APIs are not easy to use, some of the users might choose the new OOB package with a copy of BF and remain vulnerable to all attacks. This defeats the purpose of our initiative and must be avoided.

              Metadata

              Metadata

              Assignees

              Labels

              api-approvedAPI was approved in API review, it can be implementedarea-System.Formats.Nrbfbinaryformatter-migrationIssues related to the removal of BinaryFormatter and migrations away from itblockingMarks issues that we want to fast track in order to unblock other important work

              Type

              No type

              Projects

              No projects

                Milestone

                Relationships

                None yet

                Development

                No branches or pull requests

                Issue actions

                , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                Skip to content

                BinaryFormatter PayloadReader API #102014

                Description

                @adamsitnik

                Background and Motivation

                BinaryFormatter is getting removed in .NET 9, but our customers need to be able to read the payloads that:

                • were serialized with BF (using previous .NET versions) and persisted (to disk/db etc)
                • are being generated by software they have no control over (example: 3rd party clients calling an existing web service with public API that allows for BF).

                Our primary goal is to allow the users to read BF payloads in a secure manner from untrusted input. The principles:

                • Treating every input as potentially hostile.
                • No type loading of any kind (to avoid remote code execution).
                • No recursion of any kind (to avoid unbound recursion, stack overflow and denial of service).
                • No buffer pre-allocation based on size provided in payload (to avoid running out of memory and denial of service).
                • Using collision-resistant dictionary to store records referenced by other records.
                • Only primitive types can be instantiated in implicit way. Arrays can be instantiated on demand (with a default max size limit). Other types are never instantiated.

                We also want to make the APIs easy to use, to avoid the customers using the OOB package with the copy of BinaryFormatter (and remaining vulnerable to various attacks). That is why currently the public API surface is very narrow. We could expose more information, but we don't want to confuse the users or need them to become familiar with BF specification to get simple tasks done. Example: null can be represented using three different serialization records (ObjectNull, ObjectNullMultiple and ObjectNullMultiple256). The public APIs just return null, rather than a record that represents it.

                The new APIs need to be shipped in a new OOB package that supports older monikers, as we have first party customers running on Full Framework that are going to use it.

                Proposed API

                namespaceSystem.Runtime.Serialization.BinaryFormat;publicstaticclassNrbfReader{/// <summary>/// Checks if given buffer starts with <see href="https://learn.microsoft.com/openspecs/windows_protocols/ms-nrbf/a7e578d3-400a-4249-9424-7529d10d1b3c">NRBF payload header</see>./// </summary>/// <param name="bytes">The buffer to inspect.</param>/// <returns><see langword="true" /> if it starts with NRBF payload header; otherwise, <see langword="false" />.</returns>publicstaticboolStartsWithPayloadHeader(byte[]bytes);/// <summary>/// Checks if given stream starts with <see href="https://learn.microsoft.com/openspecs/windows_protocols/ms-nrbf/a7e578d3-400a-4249-9424-7529d10d1b3c">NRBF payload header</see>./// </summary>/// <param name="stream">The stream to inspect. The stream must be both readable and seekable.</param>/// <returns><see langword="true" /> if it starts with NRBF payload header; otherwise, <see langword="false" />.</returns>/// <exception cref="ArgumentNullException"><paramref name="stream" /> is <see langword="null" />.</exception>/// <exception cref="NotSupportedException">The stream does not support reading or seeking.</exception>/// <exception cref="ObjectDisposedException">The stream was closed.</exception>/// <remarks><para>When this method returns, <paramref name="stream" /> will be restored to its original position.</para></remarks>publicstaticboolStartsWithPayloadHeader(Streamstream);/// <summary>/// Reads the provided NRBF payload./// </summary>/// <param name="payload">The NRBF payload.</param>/// <param name="options">Options to control behavior during parsing.</param>/// <param name="leaveOpen">/// <see langword="true" /> to leave <paramref name="payload"/> payload open/// after the reading is finished; otherwise, <see langword="false" />./// </param>/// <returns>A <see cref="SerializationRecord"/> that represents the root object./// It can be either <see cref="PrimitiveTypeRecord{T}"/>,/// a <see cref="ClassRecord"/> or an <see cref="ArrayRecord"/>.</returns>/// <exception cref="ArgumentNullException"><paramref name="payload"/> is <see langword="null" />.</exception>/// <exception cref="ArgumentException"><paramref name="payload"/> does not support reading or is already closed.</exception>/// <exception cref="SerializationException">Reading from <paramref name="payload"/> encounters invalid NRBF data.</exception>/// <exception cref="DecoderFallbackException">Reading from <paramref name="payload"/>/// encounters an invalid UTF8 sequence.</exception>publicstaticSerializationRecordRead(Streampayload,PayloadOptions?options=default,boolleaveOpen=false);/// <param name="recordMap">/// When this method returns, contains a mapping of <see cref="SerializationRecord.ObjectId" /> to the associated serialization record./// This parameter is treated as uninitialized./// </param>publicstaticSerializationRecordRead(Streampayload,outIReadOnlyDictionary<int,SerializationRecord>recordMap,PayloadOptions?options=default,boolleaveOpen=false);/// <summary>/// Reads the provided Binary Format payload that is expected to contain an instance of any class (or struct) that is not an <seealso cref="Array"/> or a primitive type./// </summary>/// <returns>A <seealso cref="ClassRecord"/> that represents the root object.</returns>publicstaticClassRecordReadClassRecord(Streampayload,PayloadOptions?options=default,boolleaveOpen=false);}publicsealedclassPayloadOptions{publicPayloadOptions(){}publicTypeNameParseOptions?TypeNameParseOptions{get;set;}/// <summary>/// Gets or sets a value that indicates whether type name truncation is undone./// </summary>/// <value><see langword="true" /> if truncated type names should be reassembled; otherwise, <see langword="false" />.</value>/// <remarks>/// Example:/// TypeName: "Namespace.TypeName`1[[Namespace.GenericArgName"/// LibraryName: "AssemblyName]]"/// Is combined into "Namespace.TypeName`1[[Namespace.GenericArgName, AssemblyName]]"/// </remarks>publicboolUndoTruncatedTypeNames{get;set;}}/// <summary>/// Abstract class that represents the serialization record./// </summary>/// <remarks>/// Every instance returned to the end user can be either <seealso cref="PrimitiveTypeRecord{T}"/>,/// a <seealso cref="ClassRecord"/> or an <seealso cref="ArrayRecord"/>./// </remarks>publicabstractclassSerializationRecord{internalSerializationRecord();// others can't derive from this type/// <summary>/// Gets the type of the record./// </summary>/// <value>The type of the record.</value>publicabstractRecordTypeRecordType{get;}/// <summary>/// Gets the ID of the record./// </summary>/// <value>The ID of the record.</value>publicabstractintObjectId{get;}/// <summary>/// Compares the type and assembly name read from the payload against the specified type./// </summary>/// <remarks>/// <para>This method takes type forwarding into account.</para>/// <para>This method does NOT take into account member names or their types.</para>/// </remarks>/// <param name="type">The type to compare against.</param>/// <returns><see langword="true" /> if the serialized type and assembly name match provided type; otherwise, <see langword="false" />.</returns>publicvirtualboolIsTypeNameMatching(Typetype);}/// <summary>/// Record type./// </summary>/// <remarks>/// <para>/// The enumeration does not contain all values supported by the <see href="https://learn.microsoft.com/openspecs/windows_protocols/ms-nrbf/954a0657-b901-4813-9398-4ec732fe8b32">/// [MS-NRBF] 2.1.2.1</see>, but only those supported by the <see cref="PayloadReader"/>./// </para>/// </remarks>publicenumRecordType:byte{SerializedStreamHeader,ClassWithId,// SystemClassWithMembers and ClassWithMembers are not supported by design (require type loading) and not includedSystemClassWithMembersAndTypes=4,ClassWithMembersAndTypes,BinaryObjectString,BinaryArray,MemberPrimitiveTyped,MemberReference,ObjectNull,MessageEnd,BinaryLibrary,ObjectNullMultiple256,ObjectNullMultiple,ArraySinglePrimitive,ArraySingleObject,ArraySingleString}/// <summary>/// Represents a record that itself represents the primitive value of <typeparamref name="T"/> type./// </summary>/// <typeparam name="T">The type of the primitive value.</typeparam>/// <remarks>/// <para>/// The NRBF specification considers the following types to be primitive:/// <see cref="string"/>, <see cref="bool"/>, <see cref="byte"/>, <see cref="sbyte"/>/// <see cref="char"/>, <see cref="short"/>, <see cref="ushort"/>,/// <see cref="int"/>, <see cref="uint"/>, <see cref="long"/>, <see cref="ulong"/>,/// <see cref="float"/>, <see cref="double"/>, <see cref="decimal"/>,/// <see cref="DateTime"/> and <see cref="TimeSpan"/>./// </para>/// <para>Other serialization records are represented with <see cref="ClassRecord"/> or <see cref="ArrayRecord"/>.</para>/// </remarks>publicabstractclassPrimitiveTypeRecord<T>:SerializationRecord{privateprotectedPrimitiveTypeRecord(Tvalue);publicTValue{get;}}/// <summary>/// Defines the core behavior for NRBF class records and provides a base for derived classes./// </summary>publicabstractclassClassRecord:SerializationRecord{privateprotectedClassRecord(ClassInfoclassInfo);publicTypeNameTypeName{get;}publicIEnumerable<string>MemberNames{get;}/// <summary>/// Checks if member of given name was present in the payload./// </summary>/// <param name="memberName">The name of the member.</param>/// <returns><see langword="true" /> if it was present, otherwise <see langword="false" />.</returns>/// <remarks>/// <para>/// It's recommended to use this method when dealing with payload that may contain/// different versions of the same type./// </para>/// </remarks>publicboolHasMember(stringmemberName);publicstring?GetString(stringmemberName);publicboolGetBoolean(stringmemberName);publicbyteGetByte(stringmemberName);publicsbyteGetSByte(stringmemberName);publicshortGetInt16(stringmemberName);publicushortGetUInt16(stringmemberName);publiccharGetChar(stringmemberName);publicintGetInt32(stringmemberName);publicuintGetUInt32(stringmemberName);publicfloatGetSingle(stringmemberName);publiclongGetInt64(stringmemberName);publiculongGetUInt64(stringmemberName);publicdoubleGetDouble(stringmemberName);publicdecimalGetDecimal(stringmemberName);publicTimeSpanGetTimeSpan(stringmemberName);publicDateTimeGetDateTime(stringmemberName);/// <summary>/// Retrieves an array for the provided <paramref name="memberName"/>./// </summary>/// <param name="memberName">The name of the field.</param>/// <param name="allowNulls">Specifies whether null values are allowed.</param>/// <returns>The array itself or null.</returns>/// <exception cref="KeyNotFoundException">Member of such name does not exist.</exception>/// <exception cref="InvalidOperationException">Member of such name has value of a different type.</exception>publicT?[]?GetArrayOfPrimitiveType<T>(stringmemberName,boolallowNulls=true);/// <summary>/// Retrieves the <see cref="SerializationRecord" /> of the provided <paramref name="memberName"/>./// </summary>/// <param name="memberName">The name of the field.</param>/// <returns>The serialization record, which can be any of <see cref="PrimitiveTypeRecord{T}"/>,/// <see cref="ClassRecord"/>, <see cref="ArrayRecord"/> or <see langword="null" />./// </returns>/// <exception cref="KeyNotFoundException"><paramref name="memberName" /> does not refer to a known member. You can use <see cref="HasMember(string)"/> to check if given member exists.</exception>/// <exception cref="InvalidOperationException">The specified member is not a <see cref="SerializationRecord"/>, but just a raw primitive value.</exception>publicSerializationRecord?GetSerializationRecord(stringmemberName);/// <summary>/// Retrieves the value of the provided <paramref name="memberName"/>./// </summary>/// <param name="memberName">The name of the member.</param>/// <returns>The value.</returns>/// <exception cref="KeyNotFoundException"><paramref name="memberName" /> does not refer to a known member. You can use <see cref="HasMember(string)"/> to check if given member exists.</exception>/// <exception cref="InvalidOperationException">Member of such name has value of a different type.</exception>publicClassRecord?GetClassRecord(stringmemberName);/// <returns>/// <para>For primitive types like <see cref="int"/>, <see langword="string"/> or <see cref="DateTime"/> returns their value.</para>/// <para>For nulls, returns a null.</para>/// <para>For other types that are not arrays, returns an instance of <see cref="ClassRecord"/>.</para>/// <para>For single-dimensional arrays returns <see cref="ArrayRecord{T}"/> where the generic type is the primitive type or <see cref="ClassRecord"/>.</para>/// <para>For jagged and multi-dimensional arrays, returns an instance of <see cref="ArrayRecord"/>.</para>/// </returns>publicobject?GetRawValue(stringmemberName);}/// <summary>/// Defines the core behavior for NRBF array records and provides a base for derived classes./// </summary>publicabstractclassArrayRecord:SerializationRecord{privateprotectedArrayRecord(ArrayInfoarrayInfo);/// <summary>/// When overridden in a derived class, gets a buffer of integers that represent the number of elements in every dimension./// </summary>/// <value>A buffer of integers that represent the number of elements in every dimension.</value>publicabstractReadOnlySpan<int>Lengths{get;}/// <summary>/// Gets the rank of the array./// </summary>/// <value>The rank of the array.</value>publicintRank{get;}/// <summary>/// Gets the type of the array./// </summary>/// <value>The type of the array.</value>publicBinaryArrayTypeArrayType{get;}/// <summary>/// Gets the name of the array element type./// </summary>/// <value>The name of the array element type.</value>publicabstractTypeNameElementTypeName{get;}/// <summary>/// Allocates an array and fills it with the data provided in the serialized records (in case of primitive types like <see cref="string"/> or <see cref="int"/>) or the serialized records themselves./// </summary>/// <param name="expectedArrayType">Expected array type.</param>/// <param name="allowNulls">/// <see langword="true" /> to permit <see langword="null" /> values within the array;/// otherwise, <see langword="false" />./// </param>/// <returns>An array filled with the data provided in the serialized records.</returns>/// <exception cref="InvalidOperationException"><paramref name="expectedArrayType" /> does not match the data from the payload.</exception>publicArrayGetArray(TypeexpectedArrayType,boolallowNulls=true);}/// <summary>/// Binary array type./// </summary>/// <remarks>/// BinaryArrayType enumeration is described in <see href="https://learn.microsoft.com/openspecs/windows_protocols/ms-nrbf/4dbbf3a8-6bc4-4dfc-aa7e-36a35be6ff58">[MS-NRBF] 2.4.1.1</see>./// </remarks>publicenumBinaryArrayType:byte{/// <summary>/// A single-dimensional array./// </summary>Single=0,/// <summary>/// An array whose elements are arrays. The elements of a jagged array can be of different dimensions and sizes./// </summary>Jagged=1,/// <summary>/// A multi-dimensional rectangular array./// </summary>Rectangular=2,/// <summary>/// A single-dimensional array where the lower bound index is greater than 0./// </summary>SingleOffset=3,/// <summary>/// A jagged array where the lower bound index is greater than 0./// </summary>JaggedOffset=4,/// <summary>/// Multi-dimensional arrays where the lower bound index of at least one of the dimensions is greater than 0./// </summary>RectangularOffset=5}/// <summary>/// Defines the core behavior for NRBF single dimensional, zero-indexed array records and provides a base for derived classes./// </summary>publicabstractclassArrayRecord<T>:ArrayRecord{privateprotectedArrayRecord(ArrayInfoarrayInfo);/// <summary>/// Gets the length of the array./// </summary>/// <value>The length of the array.</value>publicintLength{get;}/// <summary>/// When overridden in a derived class, allocates an array of <typeparamref name="T"/> and fills it with the data provided in the serialized records (in case of primitive types like <see cref="string"/> or <see cref="int"/>) or the serialized records themselves./// </summary>/// <param name="allowNulls">/// <see langword="true" /> to permit <see langword="null" /> values within the array;/// otherwise, <see langword="false" />./// </param>/// <returns>An array filled with the data provided in the serialized records.</returns>publicabstractT?[]GetArray(boolallowNulls=true);}

                Usage Examples

                The implementation with no dependency to dotnet/runtime can be found here.

                Reading a class serialized with BF to a file

                ClassRecordrootRecord=NrbfReader.ReadClassRecord(File.OpenRead("peristedPayload.bf"));Sampleoutput=new(){// using the dedicated methods to read primitive valuesInteger=rootRecord.GetInt32(nameof(Sample.Integer)),Text=rootRecord.GetString(nameof(Sample.Text)),// using dedicated method to read an array of bytesArrayOfBytes=rootRecord.GetArrayOfPrimitiveType<byte>(nameof(Sample.ArrayOfBytes)),// using GetClassRecord to read a class recordClassInstance=new(){Text=rootRecord.GetClassRecord(nameof(Sample.ClassInstance))!.GetString(nameof(Sample.Text))}};[Serializable]publicclassSample{publicintInteger;publicstring?Text;publicbyte[]?ArrayOfBytes;publicSample?ClassInstance;}

                Checking if Stream contains BF payload

                The users need to be able to check if given Stream contains BF data, as they might want to migrate the data on demand to new serialization format:

                staticTPseudocode<T>(Streampayload,NewSerializernewSerializer){if(NrbfReader.StartsWithPayloadHeader(payload)){TfromPayload=UseThePayloadReaderToReadTheData<T>(payload);payload.Seek(0,SeekOrigin.Begin);newSerializer.Serialize(payload,fromPayload);payload.Flush();}else{returnnewSerializer.Deserialize<T>(payload)}}

                SzArrays

                Single dimension, zero-indexed arrays are expected to be the most frequently used arrays.

                SerializationRecordrootObject=NrbfReader.Read(File.OpenRead("peristedPayload.bf"));if(rootObjectisArrayRecord<string>arrayOfStrings){string?[]strings=arrayOfStrings.GetArray();}

                Other arrays

                BF supports:

                • jagged arrays
                • multi-dimensional array
                • non-zero indexed arrays

                They are all represented by internal types that derive from ArrayRecord. The users can use the API to instantiate such arrays, but they need to provide the expected array type. By doing that we make this advanced scenario possible and safe (the library is not loading any types, if there is a type mismatch it throws).

                publicabstractclassArrayRecord:SerializationRecrd{publicArrayGetArray(TypeexpectedArrayType,boolallowNulls=true);}
                ArrayRecordarrayRecord=(ArrayRecord)NrbfReader.Read(File.OpenRead("peristedPayload.bf"));if(arrayRecord.ArrayType==ArrayType.Jagged){int[][][]array=(int[][][])arrayRecord.GetArray(expectedArrayType:typeof(int[][][]));}

                For more usages of this API please refer to JaggedArraysTests.cs, RectangularArraysTests.cs and CustomOffsetArrays.cs.

                Arrays of non-primitive types

                Arrays of non-primitive types are represented as ArrayRecord<ClassRecord> or just ArrayRecord.

                ArrayRecord<ClassRecord>rootRecord=(ArrayRecord<ClassRecord>)NrbfReader.Read(File.OpenRead("peristedPayload.bf"));ClassRecord[]classRecords=rootRecord.GetArray(allowNulls:false)!;Sample[]output=classRecords.Select(classRecord =>newSample(){Integer=classRecord.GetInt32(nameof(Sample.Integer)),Text=classRecord.GetString(nameof(Sample.Text))}).ToArray();

                Risks

                If the new APIs are not easy to use, some of the users might choose the new OOB package with a copy of BF and remain vulnerable to all attacks. This defeats the purpose of our initiative and must be avoided.

                Metadata

                Metadata

                Assignees

                Labels

                api-approvedAPI was approved in API review, it can be implementedarea-System.Formats.Nrbfbinaryformatter-migrationIssues related to the removal of BinaryFormatter and migrations away from itblockingMarks issues that we want to fast track in order to unblock other important work

                Type

                No type

                Projects

                No projects

                  Milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions