@@ -821,6 +821,112 @@ public override void Deflate(Stream destination)
821821 }
822822 }
823823
824+ // ──────────────────────────────────────────────────────────────────────────
825+ // Test 10: Post-login batch response — sql_variant with oversized binary
826+ // ──────────────────────────────────────────────────────────────────────────
827+
828+ /// <summary>
829+ /// Verifies that <c>TryReadSqlValueInternal</c> rejects a binary value inside
830+ /// a sql_variant column whose inner data length exceeds
831+ /// <see cref="TdsEnums.MAXSIZE"/> (8000 bytes). The bounds check in the
832+ /// sql_variant deserialization path prevents unbounded heap allocation.
833+ /// </summary>
834+ [ Fact ]
835+ public void BatchResponse_SqlVariantBinary_OversizedLength_ThrowsParsingError ( )
836+ {
837+ _server . OnSQLBatchCompleted = responseMessage =>
838+ {
839+ responseMessage . Clear ( ) ;
840+
841+ // COLMETADATA: one SSVariant column
842+ var metadata = new TDSColMetadataToken ( ) ;
843+ var col = new TDSColumnData ( ) ;
844+ col . DataType = TDSDataType . SSVariant ;
845+ col . DataTypeSpecific = ( uint ) 8009 ; // max length for SSVariant
846+ col . Flags . IsNullable = true ;
847+ col . Name = string . Empty ;
848+ metadata . Columns . Add ( col ) ;
849+ responseMessage . Add ( metadata ) ;
850+
851+ // ROW with a sql_variant containing oversized binary data
852+ responseMessage . Add ( new MaliciousSqlVariantBinaryRowToken ( ) ) ;
853+
854+ // DONE
855+ responseMessage . Add ( new TDSDoneToken ( TDSDoneTokenStatusType . Final | TDSDoneTokenStatusType . Count , TDSDoneTokenCommandType . Select , 1 ) ) ;
856+ } ;
857+
858+ try
859+ {
860+ using SqlConnection connection = new ( _connectionString ) ;
861+ connection . Open ( ) ;
862+
863+ using SqlCommand command = connection . CreateCommand ( ) ;
864+ command . CommandText = "MALICIOUS_QUERY_NOT_RECOGNIZED" ;
865+
866+ SqlDataReader reader = command . ExecuteReader ( ) ;
867+ try
868+ {
869+ Assert . True ( reader . Read ( ) ) ;
870+ Exception ex = Assert . ThrowsAny < InvalidOperationException > (
871+ ( ) => reader . GetValue ( 0 ) ) ;
872+ Assert . Contains ( "18" , ex . Message ) ; // CorruptedTdsStream
873+ }
874+ finally
875+ {
876+ using ( new DebugAssertSuppressor ( ) )
877+ {
878+ try { reader . Dispose ( ) ; } catch { }
879+ }
880+ }
881+ }
882+ finally
883+ {
884+ _server . OnSQLBatchCompleted = null ;
885+ }
886+ }
887+
888+ /// <summary>
889+ /// Writes a ROW token (0xD1) with a single SSVariant column containing a
890+ /// BigVarBinary variant whose inner data length exceeds MAXSIZE (8000).
891+ /// Wire layout for the variant:
892+ /// [int32] total variant length = 8005
893+ /// [byte] inner type = 0xA5 (BigVarBinary)
894+ /// [byte] cbPropBytes = 2
895+ /// [ushort] maxLen (property) = 8001
896+ /// [8001 bytes would be data, but we only write 4 to trigger the check]
897+ /// lenData = 8005 - 2(SQLVARIANT_SIZE) - 2(cbProps) = 8001 > MAXSIZE → throws
898+ /// </summary>
899+ private sealed class MaliciousSqlVariantBinaryRowToken : TDSPacketToken
900+ {
901+ public override bool Inflate ( Stream source ) => throw new NotSupportedException ( ) ;
902+
903+ public override void Deflate ( Stream destination )
904+ {
905+ // ROW token type
906+ destination . WriteByte ( 0xD1 ) ;
907+
908+ // SSVariant column data: total length (int32 LE)
909+ // lenData = totalLength - SQLVARIANT_SIZE(2) - cbPropBytes(2) = totalLength - 4
910+ // We want lenData = 8001, so totalLength = 8005
911+ int totalLength = 8005 ;
912+ byte [ ] lenBytes = BitConverter . GetBytes ( totalLength ) ;
913+ destination . Write ( lenBytes , 0 , 4 ) ;
914+
915+ // Inner type: BigVarBinary = 0xA5
916+ destination . WriteByte ( 0xA5 ) ;
917+
918+ // cbPropBytes = 2
919+ destination . WriteByte ( 0x02 ) ;
920+
921+ // Properties: maxLen (ushort) = 8001
922+ destination . WriteByte ( 0x41 ) ; // 8001 & 0xFF = 0x41
923+ destination . WriteByte ( 0x1F ) ; // 8001 >> 8 = 0x1F
924+
925+ // Write 4 bytes of dummy data (bounds check fires before trying to read 8001)
926+ destination . Write ( new byte [ 4 ] , 0 , 4 ) ;
927+ }
928+ }
929+
824930 /// <summary>
825931 /// Temporarily suppresses Debug.Assert failures by clearing trace listeners.
826932 /// Used when disposing resources after intentionally corrupting a TDS stream.
0 commit comments