Repository navigation
Validate PRELOGIN option offsets before reading option data - #4902
priyankatiwari08 merged 4 commits into
Conversation
ConsumePreLoginHandshake read the VERSION, ENCRYPT, INSTANCE, MARS and FEDAUTHREQUIRED option headers and data without checking that they lie within the received PRELOGIN response. A malformed response could cause an IndexOutOfRangeException to escape from SqlConnection.Open/OpenAsync. Read each option header through a helper that verifies the 4-byte header and the option data it points to are within the response, and report a CorruptedTdsStream parsing error otherwise. Responses that were previously accepted are unaffected. Add simulated-server tests covering out-of-range offsets, data truncated at the end of the response, and truncated option headers for each option, on both sync and async open paths, plus a well-formed control. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7ef2b899-d3ce-43cc-91d5-a321d447d1d4
There was a problem hiding this comment.
🔵 Needs a closer look
Current-head CI remains unverified, and the reported full .NET Framework simulated-server run is incomplete, leaving validation for human confirmation.
No open issues 🎉
What changed in this PR
This PR hardens SqlClient’s PRELOGIN parsing so malformed option headers or data offsets produce an existing connection parsing error instead of IndexOutOfRangeException.
Changes:
- Adds shared bounds validation for VERSION, ENCRYPT, INSTANCE, MARS, and FEDAUTHREQUIRED.
- Preserves existing handling of declared option lengths, skipped options, and TDS 8 encryption.
- Adds 32 simulated-server cases covering valid responses and malformed headers or offsets through both
OpenandOpenAsync.
| File | Description |
|---|---|
| src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/PreLoginOptionBoundsTests.cs | Adds sync/async PRELOGIN bounds regression tests and a valid-response control. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs | Validates option headers and consumed data before reading them. |
Review effort: Balanced
Options the client reads must declare exactly their defined data length (VERSION 6 bytes; ENCRYPT, INSTANCE, MARS and FEDAUTHREQUIRED 1 byte), so a short option can no longer be read together with the bytes of the option that follows it. Every other option header (THREADID, TRACEID, unknown options and ENCRYPT under TDS 8) is now validated as well: the header and the declared data must lie within the response. Rename the helper parameter to expectedBytes and add tests for length mismatches, a short option overlapping the next option's data, and bounds violations in options the client does not read. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7ef2b899-d3ce-43cc-91d5-a321d447d1d4
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4902 +/- ##
==========================================
- Coverage 67.68% 66.74% -0.95%
==========================================
Files 311 305 -6
Lines 45249 68458 +23209
==========================================
+ Hits 30629 45695 +15066
- Misses 14620 22763 +8143
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:
|
mdaigle
left a comment
There was a problem hiding this comment.
Some code organization nits.
- Read and validate each option header once, before dispatching on the option, using ReadPreLoginOptionHeader, which returns the data offset and length instead of advancing a ref offset. - Treat THREADID, TRACEID and unrecognized options in a single branch that skips the option after its header is validated; drop Debug.Fail. - Require the option tokens, headers, data and terminator to describe exactly the bytes of the response, rejecting undescribed bytes, data claimed by more than one option without a gap, and a missing terminator. - Make the test TDS server write ENCRYPT and TRACEID data only when it writes the matching option header. - Add tests for unknown options and for the total-size check. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7ef2b899-d3ce-43cc-91d5-a321d447d1d4
Description
TdsParser.ConsumePreLoginHandshakereads the option header (2-byte offset + 2-byte length) and the option data forVERSION,ENCRYPT,INSTANCE,MARSandFEDAUTHREQUIREDwithout checking that they fall inside the received PRELOGIN response. If a server returns a malformed PRELOGIN response, anIndexOutOfRangeExceptionescapes fromSqlConnection.Open/OpenAsyncinstead of a connection error.This change:
Adds
ReadPreLoginOptionHeader(preLoginResponse, headerOffset, int? expectedBytes), which validates an option header and returns the option's data offset and length:expectedBytesis given, the declared length equals it (VERSION = 6 bytes for UL_VERSION + US_SUBBUILD; ENCRYPT, INSTANCE, MARS and FEDAUTHREQUIRED = 1 byte). This stops a short option from being read together with the bytes of the next option;dataOffset + dataLength) lies within the response.Failures throw the existing
ParsingErrorState.CorruptedTdsStreamparsing error (Internal connection fatal error. Error state: 18, Offset: Nor..., Length: N) and write a trace event. This matches how other PRELOGIN/TDS validation failures are reported, e.g.FedAuthRequiredPreLoginResponseInvalidValue.Reads and validates every option header once, before dispatching on the option: exact length for the options the client reads, and bounds-only (
expectedBytes: null) forTHREADID,TRACEIDand unknown options, which share one branch that skips the option (the previousDebug.Failfor unknown options is removed).ENCRYPTunder TDS 8 is validated with its 1-byte length before the value is ignored.Adds a total-size check, as ODBC/OLE DB do: each option contributes its token, 4-byte header and declared data length, the terminator contributes 1 byte, and the total must equal the response length. This rejects bytes that no option describes, data claimed by more than one option (when there is no compensating gap), and a response with no terminator.
Test TDS server:
TDSPreLoginToken.Deflatenow writes the ENCRYPT and TRACEID data only when it writes the matching option header (it previously wrote an encryption byte even when the ENCRYPT option was omitted).Compatibility: Responses whose option data falls outside the response, whose read options declare a non-standard length, or whose options do not describe exactly the bytes of the response, are now rejected with a parsing error. The required lengths are the ones defined by MS-TDS and already enforced by ODBC/OLE DB (JDBC enforces them for VERSION and ENCRYPT); SQL Server 2022 and 2025 responses were verified to connect (see Testing). No public API changes.
Comparison with other Microsoft SQL Server drivers
The checks added here match how the other Microsoft drivers already parse the PRELOGIN response:
ipInBuf + sizeof(PRELOGINOPTHDR) > cbPayload)responseIndex + 4 >= responseLength)offset + 4 > payload.Length)iOffSet + 6 > cbPayloadfor VERSION,iOffSet >= cbPayloadfor the others)optionOffset + optionLength > responseLength)payloadOffset + payloadLength > payload.Length)cbOptDataTotal != cbPayload)With this change SqlClient enforces the same declared-length and total-size rules as ODBC/OLE DB (a superset of the JDBC checks) and, like JDBC, also bounds-checks the headers and data of options it does not read.
Suggested release note (Fixed): Fixed
SqlConnection.Open/OpenAsyncthrowingIndexOutOfRangeExceptioninstead of a connection error when the server's PRELOGIN response contains option offsets or lengths that are invalid or outside the response.Issues
N/A
Testing
Added
SimulatedServerTests/PreLoginOptionBoundsTests(85 cases). It uses aTdsServersubclass that replaces the PRELOGIN response with hand-built bytes. Cases run on both syncOpenand asyncOpenAsync:0xFFFF: parsing error, state 18.Length: N.Length: 2.Length: N.Results:
dotnet test src\Microsoft.Data.SqlClient\tests\UnitTests\Microsoft.Data.SqlClient.UnitTests.csproj -f net10.0 --filter "FullyQualifiedName~PreLoginOptionBoundsTests": Passed (85/85).IndexOutOfRangeException.dotnet test ... -f net10.0 --filter "FullyQualifiedName~SimulatedServerTests": Passed (576/576, after merging latestmain).dotnet test src\Microsoft.Data.SqlClient\tests\UnitTests\Microsoft.Data.SqlClient.UnitTests.csproj -f net10.0(full suite, after the total-size check): Passed (2062 passed, 9 skipped, 0 failed).dotnet test src\Microsoft.Data.SqlClient\tests\FunctionalTests\Microsoft.Data.SqlClient.FunctionalTests.csproj -f net10.0: 1364 passed, 5 failed; the 5 failures are environment-specific on the Windows dev box (Unix-only Always Encrypted provider tests, certificate store paths, AttachDbFileName on Linux) and unrelated to PRELOGIN.dotnet test ... -f net462 --filter "FullyQualifiedName~PreLoginOptionBoundsTests": Passed (85/85).Encrypt=OptionalandEncrypt=Mandatory, MARS on and off, sync and async open: all 32 combinations connected.Encrypt=Strictcould not be exercised locally (TCP is not enabled on these instances); CI covers it.Guidelines
Please review the contribution guidelines before submitting a pull request: