diff --git a/CHANGELOG.md b/CHANGELOG.md index aae34facd..a2b75c64e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +* Masked the sensitive credential data in the connection string (DSN, data source name) from error messages for security reasons + ## v3.121.0 * Changed internal pprof label to pyroscope supported format * Added `query.ImplicitTxControl()` transaction control (the same as `query.NoTx()` and `query.EmptyTxControl()`). See more about implicit transactions on [ydb.tech](https://ydb.tech/docs/en/concepts/transactions?version=v25.2#implicit) diff --git a/driver.go b/driver.go index e838f53aa..6fec52c89 100644 --- a/driver.go +++ b/driver.go @@ -31,6 +31,7 @@ import ( schemeConfig "github.com/ydb-platform/ydb-go-sdk/v3/internal/scheme/config" internalScripting "github.com/ydb-platform/ydb-go-sdk/v3/internal/scripting" scriptingConfig "github.com/ydb-platform/ydb-go-sdk/v3/internal/scripting/config" + "github.com/ydb-platform/ydb-go-sdk/v3/internal/secret" "github.com/ydb-platform/ydb-go-sdk/v3/internal/stack" internalTable "github.com/ydb-platform/ydb-go-sdk/v3/internal/table" tableConfig "github.com/ydb-platform/ydb-go-sdk/v3/internal/table/config" @@ -290,7 +291,7 @@ func Open(ctx context.Context, dsn string, opts ...Option) (_ *Driver, _ error) if parser := dsnParsers[parserIdx]; parser != nil { optsFromParser, err := parser(dsn) if err != nil { - return nil, xerrors.WithStackTrace(fmt.Errorf("data source name '%s' wrong: %w", dsn, err)) + return nil, xerrors.WithStackTrace(fmt.Errorf("data source name '%s' wrong: %w", secret.DSN(dsn), err)) } opts = append(opts, optsFromParser...) } diff --git a/driver_string_test.go b/driver_string_test.go index eda19532b..072267907 100644 --- a/driver_string_test.go +++ b/driver_string_test.go @@ -52,7 +52,7 @@ func TestDriver_String(t *testing.T) { config.WithSecure(true), config.WithCredentials(credentials.NewStaticCredentials("user", "password", "")), )}, - s: `Driver{Endpoint:"localhost",Database:"local",Secure:true,Credentials:Static{User:"user",Password:"pas***rd",Token:"****(CRC-32c: 00000000)",From:"github.com/ydb-platform/ydb-go-sdk/v3/credentials.NewStaticCredentials(credentials.go:35)"}}`, //nolint:lll + s: `Driver{Endpoint:"localhost",Database:"local",Secure:true,Credentials:Static{User:"user",Password:"p******d",Token:"****(CRC-32c: 00000000)",From:"github.com/ydb-platform/ydb-go-sdk/v3/credentials.NewStaticCredentials(credentials.go:35)"}}`, //nolint:lll }, { name: xtest.CurrentFileLine(), diff --git a/internal/credentials/errors_test.go b/internal/credentials/errors_test.go index 0971ab177..cfe48f2b1 100644 --- a/internal/credentials/errors_test.go +++ b/internal/credentials/errors_test.go @@ -104,7 +104,7 @@ func TestAccessError(t *testing.T) { errorString: "something went wrong (" + "endpoint:\"grps://localhost:2135\"," + "database:\"/local\"," + - "credentials:\"Static{User:\\\"USER\\\",Password:\\\"SEC**********RD\\\",Token:\\\"****(CRC-32c: 00000000)\\\"}\"" + //nolint:lll + "credentials:\"Static{User:\\\"USER\\\",Password:\\\"S*************D\\\",Token:\\\"****(CRC-32c: 00000000)\\\"}\"" + //nolint:lll "): test " + "at `github.com/ydb-platform/ydb-go-sdk/v3/internal/credentials.TestAccessError(errors_test.go:93)`", }, @@ -123,7 +123,7 @@ func TestAccessError(t *testing.T) { errorString: "something went wrong (" + "endpoint:\"grps://localhost:2135\"," + "database:\"/local\"," + - "credentials:\"Static{User:\\\"USER\\\",Password:\\\"SEC**********RD\\\",Token:\\\"****(CRC-32c: 00000000)\\\",From:\\\"TestAccessError\\\"}\"" + //nolint:lll + "credentials:\"Static{User:\\\"USER\\\",Password:\\\"S*************D\\\",Token:\\\"****(CRC-32c: 00000000)\\\",From:\\\"TestAccessError\\\"}\"" + //nolint:lll "): test " + "at `github.com/ydb-platform/ydb-go-sdk/v3/internal/credentials.TestAccessError(errors_test.go:112)`", }, diff --git a/internal/secret/dsn.go b/internal/secret/dsn.go new file mode 100644 index 000000000..be205c770 --- /dev/null +++ b/internal/secret/dsn.go @@ -0,0 +1,62 @@ +package secret + +import ( + "net/url" + "strings" + + "github.com/ydb-platform/ydb-go-sdk/v3/pkg/xstring" +) + +func DSN(dsn string) string { + u, err := url.Parse(dsn) + if err != nil { + return "" + } + + buffer := xstring.Buffer() + defer buffer.Free() + + if u.Scheme != "" { + buffer.WriteString(u.Scheme + "://") + } + + if u.User != nil { + buffer.WriteString(u.User.Username()) + if password, has := u.User.Password(); has { + buffer.WriteString(":" + Password(password)) + } + buffer.WriteString("@") + } + + buffer.WriteString(u.Host) + buffer.WriteString(u.Path) + + if len(u.RawQuery) > 0 { + buffer.WriteString("?") + + params := strings.Split(u.RawQuery, "&") + + for i, param := range params { + if i > 0 { + buffer.WriteString("&") + } + paramValue := strings.SplitN(param, "=", 2) + buffer.WriteString(paramValue[0]) + if len(paramValue) > 1 { + buffer.WriteString("=") + switch paramValue[0] { + case "token", "password": + buffer.WriteString(Mask(paramValue[1])) + default: + buffer.WriteString(paramValue[1]) + } + } + } + } + + if u.Fragment != "" { + buffer.WriteString("#" + u.Fragment) + } + + return buffer.String() +} diff --git a/internal/secret/dsn_test.go b/internal/secret/dsn_test.go new file mode 100644 index 000000000..89f1ee208 --- /dev/null +++ b/internal/secret/dsn_test.go @@ -0,0 +1,55 @@ +package secret + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestDSN(t *testing.T) { + for _, tt := range []struct { + dsn string + exp string + }{ + { + dsn: "grpc://192.168.0.%31:2136/", + exp: "", + }, + { + dsn: "grpc://debuguser:debugpassword@localhost:2136/local1", + exp: "grpc://debuguser:d***********d@localhost:2136/local1", + }, + { + dsn: "grpc://host/db?password=pass%3Dword", + exp: "grpc://host/db?password=p*********d", + }, + { + dsn: "grpc://host/db?password=pass%26word", + exp: "grpc://host/db?password=p*********d", + }, + { + dsn: "grpc://localhost:2136/local1?user=debuguser&password=debugpassword", + exp: "grpc://localhost:2136/local1?user=debuguser&password=d***********d", + }, + { + dsn: "grpc://localhost:2136/local1?login=debuguser&password=debugpassword", + exp: "grpc://localhost:2136/local1?login=debuguser&password=d***********d", + }, + { + dsn: "grpc://localhost:2136/local1?param1=value1&login=debuguser¶m2=value2&password=debugpassword¶m2=value3", + exp: "grpc://localhost:2136/local1?param1=value1&login=debuguser¶m2=value2&password=d***********d¶m2=value3", + }, + { + dsn: "grpc://localhost:2136/local1?param1&login=debuguser¶m2=value2&password=debugpassword¶m2=value3", + exp: "grpc://localhost:2136/local1?param1&login=debuguser¶m2=value2&password=d***********d¶m2=value3", + }, + { + dsn: "grpc://localhost:2136/local1?token=secrettoken123", + exp: "grpc://localhost:2136/local1?token=s************3", + }, + } { + t.Run(tt.dsn, func(t *testing.T) { + require.Equal(t, tt.exp, DSN(tt.dsn)) + }) + } +} diff --git a/internal/secret/mask.go b/internal/secret/mask.go new file mode 100644 index 000000000..578504326 --- /dev/null +++ b/internal/secret/mask.go @@ -0,0 +1,20 @@ +package secret + +func Mask(s string) string { + var ( + runes = []rune(s) + startPosition = 1 + endPosition = len(runes) - 1 + ) + + if len(runes) < 5 { + startPosition = 0 + endPosition = len(runes) + } + + for i := startPosition; i < endPosition; i++ { + runes[i] = '*' + } + + return string(runes) +} diff --git a/internal/secret/mask_test.go b/internal/secret/mask_test.go new file mode 100644 index 000000000..7078b4df9 --- /dev/null +++ b/internal/secret/mask_test.go @@ -0,0 +1,35 @@ +package secret + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMask(t *testing.T) { + for _, tt := range []struct { + s string + exp string + }{ + { + s: "test", + exp: "****", + }, + { + s: "test-long-password", + exp: "t****************d", + }, + { + s: "пароль", + exp: "п****ь", + }, + { + s: "пар", + exp: "***", + }, + } { + t.Run("", func(t *testing.T) { + require.Equal(t, tt.exp, Mask(tt.s)) + }) + } +} diff --git a/internal/secret/password.go b/internal/secret/password.go index 888d07525..8351f5a15 100644 --- a/internal/secret/password.go +++ b/internal/secret/password.go @@ -1,24 +1,5 @@ package secret -import ( - "github.com/ydb-platform/ydb-go-sdk/v3/pkg/xstring" -) - func Password(password string) string { - var ( - bytes = []byte(password) - startPosition = 3 - endPosition = len(bytes) - 2 - ) - if startPosition > endPosition { - for i := range bytes { - bytes[i] = '*' - } - } else { - for i := startPosition; i < endPosition; i++ { - bytes[i] = '*' - } - } - - return xstring.FromBytes(bytes) + return Mask(password) } diff --git a/internal/secret/password_test.go b/internal/secret/password_test.go index 5570ae6ef..8bd2c4aa2 100644 --- a/internal/secret/password_test.go +++ b/internal/secret/password_test.go @@ -17,7 +17,15 @@ func TestPassword(t *testing.T) { }, { password: "test-long-password", - exp: "tes*************rd", + exp: "t****************d", + }, + { + password: "пароль", + exp: "п****ь", + }, + { + password: "пар", + exp: "***", }, } { t.Run("", func(t *testing.T) { diff --git a/options.go b/options.go index 15c083eae..76c499bf8 100644 --- a/options.go +++ b/options.go @@ -21,6 +21,7 @@ import ( ratelimiterConfig "github.com/ydb-platform/ydb-go-sdk/v3/internal/ratelimiter/config" schemeConfig "github.com/ydb-platform/ydb-go-sdk/v3/internal/scheme/config" scriptingConfig "github.com/ydb-platform/ydb-go-sdk/v3/internal/scripting/config" + "github.com/ydb-platform/ydb-go-sdk/v3/internal/secret" tableConfig "github.com/ydb-platform/ydb-go-sdk/v3/internal/table/config" "github.com/ydb-platform/ydb-go-sdk/v3/internal/xerrors" "github.com/ydb-platform/ydb-go-sdk/v3/internal/xsql" @@ -203,7 +204,7 @@ func WithConnectionString(connectionString string) Option { info, err := dsn.Parse(connectionString) if err != nil { return xerrors.WithStackTrace( - fmt.Errorf("parse connection string '%s' failed: %w", connectionString, err), + fmt.Errorf("parse connection string '%s' failed: %w", secret.DSN(connectionString), err), ) } d.options = append(d.options, info.Options...) diff --git a/sql.go b/sql.go index 2ee1ecd32..e8d9d5ad7 100644 --- a/sql.go +++ b/sql.go @@ -7,6 +7,7 @@ import ( "fmt" "github.com/ydb-platform/ydb-go-sdk/v3/internal/bind" + "github.com/ydb-platform/ydb-go-sdk/v3/internal/secret" "github.com/ydb-platform/ydb-go-sdk/v3/internal/tx" "github.com/ydb-platform/ydb-go-sdk/v3/internal/xerrors" "github.com/ydb-platform/ydb-go-sdk/v3/internal/xsql" @@ -47,7 +48,10 @@ func (d *sqlDriver) Open(string) (driver.Conn, error) { func (d *sqlDriver) OpenConnector(dataSourceName string) (driver.Connector, error) { db, err := Open(context.Background(), dataSourceName) if err != nil { - return nil, xerrors.WithStackTrace(fmt.Errorf("failed to connect by data source name '%s': %w", dataSourceName, err)) + return nil, xerrors.WithStackTrace(fmt.Errorf( + "failed to connect by data source name '%s': %w", + secret.DSN(dataSourceName), err, + )) } c, err := Connector(db, append(db.databaseSQLOptions,