Files
sub2api/backend/internal/util/logredact/redact.go
yangjianbo d7011163b8 fix: 修复代码审核发现的安全和质量问题
安全修复(P0):
- 移除硬编码的 OAuth client_secret(Antigravity、Gemini CLI),
  改为通过环境变量注入(ANTIGRAVITY_OAUTH_CLIENT_SECRET、
  GEMINI_CLI_OAUTH_CLIENT_SECRET)
- 新增 logredact.RedactText() 对非结构化文本做敏感信息脱敏,
  覆盖 GOCSPX-*/AIza* 令牌和常见 key=value 模式
- 日志中不再打印 org_uuid、account_uuid、email_address 等敏感值

安全修复(P1):
- URL 验证增强:新增 ValidateHTTPURL 统一入口,支持 allowlist 和
  私网地址阻断(localhost/内网 IP)
- 代理回退安全:代理初始化失败时默认阻止直连回退,防止 IP 泄露,
  可通过 security.proxy_fallback.allow_direct_on_error 显式开启
- Gemini OAuth 配置校验:client_id 与 client_secret 必须同时
  设置或同时留空

其他改进:
- 新增 tools/secret_scan.py 密钥扫描工具和 Makefile secret-scan 目标
- 更新所有 docker-compose 和部署配置,传递 OAuth secret 环境变量
- google_one OAuth 类型使用固定 redirectURI,与 code_assist 对齐

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 09:58:13 +08:00

174 lines
4.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package logredact
import (
"encoding/json"
"regexp"
"strings"
)
// maxRedactDepth 限制递归深度以防止栈溢出
const maxRedactDepth = 32
var defaultSensitiveKeys = map[string]struct{}{
"authorization_code": {},
"code": {},
"code_verifier": {},
"access_token": {},
"refresh_token": {},
"id_token": {},
"client_secret": {},
"password": {},
}
var defaultSensitiveKeyList = []string{
"authorization_code",
"code",
"code_verifier",
"access_token",
"refresh_token",
"id_token",
"client_secret",
"password",
}
var (
reGOCSPX = regexp.MustCompile(`GOCSPX-[0-9A-Za-z_-]{24,}`)
reAIza = regexp.MustCompile(`AIza[0-9A-Za-z_-]{35}`)
)
func RedactMap(input map[string]any, extraKeys ...string) map[string]any {
if input == nil {
return map[string]any{}
}
keys := buildKeySet(extraKeys)
redacted, ok := redactValueWithDepth(input, keys, 0).(map[string]any)
if !ok {
return map[string]any{}
}
return redacted
}
func RedactJSON(raw []byte, extraKeys ...string) string {
if len(raw) == 0 {
return ""
}
var value any
if err := json.Unmarshal(raw, &value); err != nil {
return "<non-json payload redacted>"
}
keys := buildKeySet(extraKeys)
redacted := redactValueWithDepth(value, keys, 0)
encoded, err := json.Marshal(redacted)
if err != nil {
return "<redacted>"
}
return string(encoded)
}
// RedactText 对非结构化文本做轻量脱敏。
//
// 规则:
// - 如果文本本身是 JSON则按 RedactJSON 处理。
// - 否则尝试对常见 key=value / key:"value" 片段做脱敏。
//
// 注意:该函数用于日志/错误信息兜底,不保证覆盖所有格式。
func RedactText(input string, extraKeys ...string) string {
input = strings.TrimSpace(input)
if input == "" {
return ""
}
raw := []byte(input)
if json.Valid(raw) {
return RedactJSON(raw, extraKeys...)
}
keyAlt := buildKeyAlternation(extraKeys)
// JSON-like: "access_token":"..."
reJSONLike := regexp.MustCompile(`(?i)("(?:` + keyAlt + `)"\s*:\s*")([^"]*)(")`)
// Query-like: access_token=...
reQueryLike := regexp.MustCompile(`(?i)\b((?:` + keyAlt + `))=([^&\s]+)`)
// Plain: access_token: ... / access_token = ...
rePlain := regexp.MustCompile(`(?i)\b((?:` + keyAlt + `))\b(\s*[:=]\s*)([^,\s]+)`)
out := input
out = reGOCSPX.ReplaceAllString(out, "GOCSPX-***")
out = reAIza.ReplaceAllString(out, "AIza***")
out = reJSONLike.ReplaceAllString(out, `$1***$3`)
out = reQueryLike.ReplaceAllString(out, `$1=***`)
out = rePlain.ReplaceAllString(out, `$1$2***`)
return out
}
func buildKeyAlternation(extraKeys []string) string {
seen := make(map[string]struct{}, len(defaultSensitiveKeyList)+len(extraKeys))
keys := make([]string, 0, len(defaultSensitiveKeyList)+len(extraKeys))
for _, k := range defaultSensitiveKeyList {
seen[k] = struct{}{}
keys = append(keys, regexp.QuoteMeta(k))
}
for _, k := range extraKeys {
n := normalizeKey(k)
if n == "" {
continue
}
if _, ok := seen[n]; ok {
continue
}
seen[n] = struct{}{}
keys = append(keys, regexp.QuoteMeta(n))
}
return strings.Join(keys, "|")
}
func buildKeySet(extraKeys []string) map[string]struct{} {
keys := make(map[string]struct{}, len(defaultSensitiveKeys)+len(extraKeys))
for k := range defaultSensitiveKeys {
keys[k] = struct{}{}
}
for _, key := range extraKeys {
normalized := normalizeKey(key)
if normalized == "" {
continue
}
keys[normalized] = struct{}{}
}
return keys
}
func redactValueWithDepth(value any, keys map[string]struct{}, depth int) any {
if depth > maxRedactDepth {
return "<depth limit exceeded>"
}
switch v := value.(type) {
case map[string]any:
out := make(map[string]any, len(v))
for k, val := range v {
if isSensitiveKey(k, keys) {
out[k] = "***"
continue
}
out[k] = redactValueWithDepth(val, keys, depth+1)
}
return out
case []any:
out := make([]any, len(v))
for i, item := range v {
out[i] = redactValueWithDepth(item, keys, depth+1)
}
return out
default:
return value
}
}
func isSensitiveKey(key string, keys map[string]struct{}) bool {
_, ok := keys[normalizeKey(key)]
return ok
}
func normalizeKey(key string) string {
return strings.ToLower(strings.TrimSpace(key))
}