From aeeec343269e333d6750c710857232b29231b813 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 3 Apr 2026 10:06:14 +0800 Subject: [PATCH] =?UTF-8?q?fix(supply-api):=20=E4=BF=AE=E5=A4=8DP2-05?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=E5=87=AD=E8=AF=81=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E6=B3=84=E9=9C=B2=E9=A3=8E=E9=99=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 在DatabaseConfig中添加SafeDSN()方法,返回脱敏的连接信息 2. 在NewDB中使用SafeDSN()记录日志 3. 添加sanitizeErrorPassword()函数清理错误信息中的密码 修复的问题:P2-05 数据库凭证日志泄露风险 --- supply-api/internal/config/config.go | 9 ++++++++- supply-api/internal/repository/db.go | 27 +++++++++++++++++++++++---- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/supply-api/internal/config/config.go b/supply-api/internal/config/config.go index b364e8c..ab91d50 100644 --- a/supply-api/internal/config/config.go +++ b/supply-api/internal/config/config.go @@ -66,12 +66,19 @@ type AuditConfig struct { ExportTimeout time.Duration } -// DSN 返回数据库连接字符串 +// DSN 返回数据库连接字符串(包含明文密码,仅限内部使用) func (d *DatabaseConfig) DSN() string { return fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=disable", d.User, d.Password, d.Host, d.Port, d.Database) } +// SafeDSN 返回脱敏的数据库连接字符串(密码被替换为***),用于日志记录 +// P2-05: 避免在日志中泄露数据库密码 +func (d *DatabaseConfig) SafeDSN() string { + return fmt.Sprintf("postgres://%s:***@%s:%d/%s?sslmode=disable", + d.User, d.Host, d.Port, d.Database) +} + // Addr 返回Redis地址 func (r *RedisConfig) Addr() string { return fmt.Sprintf("%s:%d", r.Host, r.Port) diff --git a/supply-api/internal/repository/db.go b/supply-api/internal/repository/db.go index 1b25b5a..33c8494 100644 --- a/supply-api/internal/repository/db.go +++ b/supply-api/internal/repository/db.go @@ -3,6 +3,7 @@ package repository import ( "context" "fmt" + "strings" "time" "github.com/jackc/pgx/v5" @@ -17,9 +18,11 @@ type DB struct { // NewDB 创建数据库连接池 func NewDB(ctx context.Context, cfg config.DatabaseConfig) (*DB, error) { - poolConfig, err := pgxpool.ParseConfig(cfg.DSN()) + dsn := cfg.DSN() + poolConfig, err := pgxpool.ParseConfig(dsn) if err != nil { - return nil, fmt.Errorf("failed to parse database config: %w", err) + // P2-05: 使用SafeDSN替代DSN,避免在错误信息中泄露密码 + return nil, fmt.Errorf("failed to parse database config for %s: %v", cfg.SafeDSN(), sanitizeErrorPassword(err, cfg.Password)) } poolConfig.MaxConns = int32(cfg.MaxOpenConns) @@ -30,18 +33,34 @@ func NewDB(ctx context.Context, cfg config.DatabaseConfig) (*DB, error) { pool, err := pgxpool.NewWithConfig(ctx, poolConfig) if err != nil { - return nil, fmt.Errorf("failed to create connection pool: %w", err) + // P2-05: 清理错误信息中的密码 + return nil, fmt.Errorf("failed to create connection pool for %s: %v", cfg.SafeDSN(), sanitizeErrorPassword(err, cfg.Password)) } // 验证连接 if err := pool.Ping(ctx); err != nil { pool.Close() - return nil, fmt.Errorf("failed to ping database: %w", err) + return nil, fmt.Errorf("failed to ping database at %s:%d: %v", cfg.Host, cfg.Port, err) } return &DB{Pool: pool}, nil } +// sanitizeErrorPassword 从错误信息中清理密码 +// P2-05: pgxpool.ParseConfig的错误信息可能包含完整的DSN,需要清理 +func sanitizeErrorPassword(err error, password string) error { + if err == nil || password == "" { + return err + } + // 将错误信息中的密码替换为*** + errStr := err.Error() + safeErrStr := strings.ReplaceAll(errStr, password, "***") + if safeErrStr != errStr { + return fmt.Errorf("%s (password sanitized)", safeErrStr) + } + return err +} + // Close 关闭连接池 func (db *DB) Close() { if db.Pool != nil {