Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
3dc3f4b
test: add TDS response fuzz harness and end-to-end target
Aug 14, 2026
8a474c6
fix: bound TDS response allocations to prevent OOM from malformed str…
Aug 14, 2026
062a009
test: remove ineffectual assignment in bufFromBytes helper
Aug 14, 2026
bea21c1
test: address review feedback on TDS fuzz harness
Aug 14, 2026
153bd96
fix: panic StreamError for bounded TDS allocation guards
Aug 14, 2026
a2b636e
Merge remote-tracking branch 'origin/saurabh500-fuzz-tds-response-har…
Aug 14, 2026
8ccfd10
fix: bound sql_variant length to 8 KB instead of the LOB ceiling
Aug 14, 2026
a34ab7a
Merge remote-tracking branch 'origin/main' into saurabh500-fuzz-tds-r…
Aug 16, 2026
abb9475
test: assert malformed seeds error and valid seeds parse cleanly
Aug 16, 2026
6f66037
test: frame TDS fuzz packets by payload size for arbitrary seams
Aug 16, 2026
520380d
Merge remote-tracking branch 'origin/main' into saurabh500-fuzz-tds-r…
Aug 21, 2026
7484a27
Merge remote-tracking branch 'origin/saurabh500-fuzz-tds-response-har…
Aug 21, 2026
d559b62
fix: clarify TEXT/NTEXT/IMAGE invalid-length error message
Aug 21, 2026
a3e1f83
test: compare parsed token values across packet boundaries
Aug 21, 2026
ff9fd02
test: compare decoded ERROR contents across packet boundaries
Aug 21, 2026
18e8c32
fix: fail malformed FEDAUTHINFO/typeid streams as StreamError, guard …
Aug 21, 2026
320dbc2
test: correct trailing-bytes seed's documented intent
Aug 21, 2026
fc28416
Merge remote-tracking branch 'origin/saurabh500-fuzz-tds-response-har…
Aug 21, 2026
a2cb70c
test: harden alloc regression helpers per review feedback
Aug 21, 2026
199d3fb
test: verify INFO/ERROR message values across packet boundaries
Aug 21, 2026
dfee301
fix: say 'token' not 'packet' in FEDAUTHINFO stream errors; correct t…
Aug 21, 2026
6ce1f09
test: drain return-message queue concurrently to avoid harness hang
Aug 21, 2026
ff1bd98
fix: report FEDAUTHINFO opt offset and length separately in stream error
Aug 21, 2026
fd5f0a8
test: drop trailing-bytes seed from the valid boundary corpus
Aug 21, 2026
d7e2253
fix: stream TEXT/NTEXT/IMAGE reads to avoid preallocating from length…
Aug 21, 2026
6817aef
test: seed non-zero ERROR/INFO fields and non-empty browse tokens
Aug 21, 2026
e39b471
test: make TABNAME seed a spec-faithful TDS 7.2 name
Aug 21, 2026
70274e4
test: add non-final DONE + trailing garbage malformed seed
Aug 21, 2026
5932862
test: strengthen valid seeds with non-zero DONE/RETURNSTATUS and real…
Aug 21, 2026
26e16f2
Merge remote-tracking branch 'origin/main' into saurabh500-fuzz-tds-r…
Aug 21, 2026
79c012b
ci: retrigger AppVeyor after account-level build cancellation
Aug 22, 2026
e3164a6
Merge remote-tracking branch 'origin/saurabh500-fuzz-tds-response-har…
Aug 22, 2026
a56568b
Merge remote-tracking branch 'origin/main' into saurabh500-fuzz-tds-r…
Aug 22, 2026
1886f5a
Merge remote-tracking branch 'origin/saurabh500-fuzz-tds-response-har…
Aug 22, 2026
2bc5c60
Merge remote-tracking branch 'origin/main' into saurabh500-fuzz-tds-r…
Aug 22, 2026
326d4a8
test: widen fuzz fragmentation arg to uint16 for full seam coverage
Aug 22, 2026
95a2a46
Merge remote-tracking branch 'origin/saurabh500-fuzz-tds-response-har…
Aug 22, 2026
d2b7237
fix: bound COLMETADATA column count to prevent OOM from malformed str…
Aug 22, 2026
a8d58be
docs: explain why the COLMETADATA and FEDAUTHINFO allocation caps are…
Aug 23, 2026
ca438b0
Merge remote-tracking branch 'origin/main' into saurabh500-bound-tds-…
Aug 23, 2026
3b04116
refactor: grow COLMETADATA columns incrementally instead of capping c…
Aug 23, 2026
cf0ee5d
Merge remote-tracking branch 'origin/main' into saurabh500-bound-tds-…
Aug 24, 2026
cfa1c87
Merge remote-tracking branch 'origin/main' into saurabh500-bound-tds-…
Aug 28, 2026
82a076c
docs: clarify COLMETADATA growth and sql_variant bound comments
Aug 28, 2026
8223999
Merge remote-tracking branch 'origin/main' into saurabh500-bound-tds-…
Aug 31, 2026
977eae3
test: measure allocation in COLMETADATA bogus-count regression test
Sep 2, 2026
5ad6a3f
test: loosen COLMETADATA alloc ceiling to avoid parallel-test flakiness
Sep 2, 2026
8640697
test: cover FEDAUTHINFO dataOffset+dataLength uint32 overflow guard
Sep 3, 2026
0194132
Merge remote-tracking branch 'origin/main' into saurabh500-bound-tds-…
Sep 10, 2026
7b20dcc
Merge remote-tracking branch 'origin/main' into saurabh500-bound-tds-…
Sep 10, 2026
d0f5dec
fix: bound CEK table allocations and validate key value counts
Sep 11, 2026
30488dc
fix: defer value buffers and bound cumulative PLP payloads
Sep 11, 2026
b8067c0
chore: merge main with CI password generation fix
Sep 11, 2026
0f46c75
chore: merge main query-response draining fix
Sep 11, 2026
3d421ef
chore: merge main response-cleanup revert
Sep 23, 2026
42b08b2
chore: merge main v1.11.1 release update
Sep 23, 2026
22e913d
fix: validate sql_variant widths before reading values
Sep 23, 2026
2e0b392
refactor: use tagged switches for variant widths
Sep 23, 2026
c1af349
fix: reject odd Unicode variant lengths as stream errors
Sep 23, 2026
ca7e443
chore: merge main boolean connection option update
Sep 23, 2026
8b202f0
chore: merge main v1.11.2 release update
Sep 23, 2026
9afac4e
chore: merge main authentication connection aliases
Sep 23, 2026
bfee5b4
chore: merge main review compatibility guidance
Sep 24, 2026
70125bc
chore: merge main Codecov action update
Oct 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 66 additions & 19 deletions token.go
Original file line number Diff line number Diff line change
Expand Up @@ -500,8 +500,23 @@ type fedAuthInfoOpt struct {
dataLength, dataOffset uint32
}

// _MAX_FEDAUTHINFO_LEN bounds the total FEDAUTHINFO token size. The token
// carries only a STSURL and SPN: the STSURL is a login endpoint and the SPN a
// service principal name, both short URL/UPN-shaped strings encoded in UTF-16,
// so a few hundred bytes each is realistic and even a pathological pair stays
// well under 64 KiB. 1 MiB is therefore many times any legitimate token while
// still small enough that rejecting past it costs nothing; any larger advertised
// size is a malformed or hostile stream rather than something we should allocate
// for. The cap keeps an attacker-controlled length prefix from driving an
// unbounded allocation (OOM DoS, issue #420); a violation fails the stream as a
// StreamError.
const _MAX_FEDAUTHINFO_LEN = 1 << 20

func parseFedAuthInfo(r *tdsBuffer) fedAuthInfoStruct {
size := r.uint32()
if size > _MAX_FEDAUTHINFO_LEN {
badStreamPanic(fmt.Errorf("federated authentication info size %d exceeds maximum of %d bytes", size, _MAX_FEDAUTHINFO_LEN))
}
Comment thread
saurabh500 marked this conversation as resolved.

var STSURL, SPN string
var err error
Expand All @@ -510,6 +525,12 @@ func parseFedAuthInfo(r *tdsBuffer) fedAuthInfoStruct {
// then a four byte offset and a four byte length.
count := r.uint32()
offset := uint32(4)
// The option headers (9 bytes each) plus the trailing data must all fit
// within the advertised token size. Reject a count that cannot fit before
// allocating, so a bogus count cannot pre-allocate gigabytes of options.
if uint64(count)*9+uint64(offset) > uint64(size) {
badStreamPanic(fmt.Errorf("federated authentication info advertised %d options that do not fit in %d bytes", count, size))
}
opts := make([]fedAuthInfoOpt, count)

for i := uint32(0); i < count; i++ {
Expand All @@ -530,14 +551,16 @@ func parseFedAuthInfo(r *tdsBuffer) fedAuthInfoStruct {

for i := uint32(0); i < count; i++ {
if opts[i].dataOffset < offset {
badStreamPanicf("Fed auth info opt stated data offset %d is before data begins in packet at %d",
opts[i].dataOffset, offset)
badStreamPanic(fmt.Errorf("fed auth info opt stated data offset %d is before data begins in token at %d",
opts[i].dataOffset, offset))
Comment thread
saurabh500 marked this conversation as resolved.
// returns via panic
}

if opts[i].dataOffset+opts[i].dataLength > size {
badStreamPanicf("Fed auth info opt stated data length %d added to stated offset exceeds size of packet %d",
opts[i].dataOffset+opts[i].dataLength, size)
// Compute in uint64 so an attacker-controlled offset+length cannot
// overflow uint32 and slip past this bounds check (issue #420).
if uint64(opts[i].dataOffset)+uint64(opts[i].dataLength) > uint64(size) {
Comment thread
saurabh500 marked this conversation as resolved.
Comment thread
saurabh500 marked this conversation as resolved.
badStreamPanic(fmt.Errorf("fed auth info opt data offset %d plus length %d exceeds token size %d",
opts[i].dataOffset, opts[i].dataLength, size))
// returns via panic
}

Expand Down Expand Up @@ -653,15 +676,32 @@ func parseColMetadata72(r *tdsBuffer, s *tdsSession) (columns []columnStruct) {
// no metadata is sent
return nil
}
columns = make([]columnStruct, count)
var cekTable *cekTable
if s.alwaysEncrypted {
// column encryption key list
cekTable = readCekTable(r)
}

for i := range columns {
column := &columns[i]
// Grow the column slice as each column is actually parsed rather than
// preallocating make([]columnStruct, count). count is an attacker-controlled
// uint16 and columnStruct is large, so pre-sizing from the count alone lets a
// bogus value (up to 0xFFFE) commit many MiB up front before any of the
// backing bytes are read (OOM DoS, issue #420). The loop still iterates count
Comment thread
saurabh500 marked this conversation as resolved.
// times per protocol, but parsing each column consumes bytes from the stream,
// so a count that outruns the data fails via badStreamPanic (EOF) after only
// the columns actually present are read; the slice therefore never grows past
// what the server genuinely sent, no matter how large the declared count is.
// The capacity hint is bounded so the count cannot drive even the first
// allocation.
const initialColumnCap = 64
capHint := int(count)
if capHint > initialColumnCap {
capHint = initialColumnCap
}
columns = make([]columnStruct, 0, capHint)

for i := 0; i < int(count); i++ {
var column columnStruct
baseTi := getBaseTypeInfo(r, true)
typeInfo := readTypeInfo(r, baseTi.TypeId, column.cryptoMeta, s.encoding)
Comment thread
saurabh500 marked this conversation as resolved.
typeInfo.UserType = baseTi.UserType
Expand All @@ -682,6 +722,7 @@ func parseColMetadata72(r *tdsBuffer, s *tdsSession) (columns []columnStruct) {
}

column.ColName = r.BVarChar()
columns = append(columns, column)
}
return columns
}
Expand Down Expand Up @@ -763,48 +804,54 @@ func readCekTable(r *tdsBuffer) *cekTable {
var cekTable *cekTable = nil

if tableSize != 0 {
mCekTable := newCekTable(tableSize)
// Allocate entries only after parsing them, not from the wire count.
mCekTable := newCekTable(0)
for i := uint16(0); i < tableSize; i++ {
mCekTable.entries[i] = readCekTableEntry(r)
mCekTable.entries = append(mCekTable.entries, readCekTableEntry(r))
Comment thread
saurabh500 marked this conversation as resolved.
}
cekTable = &mCekTable
}

return cekTable
}

// SQL Server permits two encrypted values per CEK during master-key rotation.
// https://learn.microsoft.com/sql/t-sql/statements/alter-column-encryption-key-transact-sql
const _MAX_CEK_VALUES = 2

func readCekTableEntry(r *tdsBuffer) cekTableEntry {
databaseId := r.int32()
cekID := r.int32()
cekVersion := r.int32()
var cekMdVersion = make([]byte, 8)
_, err := r.Read(cekMdVersion)
if err != nil {
badStreamPanicf("unable to read cekMdVersion")
r.ReadFull(cekMdVersion)

cekValueCount := int(r.byte())
if cekValueCount > _MAX_CEK_VALUES {
badStreamPanic(fmt.Errorf("CEK value count %d exceeds maximum %d", cekValueCount, _MAX_CEK_VALUES))
}

cekValueCount := uint(r.byte())
// not using ucs22str because we already know the data is utf16
enc := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM)
utf16dec := enc.NewDecoder()
cekValues := make([]encryptionKeyInfo, cekValueCount)

for i := uint(0); i < cekValueCount; i++ {
for i := 0; i < cekValueCount; i++ {
encryptedCekLength := r.uint16()
encryptedCek := make([]byte, encryptedCekLength)
r.ReadFull(encryptedCek)

keyStoreLength := r.byte()
keyStoreLength := int(r.byte())
keyStoreNameUtf16 := make([]byte, keyStoreLength*2)
r.ReadFull(keyStoreNameUtf16)
keyStoreName, _ := utf16dec.Bytes(keyStoreNameUtf16)

keyPathLength := r.uint16()
keyPathLength := int(r.uint16())
keyPathUtf16 := make([]byte, keyPathLength*2)
r.ReadFull(keyPathUtf16)
keyPath, _ := utf16dec.Bytes(keyPathUtf16)

algLength := r.byte()
algLength := int(r.byte())
algNameUtf16 := make([]byte, algLength*2)
r.ReadFull(algNameUtf16)
algName, _ := utf16dec.Bytes(algNameUtf16)
Expand All @@ -826,7 +873,7 @@ func readCekTableEntry(r *tdsBuffer) cekTableEntry {
keyId: int(cekID),
keyVersion: int(cekVersion),
mdVersion: cekMdVersion,
valueCount: int(cekValueCount),
valueCount: cekValueCount,
cekValues: cekValues,
}
}
Expand Down
Loading
Loading