Files
nofx/api/utils_test.go
tinkle-community 1aea7abc38 fix(security): remove decrypt oracle, redact secret logs, harden auth, bump Go
Address multiple vulnerabilities found during security review:

- Remove unauthenticated POST /api/crypto/decrypt decryption oracle (route,
  handler, dead frontend helper) + regression test. Transport encryption is
  one-directional; the server never needs to decrypt arbitrary client payloads.
- Redact secrets in config-update logs: handler_ai_model/handler_exchange logged
  %+v of decrypted requests, leaking API keys / secret keys / passphrases /
  private keys. Use named types shared with the log sanitizer so the masking
  can never drift again; extend masking to passphrase + lighter_api_key_private_key.
- crypto: require a valid timestamp in DecryptPayload (a missing ts previously
  skipped replay protection entirely).
- crypto: EncryptedString.Value() now fails closed instead of silently
  persisting plaintext secrets when encryption errors.
- auth: per-IP token-bucket rate limiting on /login and /register against online
  brute-force; raise registration password minimum 6 -> 8; add dummy bcrypt
  compare on unknown-email login to close the user-enumeration timing channel.
- IDOR: getTraderFromQuery no longer falls back to the global in-memory trader
  map; trader access is strictly scoped to the authenticated caller.
- Bump Go 1.25.10 -> 1.25.11 to resolve reachable net/textproto and crypto/x509
  stdlib advisories (govulncheck now reports 0 affecting vulnerabilities).
2026-06-05 22:08:26 +08:00

255 lines
7.0 KiB
Go

package api
import (
"fmt"
"strings"
"testing"
)
func TestMaskSensitiveString(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "Empty string",
input: "",
expected: "",
},
{
name: "Short string (8 characters or less)",
input: "short",
expected: "****",
},
{
name: "Normal API key",
input: "sk-1234567890abcdefghijklmnopqrstuvwxyz",
expected: "sk-1****wxyz",
},
{
name: "Normal private key",
input: "0x1234567890abcdef1234567890abcdef12345678",
expected: "0x12****5678",
},
{
name: "Exactly 9 characters",
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]ModelConfigUpdate{
"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]ExchangeConfigUpdate{
"binance": {
Enabled: true,
APIKey: "binance_api_key_1234567890abcdef",
SecretKey: "binance_secret_key_1234567890abcdef",
Testnet: false,
},
"okx": {
Enabled: true,
APIKey: "okx_api_key_1234567890abcdef",
SecretKey: "okx_secret_key_1234567890abcdef",
Passphrase: "okx_passphrase_supersecret_value",
},
"lighter": {
Enabled: true,
LighterWalletAddr: "0xabcdef0000000000000000000000000000000000",
LighterPrivateKey: "lighter_private_key_1234567890abcdef",
LighterAPIKeyPrivateKey: "lighter_api_key_private_key_1234567890abcdef",
},
"hyperliquid": {
Enabled: true,
HyperliquidWalletAddr: "0x1234567890abcdef1234567890abcdef12345678",
Testnet: false,
},
}
result := SanitizeExchangeConfigForLog(exchanges)
// Check Binance configuration
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)
}
// Check OKX passphrase is masked (regression: previously not covered)
okxConfig, ok := result["okx"].(map[string]interface{})
if !ok {
t.Fatal("okx config not found or wrong type")
}
maskedPassphrase, ok := okxConfig["passphrase"].(string)
if !ok {
t.Fatal("okx passphrase not found or wrong type")
}
if maskedPassphrase != "okx_****alue" {
t.Errorf("expected masked passphrase='okx_****alue', got %q", maskedPassphrase)
}
// Check Lighter API key private key is masked (regression: previously not covered)
lighterConfig, ok := result["lighter"].(map[string]interface{})
if !ok {
t.Fatal("lighter config not found or wrong type")
}
maskedLighterAPIKey, ok := lighterConfig["lighter_api_key_private_key"].(string)
if !ok {
t.Fatal("lighter_api_key_private_key not found or wrong type")
}
if maskedLighterAPIKey != "ligh****cdef" {
t.Errorf("expected masked lighter_api_key_private_key='ligh****cdef', got %q", maskedLighterAPIKey)
}
// Check Hyperliquid configuration
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")
}
// Wallet address should not be masked
if walletAddr != "0x1234567890abcdef1234567890abcdef12345678" {
t.Errorf("wallet address should not be masked, got %q", walletAddr)
}
}
// TestSanitizeExchangeConfigForLog_NoPlaintextSecrets renders the sanitized log
// output exactly as the handler does (`%+v`) and asserts that no plaintext
// secret — including the passphrase and lighter API key private key that were
// historically not redacted — survives into the log line.
func TestSanitizeExchangeConfigForLog_NoPlaintextSecrets(t *testing.T) {
secrets := map[string]string{
"api_key": "binance_api_key_1234567890abcdef",
"secret_key": "binance_secret_key_1234567890abcdef",
"passphrase": "okx_passphrase_supersecret_value",
"aster_private_key": "aster_private_key_1234567890abcdef",
"lighter_private_key": "lighter_private_key_1234567890abcdef",
"lighter_api_key_private_key": "lighter_api_key_private_key_1234567890abcdef",
}
exchanges := map[string]ExchangeConfigUpdate{
"okx": {
Enabled: true,
APIKey: secrets["api_key"],
SecretKey: secrets["secret_key"],
Passphrase: secrets["passphrase"],
AsterPrivateKey: secrets["aster_private_key"],
LighterPrivateKey: secrets["lighter_private_key"],
LighterAPIKeyPrivateKey: secrets["lighter_api_key_private_key"],
},
}
rendered := fmt.Sprintf("%+v", SanitizeExchangeConfigForLog(exchanges))
for field, secret := range secrets {
if strings.Contains(rendered, secret) {
t.Errorf("sanitized log leaked plaintext %s: %q present in %q", field, secret, rendered)
}
}
}
func TestMaskEmail(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "Empty email",
input: "",
expected: "",
},
{
name: "Invalid format",
input: "notanemail",
expected: "****",
},
{
name: "Normal email",
input: "user@example.com",
expected: "us****@example.com",
},
{
name: "Short username",
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)
}
})
}
}