From a6e53f2b761cf35dde294a33dee9425e21c40dc6 Mon Sep 17 00:00:00 2001 From: Lawrence Liu Date: Sat, 8 Nov 2025 19:33:13 +0800 Subject: [PATCH] =?UTF-8?q?fix(security):=20=E8=84=B1=E6=95=8F=E5=90=8E?= =?UTF-8?q?=E5=8F=B0=E6=97=A5=E5=BF=97=E4=B8=AD=E7=9A=84=E6=95=8F=E6=84=9F?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=20(#761)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 问题 后台日志在打印配置更新时会暴露完整的 API Key、Secret Key 和私钥等敏感信息(Issue #758)。 ## 解决方案 ### 1. 新增脱敏工具库 (api/utils.go) - `MaskSensitiveString()`: 脱敏敏感字符串(保留前4位和后4位,中间用****替代) - `SanitizeModelConfigForLog()`: 脱敏 AI 模型配置用于日志输出 - `SanitizeExchangeConfigForLog()`: 脱敏交易所配置用于日志输出 - `MaskEmail()`: 脱敏邮箱地址 ### 2. 修复日志打印 (api/server.go) - Line 1106: 脱敏 AI 模型配置更新日志 - Line 1203: 脱敏交易所配置更新日志 ### 3. 完善单元测试 (api/utils_test.go) - 4个测试函数,9个子测试,全部通过 - 工具函数测试覆盖率: 91%+ ## 脱敏效果示例 **修复前**: ``` ✓ 交易所配置已更新: map[binance:{api_key:sk-1234567890abcdef secret_key:binance_secret_1234567890abcdef}] ``` **修复后**: ``` ✓ 交易所配置已更新: map[binance:{api_key:sk-1****cdef secret_key:bina****cdef}] ``` ## 测试结果 ``` PASS ok nofx/api 0.012s coverage: 91.2% of statements in utils.go ``` ## 安全影响 - 防止日志泄露 API Key、Secret Key、私钥等敏感信息 - 保护用户隐私和账户安全 - 符合安全最佳实践 Closes #758 --- api/server.go | 4 +- api/utils.go | 97 +++++++++++++++++++++++ api/utils_test.go | 193 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 292 insertions(+), 2 deletions(-) create mode 100644 api/utils.go create mode 100644 api/utils_test.go diff --git a/api/server.go b/api/server.go index 01c2c0ae..e98db44a 100644 --- a/api/server.go +++ b/api/server.go @@ -1077,7 +1077,7 @@ func (s *Server) handleUpdateModelConfigs(c *gin.Context) { // 这里不返回错误,因为模型配置已经成功更新到数据库 } - log.Printf("✓ AI模型配置已更新: %+v", req.Models) + log.Printf("✓ AI模型配置已更新: %+v", SanitizeModelConfigForLog(req.Models)) c.JSON(http.StatusOK, gin.H{"message": "模型配置已更新"}) } @@ -1174,7 +1174,7 @@ func (s *Server) handleUpdateExchangeConfigs(c *gin.Context) { // 这里不返回错误,因为交易所配置已经成功更新到数据库 } - log.Printf("✓ 交易所配置已更新: %+v", req.Exchanges) + log.Printf("✓ 交易所配置已更新: %+v", SanitizeExchangeConfigForLog(req.Exchanges)) c.JSON(http.StatusOK, gin.H{"message": "交易所配置已更新"}) } diff --git a/api/utils.go b/api/utils.go new file mode 100644 index 00000000..4f871ef0 --- /dev/null +++ b/api/utils.go @@ -0,0 +1,97 @@ +package api + +import "strings" + +// MaskSensitiveString 脱敏敏感字符串,只显示前4位和后4位 +// 用于脱敏 API Key、Secret Key、Private Key 等敏感信息 +func MaskSensitiveString(s string) string { + if s == "" { + return "" + } + length := len(s) + if length <= 8 { + return "****" // 字符串太短,全部隐藏 + } + return s[:4] + "****" + s[length-4:] +} + +// SanitizeModelConfigForLog 脱敏模型配置用于日志输出 +func SanitizeModelConfigForLog(models map[string]struct { + Enabled bool `json:"enabled"` + APIKey string `json:"api_key"` + CustomAPIURL string `json:"custom_api_url"` + CustomModelName string `json:"custom_model_name"` +}) map[string]interface{} { + safe := make(map[string]interface{}) + for modelID, cfg := range models { + safe[modelID] = map[string]interface{}{ + "enabled": cfg.Enabled, + "api_key": MaskSensitiveString(cfg.APIKey), + "custom_api_url": cfg.CustomAPIURL, + "custom_model_name": cfg.CustomModelName, + } + } + return safe +} + +// SanitizeExchangeConfigForLog 脱敏交易所配置用于日志输出 +func SanitizeExchangeConfigForLog(exchanges map[string]struct { + Enabled bool `json:"enabled"` + APIKey string `json:"api_key"` + SecretKey string `json:"secret_key"` + Testnet bool `json:"testnet"` + HyperliquidWalletAddr string `json:"hyperliquid_wallet_addr"` + AsterUser string `json:"aster_user"` + AsterSigner string `json:"aster_signer"` + AsterPrivateKey string `json:"aster_private_key"` +}) map[string]interface{} { + safe := make(map[string]interface{}) + for exchangeID, cfg := range exchanges { + safeExchange := map[string]interface{}{ + "enabled": cfg.Enabled, + "testnet": cfg.Testnet, + } + + // 只在有值时才添加脱敏后的敏感字段 + if cfg.APIKey != "" { + safeExchange["api_key"] = MaskSensitiveString(cfg.APIKey) + } + if cfg.SecretKey != "" { + safeExchange["secret_key"] = MaskSensitiveString(cfg.SecretKey) + } + if cfg.AsterPrivateKey != "" { + safeExchange["aster_private_key"] = MaskSensitiveString(cfg.AsterPrivateKey) + } + + // 非敏感字段直接添加 + if cfg.HyperliquidWalletAddr != "" { + safeExchange["hyperliquid_wallet_addr"] = cfg.HyperliquidWalletAddr + } + if cfg.AsterUser != "" { + safeExchange["aster_user"] = cfg.AsterUser + } + if cfg.AsterSigner != "" { + safeExchange["aster_signer"] = cfg.AsterSigner + } + + safe[exchangeID] = safeExchange + } + return safe +} + +// MaskEmail 脱敏邮箱地址,保留前2位和@后部分 +func MaskEmail(email string) string { + if email == "" { + return "" + } + parts := strings.Split(email, "@") + if len(parts) != 2 { + return "****" // 格式不正确 + } + username := parts[0] + domain := parts[1] + if len(username) <= 2 { + return "**@" + domain + } + return username[:2] + "****@" + domain +} diff --git a/api/utils_test.go b/api/utils_test.go new file mode 100644 index 00000000..fb4976ff --- /dev/null +++ b/api/utils_test.go @@ -0,0 +1,193 @@ +package api + +import ( + "testing" +) + +func TestMaskSensitiveString(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "空字符串", + input: "", + expected: "", + }, + { + name: "短字符串(小于等于8位)", + input: "short", + expected: "****", + }, + { + name: "正常API key", + input: "sk-1234567890abcdefghijklmnopqrstuvwxyz", + expected: "sk-1****wxyz", + }, + { + name: "正常私钥", + input: "0x1234567890abcdef1234567890abcdef12345678", + expected: "0x12****5678", + }, + { + name: "刚好9位", + input: "123456789", + expected: "1234****6789", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := MaskSensitiveString(tt.input) + if result != tt.expected { + t.Errorf("MaskSensitiveString(%q) = %q, want %q", tt.input, result, tt.expected) + } + }) + } +} + +func TestSanitizeModelConfigForLog(t *testing.T) { + models := map[string]struct { + Enabled bool `json:"enabled"` + APIKey string `json:"api_key"` + CustomAPIURL string `json:"custom_api_url"` + CustomModelName string `json:"custom_model_name"` + }{ + "deepseek": { + Enabled: true, + APIKey: "sk-1234567890abcdefghijklmnopqrstuvwxyz", + CustomAPIURL: "https://api.deepseek.com", + CustomModelName: "deepseek-chat", + }, + } + + result := SanitizeModelConfigForLog(models) + + deepseekConfig, ok := result["deepseek"].(map[string]interface{}) + if !ok { + t.Fatal("deepseek config not found or wrong type") + } + + if deepseekConfig["enabled"] != true { + t.Errorf("expected enabled=true, got %v", deepseekConfig["enabled"]) + } + + maskedKey, ok := deepseekConfig["api_key"].(string) + if !ok { + t.Fatal("api_key not found or wrong type") + } + + if maskedKey != "sk-1****wxyz" { + t.Errorf("expected masked api_key='sk-1****wxyz', got %q", maskedKey) + } + + if deepseekConfig["custom_api_url"] != "https://api.deepseek.com" { + t.Errorf("custom_api_url should not be masked") + } +} + +func TestSanitizeExchangeConfigForLog(t *testing.T) { + exchanges := map[string]struct { + Enabled bool `json:"enabled"` + APIKey string `json:"api_key"` + SecretKey string `json:"secret_key"` + Testnet bool `json:"testnet"` + HyperliquidWalletAddr string `json:"hyperliquid_wallet_addr"` + AsterUser string `json:"aster_user"` + AsterSigner string `json:"aster_signer"` + AsterPrivateKey string `json:"aster_private_key"` + }{ + "binance": { + Enabled: true, + APIKey: "binance_api_key_1234567890abcdef", + SecretKey: "binance_secret_key_1234567890abcdef", + Testnet: false, + }, + "hyperliquid": { + Enabled: true, + HyperliquidWalletAddr: "0x1234567890abcdef1234567890abcdef12345678", + Testnet: false, + }, + } + + result := SanitizeExchangeConfigForLog(exchanges) + + // 检查币安配置 + binanceConfig, ok := result["binance"].(map[string]interface{}) + if !ok { + t.Fatal("binance config not found or wrong type") + } + + maskedAPIKey, ok := binanceConfig["api_key"].(string) + if !ok { + t.Fatal("binance api_key not found or wrong type") + } + + if maskedAPIKey != "bina****cdef" { + t.Errorf("expected masked api_key='bina****cdef', got %q", maskedAPIKey) + } + + maskedSecretKey, ok := binanceConfig["secret_key"].(string) + if !ok { + t.Fatal("binance secret_key not found or wrong type") + } + + if maskedSecretKey != "bina****cdef" { + t.Errorf("expected masked secret_key='bina****cdef', got %q", maskedSecretKey) + } + + // 检查 Hyperliquid 配置 + hlConfig, ok := result["hyperliquid"].(map[string]interface{}) + if !ok { + t.Fatal("hyperliquid config not found or wrong type") + } + + walletAddr, ok := hlConfig["hyperliquid_wallet_addr"].(string) + if !ok { + t.Fatal("hyperliquid_wallet_addr not found or wrong type") + } + + // 钱包地址不应该被脱敏 + if walletAddr != "0x1234567890abcdef1234567890abcdef12345678" { + t.Errorf("wallet address should not be masked, got %q", walletAddr) + } +} + +func TestMaskEmail(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "空邮箱", + input: "", + expected: "", + }, + { + name: "格式错误", + input: "notanemail", + expected: "****", + }, + { + name: "正常邮箱", + input: "user@example.com", + expected: "us****@example.com", + }, + { + name: "短用户名", + input: "a@example.com", + expected: "**@example.com", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := MaskEmail(tt.input) + if result != tt.expected { + t.Errorf("MaskEmail(%q) = %q, want %q", tt.input, result, tt.expected) + } + }) + } +}