Add TDS token data length bounds checks#4340
Conversation
There was a problem hiding this comment.
Pull request overview
This PR hardens the TDS parser against pre-auth (and post-auth) denial-of-service vectors by adding upper bounds checks on token/data lengths read from the wire, preventing unbounded allocations when a server spoofs lengths.
Changes:
- Added parser-side bounds checks for multiple TDS tokens/fields (SessionState, FedAuthInfo, FeatureExtAck, EnvChange PromoteTransaction, DateTime payloads, ReturnValue non‑PLP, Vector).
- Introduced new protocol max constants in
TdsEnumsand expanded simulated-server infrastructure to inject malicious tokens in both login and batch responses. - Added new simulated-server unit tests covering the new rejection paths and a PLP regression guard.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/TdsTokenBoundsTests.cs | New simulated-server tests covering several oversized-length scenarios (login + post-login injection). |
| src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/FeatureExtAckBoundsTests.cs | New tests for FeatureExtAck oversized length handling and boundary acceptance. |
| src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServer.cs | Adds a post-login SQL batch hook to allow response token injection in tests. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs | Implements the core bounds checks in the TDS parser and fixes PLP/non‑PLP handling in ReturnValue. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs | Adds new max-length constants used by the parser checks. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs | Adds bounds validation for SRECOVERY inner state option parsing in FeatureExtAck handling. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #4340 +/- ##
==========================================
- Coverage 66.50% 64.84% -1.67%
==========================================
Files 285 280 -5
Lines 43311 66191 +22880
==========================================
+ Hits 28806 42923 +14117
- Misses 14505 23268 +8763
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The binary bounds check in TryReadSqlValueInternal is only reachable through the sql_variant deserialization path (TryReadSqlVariant). For non-variant binary columns, TryReadSqlValue handles them directly without calling TryReadSqlValueInternal. This test injects a sql_variant column containing a BigVarBinary value whose inner data length (8001) exceeds MAXSIZE (8000), verifying the bounds check fires and prevents unbounded heap allocation. Addresses the 2 uncovered lines flagged by Codecov in PR #4340.
paulmedynski
left a comment
There was a problem hiding this comment.
Self-review: Test coverage annotations
Each bounds check added to driver code has been annotated with the specific test(s) that exercise it. Summary:
| Driver change | Test(s) |
|---|---|
TdsEnums.cs — MaxTokenDataLength, MaxPromoteTransactionLength, MaxDateTimeLength |
All 12 tests validate these thresholds |
TdsParser.cs — PromoteTransaction bounds check (L3351) |
EnvChange_PromoteTransaction_OversizedLength_ThrowsParsingError, BatchResponse_PromoteTransaction_OversizedLength_ThrowsParsingError |
TdsParser.cs — FeatureExtAck bounds check (L3853) |
FeatureExtAck_OversizedDataLength_ThrowsParsingError, FeatureExtAck_MaxAllowedDataLength_DoesNotThrow |
TdsParser.cs — SessionState total length (L4178) |
SessionState_OversizedTotalLength_ThrowsParsingError |
TdsParser.cs — SessionState inner option (L4234) |
SessionState_OversizedInnerOptionLength_ThrowsParsingError |
TdsParser.cs — FedAuthInfo token length (L4455) |
FedAuthInfo_OversizedTokenLength_ThrowsParsingError |
TdsParser.cs — ReturnValue non-PLP length (L4936) |
BatchResponse_ReturnValue_OversizedLength_ThrowsParsingError, BatchResponse_ReturnValue_PlpUnknownLength_Succeeds |
TdsParser.cs — DateTime length (L7143) |
BatchResponse_DateTime_OversizedLength_ThrowsParsingError |
TdsParser.cs — Binary/Vector sql_variant (L7405) |
BatchResponse_SqlVariantBinary_OversizedLength_ThrowsParsingError |
SqlConnectionInternal.cs — SRECOVERY inner state (L1349) |
SRecovery_MalformedInnerStateLength_ThrowsParsingError |
All 12 tests pass in ~19s.
Prevent unbounded memory allocation from malicious TDS servers by adding runtime bounds checks on token data length fields before heap allocation. Changes: - Add MaxTokenDataLength (1 MB) and MaxPromoteTransactionLength (64 KB) constants to TdsEnums.cs - Add bounds checks in TryProcessFeatureExtAck, TryProcessSessionState, TryProcessFedAuthInfo, ENV_PROMOTETRANSACTION, and TryReadSqlValueInternal - Add bounds check in SqlConnectionInternal SRECOVERY secondary parse - Promote Debug.Assert to runtime checks in TryProcessReturnValue and TryReadSqlValueInternal (binary types), remove now-redundant asserts - Replace conditional heap allocation in TryReadSqlDateTime with fixed stackalloc guarded by a length check Tests: - FeatureExtAckBoundsTests: oversized FeatureExtAck data length, positive boundary test - TdsTokenBoundsTests: oversized SessionState total/inner length, malformed SRECOVERY inner state, oversized FedAuthInfo token length
Inject a malicious PromoteTransaction environment change token (type 15) into the login response with a fraudulently large int32 newLength field. The bounds check in TryProcessEnvChange rejects it before allocation. No simulated server extension needed — env change tokens are part of the standard login response and can be injected via OnAuthenticationResponseCompleted.
Summary
Adds bounds checks on data lengths read from the TDS wire to prevent unbounded
byte[]allocation from a malicious server.All checks throw
SQL.ParsingErrorLength(ParsingErrorState.CorruptedTdsStream, ...)when a spoofed length exceeds protocol-reasonable maximums.Changes
TdsParser.csL4180TryProcessSessionState— total length checktokenLen > MaxTokenDataLength(1 MB)SessionState_OversizedTotalLengthTdsParser.csL4233TryProcessSessionState— inner option length checkstateLen < 0orstateLen > MaxTokenDataLengthSessionState_OversizedInnerOptionLength,SessionState_NegativeInnerOptionLengthTdsParser.csL4454TryProcessFedAuthInfo— token length checktokenLen > MaxTokenDataLengthFedAuthInfo_OversizedTokenLengthTdsParser.csL3351TryProcessEnvChange— PromoteTransaction newLength checknewLength > MaxPromoteTransactionLength(64 KB)EnvChange_PromoteTransaction_OversizedLength(login),BatchResponse_PromoteTransaction_OversizedLength(post-login)TdsParser.csL3853TryProcessFeatureExtAck— data length checkdataLen > MaxTokenDataLengthFeatureExtAck_OversizedDataLength,FeatureExtAck_MaxAllowedDataLength_DoesNotThrowTdsParser.csL7144TryReadSqlDateTime— data length checklength > MaxDateTimeLength(10)BatchResponse_DateTime_OversizedLengthTdsParser.csL4937-4948TryProcessReturnValue— non-PLP length checkIsPlptypes skip bounds check; non-PLP rejectsvalLen > int.MaxValueBatchResponse_ReturnValue_OversizedLength,BatchResponse_ReturnValue_PlpUnknownLengthTdsParser.csL7405TryReadSqlValueInternal— binary/vector length checklength < 0orlength > MAXSIZE(8000) for binary types in sql_variant pathBatchResponse_SqlVariantBinary_OversizedLength,BatchResponse_SqlVariantBinary_NegativeLengthSqlConnectionInternal.csL1349OnFeatureExtAck— SRECOVERY inner state lengthSRecovery_MalformedInnerStateLengthTdsEnums.csL90-96MaxTokenDataLength(1 MB),MaxPromoteTransactionLength(64 KB),MaxDateTimeLength(10)Impact analysis
Group 1: New upper-bound constraints
These add hard limits where previously the parser would attempt to allocate whatever the server claimed. A conforming SQL Server will never exceed any of these limits, but a misbehaving proxy, network appliance, or third-party TDS implementation could.
dataLentokenLennewLengthlengthlenDataresults from a total variant length smaller than the type overheadRisk to existing apps: Effectively zero against real SQL Server. Could break connections through a non-conforming TDS proxy that inflates token lengths (e.g., for padding or encapsulation).
Group 2: Debug.Assert → runtime exception (bug fix)
These replaced
Debug.Assert(which is stripped in Release builds, meaning the invalid state was silently accepted) with a throwing check:valLen > int.MaxValueint.MaxValueParsingErrorLengthlength > MAXSIZEParsingErrorLengthRisk to existing apps: Only if the app was previously "surviving" corrupt data by silently reading garbage. In practice these indicate a corrupted stream that would likely cause downstream failures anyway.
Group 3: Buffer overrun prevention (defense-in-depth)
SqlConnectionInternal.OnFeatureExtAckSRECOVERY:len < 0 || len > data.Length - iBuffer.BlockCopyfrom a malformed feature ack payloadRisk to existing apps: None — this path only fires if the server sends internally inconsistent SRECOVERY data (claimed inner length exceeds the outer buffer already validated by the FeatureExtAck check).
Group 4: Behavioral improvement (non-breaking)
TryReadSqlDateTime((uint)length <= 16) ? stackalloc : new byte[length]with unconditionalstackalloc byte[10]Since the bounds check guarantees
length ≤ 10, the conditional heap allocation path is dead code eliminated. Minor perf win. No user-visible behavior change.Impact summary
The only scenario where a real application could be affected is if it connects through a non-conforming TDS intermediary (proxy/load balancer) that rewrites token lengths to values exceeding these bounds. Standard SQL Server, Azure SQL, and conforming TDS 7.x/8.0 implementations will never produce values exceeding any of these limits.
Test infrastructure
OnSQLBatchCompletedhook toGenericTdsServerfor post-login TDS token injectionOnAuthenticationResponseCompletedhook usage for login-phase token injectionIDisposable) for proper test isolation — each test gets a freshTdsServerFixtureChecklist