Skip to content

Commit 96954de

Browse files
the-dev-zclaude
andcommitted
security: comprehensive security fixes based on audit report
- Fix sensitive config file exposure (add logger/config.telegram.json to .gitignore) - Create logger/config.telegram.json.example template - Fix CORS configuration to use ALLOWED_ORIGINS env var (api/server.go) - Enforce JWT_SECRET requirement in production (main.go) - Fix 20+ unsafe type assertions across trader modules - trader/aster_trader.go: 8 fixes - trader/binance_futures.go: 3 fixes - trader/auto_trader.go: 7 fixes - trader/hyperliquid_trader.go: 2 fixes - Create trader/utils.go with safe type conversion helpers - SafeFloat64(), SafeString(), SafeInt() - Remove sensitive console.log in production - web/src/components/traders/ExchangeConfigModal.tsx - web/src/components/AITradersPage.tsx - Fix undefined variable $RAW_KEY -> $DATA_KEY (scripts/generate_data_key.sh) - Update .env.example with new security variables - Update docker-compose.yml environment variables - Update README.md Quick Start with security setup steps - Prevents runtime panics from unsafe type assertions - Enforces secure configuration in production - Restricts CORS to authorized origins only - Protects sensitive data from console output - Fixed issues from audit report dated 2025/11/17 - Addressed 1 Critical + 31 High priority issues - Total: 829 issues reviewed, P0/P1 issues resolved 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent a4ea480 commit 96954de

File tree

15 files changed

+349
-36
lines changed

15 files changed

+349
-36
lines changed

.env.example

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,33 @@ NOFX_FRONTEND_PORT=3000
1212
# System timezone for container time synchronization
1313
NOFX_TIMEZONE=Asia/Shanghai
1414

15+
# ============================================================================
16+
# 🔐 Security Configuration (Required for Production)
17+
# ============================================================================
18+
19+
# JWT Secret Key (REQUIRED in production)
20+
# Used for JWT token signing and verification
21+
# Generate a secure random key: openssl rand -base64 64
22+
# JWT_SECRET=your-secure-jwt-secret-minimum-32-characters
23+
24+
# Environment Type (Optional)
25+
# Options: development, production, prod
26+
# In production mode, JWT_SECRET is mandatory
27+
# ENVIRONMENT=development
28+
29+
# CORS Allowed Origins (Optional)
30+
# Comma-separated list of allowed origins for API access
31+
# Default: http://localhost:5173,http://localhost:3000
32+
# Production example: https://your-domain.com,https://app.your-domain.com
33+
# ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000
34+
35+
# ============================================================================
36+
# 📊 Market Data API Configuration (Optional - Free Tier)
37+
# ============================================================================
38+
39+
# Alpha Vantage API Key (Optional)
40+
# Used for US stock market data (S&P 500 status) to enhance AI decision context
41+
# Free tier: 500 API calls/day (sufficient for trading bot usage)
42+
# Register free API key at: https://www.alphavantage.co/support/#api-key
43+
# If not set, the system will skip US stock data (VIX and Binance data still work)
44+
# ALPHA_VANTAGE_API_KEY=

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ config.db*
3434
nofx.db
3535
configbak.json
3636

37+
# Logger 配置(包含敏感信息)
38+
logger/config.telegram.json
39+
3740
# 决策日志
3841
decision_logs/
3942
coin_pool_cache/

README.md

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -277,14 +277,28 @@ Docker automatically handles all dependencies (Go, Node.js, TA-Lib, SQLite) and
277277

278278
#### Step 1: Prepare Configuration
279279
```bash
280-
# Copy configuration template
280+
# 1. Copy environment variables template
281+
cp .env.example .env
282+
283+
# 2. Generate security keys (Important for production!)
284+
# Generate JWT secret
285+
openssl rand -base64 64
286+
287+
# Edit .env and set JWT_SECRET with the generated key
288+
nano .env # Add: JWT_SECRET=your-generated-key
289+
290+
# 3. Copy configuration template
281291
cp config.json.example config.json
282292

283-
# Edit and fill in your API keys
293+
# 4. Edit and fill in your API keys
284294
nano config.json # or use any editor
285295
```
286296

287-
⚠️ **Note**: Basic config.json is still needed for some settings, but ~~trader configurations~~ are now done through the web interface.
297+
⚠️ **Security Notes**:
298+
- **JWT_SECRET** is required for production environments
299+
- Set **ENVIRONMENT=production** in .env for production deployment
300+
- Configure **ALLOWED_ORIGINS** if deploying to a custom domain
301+
- Basic config.json is still needed for some settings, but ~~trader configurations~~ are now done through the web interface.
288302

289303
#### Step 2: One-Click Start
290304
```bash

api/server.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"nofx/hook"
1616
"nofx/manager"
1717
"nofx/trader"
18+
"os"
1819
"strconv"
1920
"strings"
2021
"time"
@@ -63,7 +64,32 @@ func NewServer(traderManager *manager.TraderManager, database *config.Database,
6364
// corsMiddleware CORS中间件
6465
func corsMiddleware() gin.HandlerFunc {
6566
return func(c *gin.Context) {
66-
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
67+
origin := c.GetHeader("Origin")
68+
69+
// 從環境變數讀取允許的來源,多個來源用逗號分隔
70+
allowedOrigins := os.Getenv("ALLOWED_ORIGINS")
71+
if allowedOrigins == "" {
72+
// 開發環境預設值
73+
allowedOrigins = "http://localhost:5173,http://localhost:3000"
74+
}
75+
76+
// 檢查請求來源是否在允許列表中
77+
allowed := false
78+
for _, allowedOrigin := range strings.Split(allowedOrigins, ",") {
79+
allowedOrigin = strings.TrimSpace(allowedOrigin)
80+
if origin == allowedOrigin {
81+
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
82+
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
83+
allowed = true
84+
break
85+
}
86+
}
87+
88+
// 如果來源不在允許列表中,仍然設置其他 CORS headers 但不設置 Allow-Origin
89+
if !allowed && origin != "" {
90+
log.Printf("⚠️ CORS: 拒絕來自未授權來源的請求: %s", origin)
91+
}
92+
6793
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
6894
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
6995

docker-compose.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ services:
2222
- AI_MAX_TOKENS=4000 # AI响应的最大token数(默认2000,建议4000-8000)
2323
- DATA_ENCRYPTION_KEY=${DATA_ENCRYPTION_KEY} # 数据库加密密钥
2424
- JWT_SECRET=${JWT_SECRET} # JWT认证密钥
25+
- ENVIRONMENT=${ENVIRONMENT:-production} # 环境类型 (development/production)
26+
- ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-http://localhost:5173,http://localhost:3000} # CORS允许的来源
2527
networks:
2628
- nofx-network
2729
healthcheck:
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"traders": [
3+
{
4+
"id": "trader1",
5+
"name": "AI Trader 1",
6+
"enabled": true,
7+
"ai_model": "deepseek",
8+
"exchange": "binance",
9+
"binance_api_key": "YOUR_BINANCE_API_KEY_HERE",
10+
"binance_secret_key": "YOUR_BINANCE_SECRET_KEY_HERE",
11+
"deepseek_key": "YOUR_DEEPSEEK_API_KEY_HERE",
12+
"initial_balance": 1000,
13+
"scan_interval_minutes": 3
14+
}
15+
],
16+
"use_default_coins": true,
17+
"default_coins": ["BTCUSDT", "ETHUSDT", "SOLUSDT"],
18+
"api_server_port": 8080,
19+
"leverage": {
20+
"btc_eth_leverage": 5,
21+
"altcoin_leverage": 5
22+
},
23+
"log": {
24+
"level": "info",
25+
"telegram": {
26+
"enabled": true,
27+
"bot_token": "YOUR_TELEGRAM_BOT_TOKEN_HERE",
28+
"chat_id": -1001234567890,
29+
"min_level": "error"
30+
}
31+
},
32+
"_comment": "日志配置说明:level 可选值为 debug/info/warn/error,默认 info。telegram 部分作为可选配置, Telegram 推送默认为 error/fatal/panic 级别,min_level 如果设置为warn,则推送warn级别及以上的日志"
33+
}

main.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -209,13 +209,25 @@ func main() {
209209
// 回退到数据库配置
210210
jwtSecret, _ = database.GetSystemConfig("jwt_secret")
211211
if jwtSecret == "" {
212-
jwtSecret = "your-jwt-secret-key-change-in-production-make-it-long-and-random"
213-
log.Printf("⚠️ 使用默认JWT密钥,建议使用加密设置脚本生成安全密钥")
212+
// 檢查是否為生產環境
213+
env := strings.ToLower(os.Getenv("ENVIRONMENT"))
214+
if env == "" {
215+
env = strings.ToLower(os.Getenv("GO_ENV"))
216+
}
217+
218+
if env == "production" || env == "prod" {
219+
log.Fatalf("❌ 生產環境必須設置 JWT_SECRET 環境變數或在數據庫中配置 jwt_secret!")
220+
}
221+
222+
// 開發環境允許使用默認值,但發出警告
223+
jwtSecret = "dev-jwt-secret-do-not-use-in-production"
224+
log.Printf("⚠️ 使用開發環境默認JWT密鑰")
225+
log.Printf("⚠️ 生產環境請務必設置 JWT_SECRET 環境變數或使用加密設置腳本")
214226
} else {
215-
log.Printf("🔑 使用数据库中JWT密钥")
227+
log.Printf("🔑 使用數據庫中的JWT密鑰")
216228
}
217229
} else {
218-
log.Printf("🔑 使用环境变量JWT密钥")
230+
log.Printf("🔑 使用環境變數JWT密鑰")
219231
}
220232
auth.SetJWTSecret(jwtSecret)
221233

scripts/generate_data_key.sh

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,23 +121,23 @@ if [[ $REPLY =~ ^[Yy]$ ]]; then
121121
# 替换现有密钥
122122
if [[ "$OSTYPE" == "darwin"* ]]; then
123123
# macOS
124-
sed -i '' "s/^DATA_ENCRYPTION_KEY=.*/DATA_ENCRYPTION_KEY=$RAW_KEY/" .env
124+
sed -i '' "s/^DATA_ENCRYPTION_KEY=.*/DATA_ENCRYPTION_KEY=$DATA_KEY/" .env
125125
else
126126
# Linux
127-
sed -i "s/^DATA_ENCRYPTION_KEY=.*/DATA_ENCRYPTION_KEY=$RAW_KEY/" .env
127+
sed -i "s/^DATA_ENCRYPTION_KEY=.*/DATA_ENCRYPTION_KEY=$DATA_KEY/" .env
128128
fi
129129
echo -e "${GREEN}✓ .env 文件中的密钥已更新${NC}"
130130
else
131131
echo -e "${BLUE}ℹ️ 保持现有密钥不变${NC}"
132132
fi
133133
else
134134
# 追加新密钥
135-
echo "DATA_ENCRYPTION_KEY=$RAW_KEY" >> .env
135+
echo "DATA_ENCRYPTION_KEY=$DATA_KEY" >> .env
136136
echo -e "${GREEN}✓ 密钥已保存到 .env 文件${NC}"
137137
fi
138138
else
139139
# 创建新的 .env 文件
140-
echo "DATA_ENCRYPTION_KEY=$RAW_KEY" > .env
140+
echo "DATA_ENCRYPTION_KEY=$DATA_KEY" > .env
141141
echo -e "${GREEN}✓ 密钥已保存到 .env 文件${NC}"
142142
fi
143143
fi

trader/aster_trader.go

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -489,12 +489,27 @@ func (t *AsterTrader) GetBalance() (map[string]interface{}, error) {
489489
totalMarginUsed := 0.0
490490
realUnrealizedPnl := 0.0
491491
for _, pos := range positions {
492-
markPrice := pos["markPrice"].(float64)
493-
quantity := pos["positionAmt"].(float64)
492+
// 安全地提取浮点数值,避免 panic
493+
markPrice, err := SafeFloat64(pos, "markPrice")
494+
if err != nil {
495+
log.Printf("⚠️ 无法解析 markPrice: %v", err)
496+
continue
497+
}
498+
499+
quantity, err := SafeFloat64(pos, "positionAmt")
500+
if err != nil {
501+
log.Printf("⚠️ 无法解析 positionAmt: %v", err)
502+
continue
503+
}
494504
if quantity < 0 {
495505
quantity = -quantity
496506
}
497-
unrealizedPnl := pos["unRealizedProfit"].(float64)
507+
508+
unrealizedPnl, err := SafeFloat64(pos, "unRealizedProfit")
509+
if err != nil {
510+
log.Printf("⚠️ 无法解析 unRealizedProfit: %v", err)
511+
continue
512+
}
498513
realUnrealizedPnl += unrealizedPnl
499514

500515
leverage := 10
@@ -544,11 +559,21 @@ func (t *AsterTrader) GetPositions() ([]map[string]interface{}, error) {
544559
continue // 跳过空仓位
545560
}
546561

547-
entryPrice, _ := strconv.ParseFloat(pos["entryPrice"].(string), 64)
548-
markPrice, _ := strconv.ParseFloat(pos["markPrice"].(string), 64)
549-
unRealizedProfit, _ := strconv.ParseFloat(pos["unRealizedProfit"].(string), 64)
550-
leverageVal, _ := strconv.ParseFloat(pos["leverage"].(string), 64)
551-
liquidationPrice, _ := strconv.ParseFloat(pos["liquidationPrice"].(string), 64)
562+
// 安全地提取并解析价格数据
563+
entryPriceStr, _ := SafeString(pos, "entryPrice")
564+
entryPrice, _ := strconv.ParseFloat(entryPriceStr, 64)
565+
566+
markPriceStr, _ := SafeString(pos, "markPrice")
567+
markPrice, _ := strconv.ParseFloat(markPriceStr, 64)
568+
569+
unRealizedProfitStr, _ := SafeString(pos, "unRealizedProfit")
570+
unRealizedProfit, _ := strconv.ParseFloat(unRealizedProfitStr, 64)
571+
572+
leverageStr, _ := SafeString(pos, "leverage")
573+
leverageVal, _ := strconv.ParseFloat(leverageStr, 64)
574+
575+
liquidationPriceStr, _ := SafeString(pos, "liquidationPrice")
576+
liquidationPrice, _ := strconv.ParseFloat(liquidationPriceStr, 64)
552577

553578
// 判断方向(与Binance一致)
554579
side := "long"
@@ -718,7 +743,12 @@ func (t *AsterTrader) CloseLong(symbol string, quantity float64) (map[string]int
718743

719744
for _, pos := range positions {
720745
if pos["symbol"] == symbol && pos["side"] == "long" {
721-
quantity = pos["positionAmt"].(float64)
746+
qty, err := SafeFloat64(pos, "positionAmt")
747+
if err != nil {
748+
log.Printf("⚠️ 无法解析 positionAmt: %v", err)
749+
continue
750+
}
751+
quantity = qty
722752
break
723753
}
724754
}
@@ -801,7 +831,12 @@ func (t *AsterTrader) CloseShort(symbol string, quantity float64) (map[string]in
801831
for _, pos := range positions {
802832
if pos["symbol"] == symbol && pos["side"] == "short" {
803833
// Aster的GetPositions已经将空仓数量转换为正数,直接使用
804-
quantity = pos["positionAmt"].(float64)
834+
qty, err := SafeFloat64(pos, "positionAmt")
835+
if err != nil {
836+
log.Printf("⚠️ 无法解析 positionAmt: %v", err)
837+
continue
838+
}
839+
quantity = qty
805840
break
806841
}
807842
}

trader/auto_trader.go

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -517,11 +517,35 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) {
517517
currentPositionKeys := make(map[string]bool)
518518

519519
for _, pos := range positions {
520-
symbol := pos["symbol"].(string)
521-
side := pos["side"].(string)
522-
entryPrice := pos["entryPrice"].(float64)
523-
markPrice := pos["markPrice"].(float64)
524-
quantity := pos["positionAmt"].(float64)
520+
symbol, err := SafeString(pos, "symbol")
521+
if err != nil {
522+
log.Printf("⚠️ 无法解析 symbol: %v", err)
523+
continue
524+
}
525+
526+
side, err := SafeString(pos, "side")
527+
if err != nil {
528+
log.Printf("⚠️ 无法解析 side: %v", err)
529+
continue
530+
}
531+
532+
entryPrice, err := SafeFloat64(pos, "entryPrice")
533+
if err != nil {
534+
log.Printf("⚠️ 无法解析 entryPrice: %v", err)
535+
continue
536+
}
537+
538+
markPrice, err := SafeFloat64(pos, "markPrice")
539+
if err != nil {
540+
log.Printf("⚠️ 无法解析 markPrice: %v", err)
541+
continue
542+
}
543+
544+
quantity, err := SafeFloat64(pos, "positionAmt")
545+
if err != nil {
546+
log.Printf("⚠️ 无法解析 positionAmt: %v", err)
547+
continue
548+
}
525549
if quantity < 0 {
526550
quantity = -quantity // 空仓数量为负,转为正数
527551
}
@@ -531,8 +555,17 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) {
531555
continue
532556
}
533557

534-
unrealizedPnl := pos["unRealizedProfit"].(float64)
535-
liquidationPrice := pos["liquidationPrice"].(float64)
558+
unrealizedPnl, err := SafeFloat64(pos, "unRealizedProfit")
559+
if err != nil {
560+
log.Printf("⚠️ 无法解析 unRealizedProfit: %v", err)
561+
continue
562+
}
563+
564+
liquidationPrice, err := SafeFloat64(pos, "liquidationPrice")
565+
if err != nil {
566+
log.Printf("⚠️ 无法解析 liquidationPrice: %v", err)
567+
continue
568+
}
536569

537570
// 计算占用保证金(估算)
538571
leverage := 10 // 默认值,实际应该从持仓信息获取

0 commit comments

Comments
 (0)