Skip to content

Commit 54a2817

Browse files
committed
Address Copilot review feedback
- ReturnValue: report actual valLen via unchecked((int)valLen) instead of int.MaxValue for more actionable error diagnostics - TryReadSqlDateTime: use TdsEnums.MaxDateTimeLength constant instead of local const for consistency with other bounds checks - SqlConnectionInternal: use ParsingErrorLength for session-recovery check to include the offending length in the error - TdsEnums: broaden MaxTokenDataLength comment scope (not login-only), add MaxDateTimeLength constant - FeatureExtAckBoundsTests: rewrite boundary test to inject token at exactly MaxTokenDataLength and assert bounds check does not fire - TdsTokenBoundsTests: fix DateTime test comment (heap, not stack)
1 parent 3bb0e4c commit 54a2817

5 files changed

Lines changed: 41 additions & 33 deletions

File tree

src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1348,7 +1348,7 @@ internal void OnFeatureExtAck(int featureId, byte[] data)
13481348

13491349
if (len < 0 || len > data.Length - i)
13501350
{
1351-
throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream);
1351+
throw SQL.ParsingErrorLength(ParsingErrorState.CorruptedTdsStream, len);
13521352
}
13531353

13541354
byte[] stateData = new byte[len];

src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,17 @@ internal static class TdsEnums
8484
public const int MAX_PACKET_SIZE = 32768;
8585
public const int MAX_SERVER_USER_NAME = 256; // obtained from luxor
8686

87-
// Maximum allowed data length for login-phase token payloads (feature ext ack,
87+
// Maximum allowed data length for token payloads (feature ext ack,
8888
// session state, fedauth info). Prevents a malicious server from causing
89-
// unbounded memory allocation on the client before authentication completes.
89+
// unbounded memory allocation via spoofed token length fields.
9090
internal const int MaxTokenDataLength = 1 << 20; // 1 MB
9191

9292
// Maximum allowed data length for a DTC promote transaction propagation token.
9393
internal const int MaxPromoteTransactionLength = 1 << 16; // 64 KB
9494

95+
// Maximum valid wire size for datetime types (DateTimeOffset = 5 time + 3 date + 2 offset).
96+
internal const int MaxDateTimeLength = 10;
97+
9598
// Severity 0 - 10 indicates informational (non-error) messages
9699
// Severity 11 - 16 indicates errors that can be corrected by user (syntax errors, etc...)
97100
// Severity 17 - 19 indicates failure due to insufficient resources in the server

src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4940,7 +4940,7 @@ internal TdsOperationStatus TryProcessReturnValue(int length,
49404940
}
49414941
else if (valLen > (ulong)int.MaxValue)
49424942
{
4943-
throw SQL.ParsingErrorLength(ParsingErrorState.CorruptedTdsStream, int.MaxValue);
4943+
throw SQL.ParsingErrorLength(ParsingErrorState.CorruptedTdsStream, unchecked((int)valLen));
49444944
}
49454945
else
49464946
{
@@ -7141,13 +7141,12 @@ private TdsOperationStatus TryReadSqlDateTime(SqlBuffer value, byte tdsType, int
71417141
{
71427142
// DateTimeOffset is the largest datetime type at 10 bytes (5 time + 3 date + 2 offset).
71437143
// Reject anything larger to prevent heap allocation from spoofed metadata.
7144-
const int MaxDateTimeLength = 10;
7145-
if (length < 0 || length > MaxDateTimeLength)
7144+
if (length < 0 || length > TdsEnums.MaxDateTimeLength)
71467145
{
71477146
throw SQL.ParsingErrorLength(ParsingErrorState.CorruptedTdsStream, length);
71487147
}
71497148

7150-
Span<byte> datetimeBuffer = stackalloc byte[MaxDateTimeLength];
7149+
Span<byte> datetimeBuffer = stackalloc byte[TdsEnums.MaxDateTimeLength];
71517150

71527151
TdsOperationStatus result = stateObj.TryReadByteArray(datetimeBuffer, length);
71537152
if (result != TdsOperationStatus.Done)

src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/FeatureExtAckBoundsTests.cs

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -78,44 +78,50 @@ public void FeatureExtAck_OversizedDataLength_ThrowsParsingError()
7878
}
7979

8080
/// <summary>
81-
/// Verifies that a FeatureExtAck token with a data length at or below the
81+
/// Verifies that a FeatureExtAck token with a data length at exactly the
8282
/// allowed maximum is accepted without error, confirming there is no
8383
/// off-by-one in the bounds check.
8484
/// </summary>
8585
[Fact]
8686
public void FeatureExtAck_MaxAllowedDataLength_DoesNotThrow()
8787
{
88-
// Arrange: inject a FeatureExtAck token at exactly the maximum allowed size.
89-
// We use a small real payload to avoid actually allocating 1 MB in the test;
90-
// the server will write a data length that equals the actual data length.
91-
// This verifies that the boundary value is accepted (not off-by-one).
92-
byte[] legitimateData = new byte[] { 0x01 }; // 1-byte GlobalTransactions ack
93-
88+
// Arrange: inject a FeatureExtAck token whose declared data length equals
89+
// MaxTokenDataLength exactly. The bounds check should NOT fire for this
90+
// value. The connection will fail for other reasons (not enough data on
91+
// the wire), but the error must NOT be state 18 (CorruptedTdsStream).
9492
_server.OnAuthenticationResponseCompleted = responseMessage =>
9593
{
96-
// Find existing FeatureExtAck or create one
9794
var existing = responseMessage.OfType<TDSFeatureExtAckToken>().FirstOrDefault();
98-
if (existing == null)
95+
if (existing != null)
9996
{
100-
int doneIndex = responseMessage.FindIndex(t => t is TDSDoneToken);
101-
if (doneIndex < 0)
102-
{
103-
doneIndex = responseMessage.Count;
104-
}
105-
106-
var token = new TDSFeatureExtAckToken(
107-
new TDSFeatureExtAckGenericOption(
108-
(TDSFeatureID)TdsEnums.FEATUREEXT_GLOBALTRANSACTIONS,
109-
(uint)legitimateData.Length,
110-
legitimateData));
111-
responseMessage.Insert(doneIndex, token);
97+
responseMessage.Remove(existing);
98+
}
99+
100+
int doneIndex = responseMessage.FindIndex(t => t is TDSDoneToken);
101+
if (doneIndex < 0)
102+
{
103+
doneIndex = responseMessage.Count;
112104
}
105+
106+
// Insert token with dataLen = MaxTokenDataLength (at boundary, not over)
107+
responseMessage.Insert(doneIndex, new MaliciousFeatureExtAckToken(
108+
featureId: (TDSFeatureID)TdsEnums.FEATUREEXT_GLOBALTRANSACTIONS,
109+
claimedDataLen: (uint)TdsEnums.MaxTokenDataLength));
113110
};
114111

115-
// Act & Assert: connection should open successfully
116-
using SqlConnection connection = new(_connectionString);
117-
connection.Open();
118-
Assert.Equal(System.Data.ConnectionState.Open, connection.State);
112+
try
113+
{
114+
using SqlConnection connection = new(_connectionString);
115+
// The connection will fail (insufficient data for the claimed length),
116+
// but the failure must NOT be the bounds check (state 18 with MaxTokenDataLength+1).
117+
Exception ex = Assert.ThrowsAny<Exception>(connection.Open);
118+
// Confirm it's NOT the bounds-check error (which would report MaxTokenDataLength+1)
119+
Assert.DoesNotContain((TdsEnums.MaxTokenDataLength + 1).ToString(), ex.Message);
120+
}
121+
finally
122+
{
123+
_server.OnAuthenticationResponseCompleted = null;
124+
}
119125
}
120126

121127
/// <summary>

src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/TdsTokenBoundsTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -507,7 +507,7 @@ public void BatchResponse_PromoteTransaction_OversizedLength_ThrowsParsingError(
507507
/// Verifies that <c>TryReadSqlDateTime</c> rejects a TIME column value whose
508508
/// data length byte exceeds the maximum datetime wire size (10 bytes). A
509509
/// malicious server could set this length to a large value, causing the
510-
/// parser to attempt an oversized stackalloc.
510+
/// parser to attempt an unbounded heap allocation.
511511
/// </summary>
512512
[Fact]
513513
public void BatchResponse_DateTime_OversizedLength_ThrowsParsingError()

0 commit comments

Comments
 (0)