Skip to content

Commit db00e2d

Browse files
committed
tapdb: sanitize db errors before returning
In this PR, we start to sanitize unexpected DB errors before returning to the caller. This is important, as ultimately for our gRPC calls, the caller is some client-side software. For now, we take the simple approach and match on some obvious strings, mapping them to a generic "db connection failed" error.
1 parent 25b3a3e commit db00e2d

File tree

3 files changed

+121
-4
lines changed

3 files changed

+121
-4
lines changed

tapdb/postgres.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ func NewPostgresStore(cfg *PostgresConfig) (*PostgresStore, error) {
9191

9292
rawDb, err := sql.Open("pgx", cfg.DSN(false))
9393
if err != nil {
94-
return nil, err
94+
return nil, MapSQLError(err)
9595
}
9696

9797
maxConns := defaultMaxConns

tapdb/postgres_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//go:build test_db_postgres
2+
3+
package tapdb
4+
5+
import (
6+
"context"
7+
"database/sql"
8+
"testing"
9+
10+
"github.com/lightningnetwork/lnd/macaroons"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
// TestPostgresErrorSanitization tests that we can handle a Postgres connection
15+
// error gracefully, without leaking sensitive information.
16+
func TestPostgresErrorSanitization(t *testing.T) {
17+
t.Parallel()
18+
19+
// We first create a Postgres fixture and a DB that will run the
20+
// migration scripts.
21+
sqlFixture := NewTestPgFixture(t, DefaultPostgresFixtureLifetime, true)
22+
_, err := NewPostgresStore(sqlFixture.GetConfig())
23+
require.NoError(t, err)
24+
25+
// Now we create a connection config that won't work because of the
26+
// wrong password.
27+
config := sqlFixture.GetConfig()
28+
config.Password = "different"
29+
config.SkipMigrations = true
30+
31+
store, err := NewPostgresStore(config)
32+
require.NoError(t, err)
33+
34+
rksDB := NewTransactionExecutor(store, func(tx *sql.Tx) KeyStore {
35+
return store.WithTx(tx)
36+
})
37+
rks := NewRootKeyStore(rksDB)
38+
39+
ctx := context.Background()
40+
rootKeyCtx := macaroons.ContextWithRootKeyID(ctx, []byte("kek"))
41+
_, _, err = rks.RootKey(rootKeyCtx)
42+
require.Error(t, err)
43+
require.Equal(
44+
t, "unknown postgres error: database connection failed",
45+
err.Error(),
46+
)
47+
}

tapdb/sqlerrors.go

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,12 @@ func MapSQLError(err error) error {
3232
return parsePostgresError(pqErr)
3333
}
3434

35-
// Return original error if it could not be classified as a database
36-
// specific error.
35+
// As a last step, check if this is a connection error that needs
36+
// sanitization to prevent leaking sensitive information.
37+
err = sanitizeConnectionError(err)
38+
39+
// Return the error (potentially sanitized) if it could not be
40+
// classified as a database specific error.
3741
return err
3842
}
3943

@@ -111,7 +115,8 @@ func parsePostgresError(pqErr *pgconn.PgError) error {
111115
}
112116

113117
default:
114-
return fmt.Errorf("unknown postgres error: %w", pqErr)
118+
return fmt.Errorf("unknown postgres error: %w",
119+
sanitizeConnectionError(pqErr))
115120
}
116121
}
117122

@@ -198,3 +203,68 @@ func IsSchemaError(err error) bool {
198203
var schemaError *ErrSchemaError
199204
return errors.As(err, &schemaError)
200205
}
206+
207+
// ErrDatabaseConnectionError is an error type which represents a database
208+
// connection error with sensitive information sanitized.
209+
type ErrDatabaseConnectionError struct {
210+
DbError error
211+
}
212+
213+
// Unwrap returns the wrapped error.
214+
func (e ErrDatabaseConnectionError) Unwrap() error {
215+
return e.DbError
216+
}
217+
218+
// Error returns a generic error message without revealing connection details.
219+
func (e ErrDatabaseConnectionError) Error() string {
220+
// Return a generic error message that doesn't reveal any connection
221+
// details to prevent information leakage.
222+
return "database connection failed"
223+
}
224+
225+
// isConnectionError checks if an error message contains patterns that indicate
226+
// a database connection error with potentially sensitive information.
227+
func isConnectionError(errStr string) bool {
228+
// List of patterns that indicate connection errors with sensitive info.
229+
patterns := []string{
230+
"failed to connect to",
231+
"dial tcp",
232+
"user=",
233+
"password=",
234+
"host=",
235+
"dbname=",
236+
"sslmode=",
237+
"connection refused",
238+
"no route to host",
239+
"password authentication failed",
240+
}
241+
242+
for _, pattern := range patterns {
243+
if strings.Contains(errStr, pattern) {
244+
return true
245+
}
246+
}
247+
248+
return false
249+
}
250+
251+
// sanitizeConnectionError checks if an error contains database connection
252+
// information and returns a sanitized version if it does.
253+
func sanitizeConnectionError(err error) error {
254+
if err == nil {
255+
return nil
256+
}
257+
258+
// Check if the error message contains connection parameters that could
259+
// leak sensitive information.
260+
if isConnectionError(err.Error()) {
261+
// Log the original error for debugging purposes, but return a
262+
// sanitized version to prevent information leakage.
263+
log.Errorf("Database connection error (sanitized): %v", err)
264+
return &ErrDatabaseConnectionError{
265+
DbError: err,
266+
}
267+
}
268+
269+
return err
270+
}

0 commit comments

Comments
 (0)