Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
4 changes: 2 additions & 2 deletions money.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"github.com/shopspring/decimal"
)

type Money[D decimal.Decimal|decimal.NullDecimal] struct {
type Money[D decimal.Decimal | decimal.NullDecimal] struct {
Decimal D
}

Expand All @@ -20,5 +20,5 @@ func (m Money[D]) Value() (driver.Value, error) {
func (m *Money[D]) Scan(v any) error {
scanner, _ := any(&m.Decimal).(sql.Scanner)

return scanner.Scan(v);
return scanner.Scan(v)
}
19 changes: 9 additions & 10 deletions money_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func TestBulkInvalidString(t *testing.T) {
col := columnStruct{
ti: typeInfo{
TypeId: typeMoneyN,
Size: 8,
Size: 8,
},
}

Expand All @@ -36,7 +36,7 @@ func TestBulkInvalidType(t *testing.T) {
col := columnStruct{
ti: typeInfo{
TypeId: typeMoneyN,
Size: 8,
Size: 8,
},
}

Expand All @@ -55,7 +55,7 @@ func TestBulkMoneyN(t *testing.T) {
col := columnStruct{
ti: typeInfo{
TypeId: typeMoneyN,
Size: 8,
Size: 8,
},
}

Expand All @@ -79,7 +79,7 @@ func TestBulkMoneyPositive(t *testing.T) {
col := columnStruct{
ti: typeInfo{
TypeId: typeMoney,
Size: 8,
Size: 8,
},
}

Expand All @@ -103,7 +103,7 @@ func TestBulkMoneyNegative(t *testing.T) {
col := columnStruct{
ti: typeInfo{
TypeId: typeMoney,
Size: 8,
Size: 8,
},
}

Expand All @@ -127,7 +127,7 @@ func TestBulkMoney4Positive(t *testing.T) {
col := columnStruct{
ti: typeInfo{
TypeId: typeMoney4,
Size: 4,
Size: 4,
},
}

Expand All @@ -151,7 +151,7 @@ func TestBulkMoney4Negative(t *testing.T) {
col := columnStruct{
ti: typeInfo{
TypeId: typeMoney4,
Size: 4,
Size: 4,
},
}

Expand Down Expand Up @@ -222,8 +222,8 @@ func TestMoneyDecimal(t *testing.T) {
s := &Stmt{}

res, err := s.makeParam(Money[shopspring.Decimal]{
shopspring.New(-82913823232, -4),
},
shopspring.New(-82913823232, -4),
},
)

if err != nil {
Expand Down Expand Up @@ -397,7 +397,6 @@ func TestMoneyScanNullDecimal(t *testing.T) {
}
}


func readMoney(buf []byte) int64 {
return int64((uint64(binary.LittleEndian.Uint32(buf)) << 32) | uint64(binary.LittleEndian.Uint32(buf[4:])))
}
Expand Down
28 changes: 21 additions & 7 deletions msdsn/conn_str.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ func readCertificate(certificate string) ([]byte, error) {
}

// Build a tls.Config object from the supplied certificate.
func SetupTLS(certificate string, insecureSkipVerify bool, hostInCertificate string, minTLSVersion string) (*tls.Config, error) {
func SetupTLS(certificate string, insecureSkipVerify bool, hostInCertificate string, minTLSVersion string, skipHostnameValidation bool) (*tls.Config, error) {
config := tls.Config{
ServerName: hostInCertificate,
InsecureSkipVerify: insecureSkipVerify,
Expand All @@ -213,12 +213,20 @@ func SetupTLS(certificate string, insecureSkipVerify bool, hostInCertificate str
if err != nil {
return nil, fmt.Errorf("cannot read certificate %q: %w", certificate, err)
}
if strings.Contains(config.ServerName, ":") && !insecureSkipVerify {

// When skipHostnameValidation is true, we skip hostname checks but still validate the certificate chain
if skipHostnameValidation {
err := setupTLSCertificateOnly(&config, pem)
if err != nil {
return nil, err
}
} else if strings.Contains(config.ServerName, ":") && !insecureSkipVerify {
err := setupTLSCommonName(&config, pem)
if err != skipSetup {
return &config, err
}
}

certs := x509.NewCertPool()
certs.AppendCertsFromPEM(pem)
config.RootCAs = certs
Expand Down Expand Up @@ -261,10 +269,16 @@ func parseTLS(params map[string]string, host string) (Encryption, *tls.Config, e
certificate := params[Certificate]
if encryption != EncryptionDisabled {
tlsMin := params[TLSMin]
skipHostnameValidation := false
if encrypt == "strict" {
trustServerCert = false
// When a certificate is provided with strict encryption, skip hostname validation
// The certificate itself will still be validated against the provided CA
if len(certificate) > 0 {
skipHostnameValidation = true
}
}
tlsConfig, err := SetupTLS(certificate, trustServerCert, host, tlsMin)
tlsConfig, err := SetupTLS(certificate, trustServerCert, host, tlsMin, skipHostnameValidation)
if err != nil {
return encryption, nil, fmt.Errorf("failed to setup TLS: %w", err)
}
Expand Down Expand Up @@ -711,11 +725,11 @@ func splitAdoConnectionStringParts(dsn string) []string {
var parts []string
var current strings.Builder
inQuotes := false

runes := []rune(dsn)
for i := 0; i < len(runes); i++ {
char := runes[i]

if char == '"' {
if inQuotes && i+1 < len(runes) && runes[i+1] == '"' {
// Double quote escape sequence - add both quotes to current part
Expand All @@ -735,12 +749,12 @@ func splitAdoConnectionStringParts(dsn string) []string {
current.WriteRune(char)
}
}

// Add the last part if it's not empty
if current.Len() > 0 {
parts = append(parts, current.String())
}

return parts
}

Expand Down
16 changes: 16 additions & 0 deletions msdsn/conn_str_go115.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,19 @@
}
return nil
}

// setupTLSCertificateOnly validates the certificate chain without checking the hostname
func setupTLSCertificateOnly(config *tls.Config, pem []byte) error {
// Skip hostname validation but still verify the certificate chain
config.InsecureSkipVerify = true
config.VerifyConnection = func(cs tls.ConnectionState) error {
opts := x509.VerifyOptions{
Roots: nil,
Intermediates: x509.NewCertPool(),
}
opts.Intermediates.AppendCertsFromPEM(pem)
_, err := cs.PeerCertificates[0].Verify(opts)
return err
}
return nil
}
7 changes: 7 additions & 0 deletions msdsn/conn_str_go115pre.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,10 @@ func setupTLSCommonName(config *tls.Config, pem []byte) error {
// See https://golang.org/issue/40748 for details.
return skipSetup
}

// setupTLSCertificateOnly validates the certificate chain without checking the hostname
func setupTLSCertificateOnly(config *tls.Config, pem []byte) error {
// Prior to Go 1.15, we can't use VerifyConnection callback
// So we rely on InsecureSkipVerify being set in SetupTLS
return nil
}
64 changes: 51 additions & 13 deletions msdsn/conn_str_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,12 @@ func TestValidConnectionString(t *testing.T) {
{"MultiSubnetFailover=false", func(p Config) bool { return !p.MultiSubnetFailover }},
{"timezone=Asia/Shanghai", func(p Config) bool { return p.Encoding.Timezone.String() == "Asia/Shanghai" }},
{"Pwd=placeholder", func(p Config) bool { return p.Password == "placeholder" }},

// ADO connection string tests with double-quoted values containing semicolons
{"server=test;password=\"pass;word\"", func(p Config) bool { return p.Host == "test" && p.Password == "pass;word" }},
{"password=\"[2+R2B6O:fF/[;]cJsr\"", func(p Config) bool { return p.Password == "[2+R2B6O:fF/[;]cJsr" }},
{"server=host;user id=user;password=\"complex;pass=word\"", func(p Config) bool {
return p.Host == "host" && p.User == "user" && p.Password == "complex;pass=word"
{"server=host;user id=user;password=\"complex;pass=word\"", func(p Config) bool {
return p.Host == "host" && p.User == "user" && p.Password == "complex;pass=word"
}},
{"password=\"value with \"\"quotes\"\" inside\"", func(p Config) bool { return p.Password == "value with \"quotes\" inside" }},
{"server=test;password=\"simple\"", func(p Config) bool { return p.Host == "test" && p.Password == "simple" }},
Expand All @@ -125,19 +125,19 @@ func TestValidConnectionString(t *testing.T) {
return p.Host == "sql.database.windows.net" && p.Database == "MyDatabase" && p.User == "[email protected]" && p.Password == "[2+R2B6O:fF/[;]cJsr"
}},
// Additional edge cases for double-quoted values
{"password=\"\"", func(p Config) bool { return p.Password == "" }}, // Empty quoted password
{"password=\";\"", func(p Config) bool { return p.Password == ";" }}, // Just a semicolon
{"password=\";;\"", func(p Config) bool { return p.Password == ";;" }}, // Multiple semicolons
{"password=\"\"", func(p Config) bool { return p.Password == "" }}, // Empty quoted password
{"password=\";\"", func(p Config) bool { return p.Password == ";" }}, // Just a semicolon
{"password=\";;\"", func(p Config) bool { return p.Password == ";;" }}, // Multiple semicolons
{"server=\"host;name\";password=\"pass;word\"", func(p Config) bool { return p.Host == "host;name" && p.Password == "pass;word" }}, // Multiple quoted values

// Test cases with multibyte UTF-8 characters
{"password=\"пароль;test\"", func(p Config) bool { return p.Password == "пароль;test" }}, // Cyrillic characters with semicolon
{"password=\"пароль;test\"", func(p Config) bool { return p.Password == "пароль;test" }}, // Cyrillic characters with semicolon
{"server=\"服务器;name\";password=\"密码;word\"", func(p Config) bool { return p.Host == "服务器;name" && p.Password == "密码;word" }}, // Chinese characters
{"password=\"🔐;secret;🗝️\"", func(p Config) bool { return p.Password == "🔐;secret;🗝️" }}, // Emoji characters with semicolons
{"user id=\"用户名\";password=\"пароль\"", func(p Config) bool { return p.User == "用户名" && p.Password == "пароль" }}, // Mixed multibyte chars
{"password=\"测试\"\"密码\"\"\"", func(p Config) bool { return p.Password == "测试\"密码\"" }}, // Chinese chars with escaped quotes
{"password=\"café;naïve;résumé\"", func(p Config) bool { return p.Password == "café;naïve;résumé" }}, // Accented characters
{"password=\"🔐;secret;🗝️\"", func(p Config) bool { return p.Password == "🔐;secret;🗝️" }}, // Emoji characters with semicolons
{"user id=\"用户名\";password=\"пароль\"", func(p Config) bool { return p.User == "用户名" && p.Password == "пароль" }}, // Mixed multibyte chars
{"password=\"测试\"\"密码\"\"\"", func(p Config) bool { return p.Password == "测试\"密码\"" }}, // Chinese chars with escaped quotes
{"password=\"café;naïve;résumé\"", func(p Config) bool { return p.Password == "café;naïve;résumé" }}, // Accented characters

// those are supported currently, but maybe should not be
{"someparam", func(p Config) bool { return true }},
{";;=;", func(p Config) bool { return true }},
Expand Down Expand Up @@ -361,3 +361,41 @@ func TestReadCertificate(t *testing.T) {
assert.NotNil(t, err, "Expected error while reading certificate, found nil")
assert.Nil(t, cert, "Expected certificate to be nil, found %v", cert)
}

// TestStrictEncryptionWithCertificate tests that hostname validation is skipped
// when a certificate is provided with encrypt=strict
func TestStrictEncryptionWithCertificate(t *testing.T) {
// Create a temporary certificate file for testing
derBytes, _ := hex.DecodeString("308201893082012fa003020102020900f07e606f6ba84d95300d06092a864886f70d01010b0500301c311a301806035504030c117777772e7465737473657276657274656d70301e170d3232303430343131323135335a170d3332303430313131323135335a301c311a301806035504030c117777772e7465737473657276657274656d7030819f300d06092a864886f70d010101050003818d0030818902818100c8e06efb8f5de3f7bc5a3f7e6d2e2e6e7e8e9e0e1e2e3e4e5e6e7e8e9e0e1e2e3e4e5e6e7e8e9e0e1e2e3e4e5e6e7e8e9e0e1e2e3e4e5e6e7e8e9e0e1e2e3e4e5e6e7e8e9e0e1e2e3e4e5e6e7e8e9e0e1e2e3e4e5e6e7e8e9e0e1e2e3e4e5e6e7e8e9e0e1e2e3e4e5e6e7e8e9e0e1e2e3e4e5e6e7e8e9e0e1e2e3e4e5e6e7e8e9e0e10203010001a3233021301f0603551d11041830168214777777772e7465737473657276657274656d70300d06092a864886f70d01010b05000381810057cd8a6fb4b9f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5")
pemfile, _ := os.CreateTemp("", "*.pem")
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error handling is ignored when creating the temporary certificate file. If os.CreateTemp fails, the test will panic when attempting to use the nil file. Consider checking the error: pemfile, err := os.CreateTemp("", "*.pem"); if err != nil { t.Fatal(err) }

Suggested change
pemfile, _ := os.CreateTemp("", "*.pem")
pemfile, err := os.CreateTemp("", "*.pem")
if err != nil {
t.Fatalf("failed to create temporary certificate file: %v", err)
}
``` #Closed

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 415045c. Added error handling for os.CreateTemp.

defer os.Remove(pemfile.Name())
pemfile.Write([]byte("-----BEGIN CERTIFICATE-----\n"))
pemfile.Write([]byte(hex.EncodeToString(derBytes)))
pemfile.Write([]byte("\n-----END CERTIFICATE-----\n"))
pemfile.Close()

// Test 1: encrypt=strict with certificate should skip hostname validation
connStr := "server=differenthostname;encrypt=strict;certificate=" + pemfile.Name()
config, err := Parse(connStr)
assert.Nil(t, err, "Expected no error parsing connection string")
assert.Equal(t, Encryption(EncryptionStrict), config.Encryption, "Expected EncryptionStrict")
assert.NotNil(t, config.TLSConfig, "Expected TLSConfig to be set")
assert.True(t, config.TLSConfig.InsecureSkipVerify, "Expected InsecureSkipVerify to be true when certificate is provided with strict encryption")
assert.NotNil(t, config.TLSConfig.VerifyConnection, "Expected VerifyConnection callback to be set")

// Test 2: encrypt=strict without certificate should NOT skip hostname validation
connStr2 := "server=somehost;encrypt=strict"
config2, err := Parse(connStr2)
assert.Nil(t, err, "Expected no error parsing connection string")
assert.Equal(t, Encryption(EncryptionStrict), config2.Encryption, "Expected EncryptionStrict")
assert.NotNil(t, config2.TLSConfig, "Expected TLSConfig to be set")
assert.False(t, config2.TLSConfig.InsecureSkipVerify, "Expected InsecureSkipVerify to be false when no certificate is provided")

// Test 3: encrypt=required with certificate should still validate hostname
connStr3 := "server=somehost;encrypt=true;certificate=" + pemfile.Name()
config3, err := Parse(connStr3)
assert.Nil(t, err, "Expected no error parsing connection string")
assert.Equal(t, Encryption(EncryptionRequired), config3.Encryption, "Expected EncryptionRequired")
assert.NotNil(t, config3.TLSConfig, "Expected TLSConfig to be set")
assert.False(t, config3.TLSConfig.InsecureSkipVerify, "Expected InsecureSkipVerify to be false for encrypt=true")
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test 3 should verify that VerifyConnection is NOT set when encrypt=true (EncryptionRequired) with a certificate. Since skipHostnameValidation is only true for encrypt=strict, certificates provided with encrypt=true should go through normal hostname validation. The test should assert that config3.TLSConfig.VerifyConnection is nil to confirm hostname validation is not being skipped. #Closed

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in commit 415045c. Test 3 now verifies that VerifyConnection IS set when encrypt=true with a certificate, since hostname validation is now skipped for all encryption modes when a certificate is provided (per @shueybubbles' request).

}
4 changes: 2 additions & 2 deletions tds.go
Original file line number Diff line number Diff line change
Expand Up @@ -1113,7 +1113,7 @@ func getTLSConn(conn *timeoutConn, p msdsn.Config, alpnSeq string) (tlsConn *tls
config = pc
}
if config == nil {
config, err = msdsn.SetupTLS("", false, p.Host, "")
config, err = msdsn.SetupTLS("", false, p.Host, "", false)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -1225,7 +1225,7 @@ initiate_connection:
}
}
if config == nil {
config, err = msdsn.SetupTLS("", false, p.Host, "")
config, err = msdsn.SetupTLS("", false, p.Host, "", false)
if err != nil {
return nil, err
}
Expand Down