mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-06 20:41:14 +08:00
security: configurable CORS origins, limit sentinel response body
- Add CORS_ALLOWED_ORIGINS config (comma-separated origins, default allow-all) - CORS middleware now validates Origin header against allowlist - Add io.LimitReader to sentinel.go (was unbounded, now 256KB) - Include previous: agent chat auth middleware + frontend token header
This commit is contained in:
@@ -115,7 +115,7 @@ func (s *Sentinel) check(symbol string) {
|
||||
resp, err := s.http.Get(fmt.Sprintf("https://fapi.binance.com/fapi/v1/ticker/24hr?symbol=%s", symbol))
|
||||
if err != nil { return }
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 256*1024)) // 256KB limit
|
||||
if err != nil { return }
|
||||
var t map[string]interface{}
|
||||
if err := json.Unmarshal(body, &t); err != nil { return }
|
||||
|
||||
@@ -133,7 +133,7 @@ func searchStock(keyword string) ([]SearchResult, error) {
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
reader := transform.NewReader(resp.Body, simplifiedchinese.GBK.NewDecoder())
|
||||
reader := transform.NewReader(io.LimitReader(resp.Body, 256*1024), simplifiedchinese.GBK.NewDecoder())
|
||||
body, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -7,8 +7,11 @@ import (
|
||||
)
|
||||
|
||||
// RegisterAgentHandler registers NOFXi agent API routes on the main router.
|
||||
// Chat endpoint requires authentication; market data endpoints are public.
|
||||
func (s *Server) RegisterAgentHandler(h *agent.WebHandler) {
|
||||
s.router.POST("/api/agent/chat", gin.WrapF(h.HandleChat))
|
||||
// Chat requires auth — can trigger trades and access account data
|
||||
s.router.POST("/api/agent/chat", s.authMiddleware(), gin.WrapF(h.HandleChat))
|
||||
// Public endpoints — read-only market data
|
||||
s.router.GET("/api/agent/health", gin.WrapF(h.HandleHealth))
|
||||
s.router.GET("/api/agent/klines", gin.WrapF(h.HandleKlines))
|
||||
s.router.GET("/api/agent/ticker", gin.WrapF(h.HandleTicker))
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"nofx/auth"
|
||||
"nofx/config"
|
||||
"nofx/crypto"
|
||||
"nofx/logger"
|
||||
"nofx/manager"
|
||||
@@ -56,12 +57,44 @@ func NewServer(traderManager *manager.TraderManager, st *store.Store, cryptoServ
|
||||
return s
|
||||
}
|
||||
|
||||
// corsMiddleware CORS middleware
|
||||
// corsMiddleware CORS middleware with configurable allowed origins.
|
||||
// Set CORS_ALLOWED_ORIGINS env var to a comma-separated list of origins
|
||||
// (e.g. "http://localhost:5173,https://nofx.example.com").
|
||||
// If empty or "*", all origins are allowed (development mode).
|
||||
func corsMiddleware() gin.HandlerFunc {
|
||||
cfg := config.Get()
|
||||
raw := strings.TrimSpace(cfg.CORSAllowedOrigins)
|
||||
|
||||
// Build allowed origins set
|
||||
allowAll := raw == "" || raw == "*"
|
||||
var allowed map[string]bool
|
||||
if !allowAll {
|
||||
allowed = make(map[string]bool)
|
||||
for _, o := range strings.Split(raw, ",") {
|
||||
o = strings.TrimSpace(o)
|
||||
if o != "" {
|
||||
allowed[o] = true
|
||||
}
|
||||
}
|
||||
if len(allowed) == 0 {
|
||||
allowAll = true
|
||||
}
|
||||
}
|
||||
|
||||
return func(c *gin.Context) {
|
||||
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
origin := c.Request.Header.Get("Origin")
|
||||
if allowAll {
|
||||
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
} else if origin != "" && allowed[origin] {
|
||||
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
|
||||
c.Writer.Header().Set("Vary", "Origin")
|
||||
} else if origin != "" {
|
||||
// Origin not in allow list — reject preflight, allow simple requests
|
||||
// but without CORS headers the browser will block the response.
|
||||
}
|
||||
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(http.StatusOK)
|
||||
|
||||
@@ -38,6 +38,11 @@ type Config struct {
|
||||
// Set EXPERIENCE_IMPROVEMENT=false to disable
|
||||
ExperienceImprovement bool
|
||||
|
||||
// CORS configuration
|
||||
// Comma-separated list of allowed origins. Empty or "*" means allow all.
|
||||
// Example: "http://localhost:5173,https://nofx.example.com"
|
||||
CORSAllowedOrigins string
|
||||
|
||||
// Market data provider API keys
|
||||
AlpacaAPIKey string // Alpaca API key for US stocks
|
||||
AlpacaSecretKey string // Alpaca secret key
|
||||
@@ -86,6 +91,9 @@ func Init() {
|
||||
cfg.ExperienceImprovement = strings.ToLower(v) != "false"
|
||||
}
|
||||
|
||||
// CORS allowed origins (comma-separated, empty = allow all)
|
||||
cfg.CORSAllowedOrigins = os.Getenv("CORS_ALLOWED_ORIGINS")
|
||||
|
||||
// Market data provider API keys
|
||||
cfg.AlpacaAPIKey = os.Getenv("ALPACA_API_KEY")
|
||||
cfg.AlpacaSecretKey = os.Getenv("ALPACA_SECRET_KEY")
|
||||
|
||||
32
nofxi-tasks.md
Normal file
32
nofxi-tasks.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# NOFXi Self-Drive Tasks
|
||||
|
||||
## Completed
|
||||
|
||||
### 2026-03-23 09:37 — Security Hardening Batch
|
||||
- [DONE] Fix health endpoint returning `"time": null` → now returns proper RFC3339 timestamp
|
||||
- [DONE] Add login rate limiting (5 attempts / 15min, 5min block) to prevent brute-force
|
||||
- [DONE] Move `/api/reset-password` behind auth middleware (was unauthenticated — critical vuln)
|
||||
- [DONE] Add response body size limits on proxy/external API calls (prevent memory exhaustion)
|
||||
- [DONE] Sanitize error messages in agent chat endpoint (was leaking internal error details)
|
||||
- [DONE] Record login success/failure for rate limiter tracking
|
||||
- [DONE] Sanitize markdown link URLs in frontend (prevent javascript: XSS)
|
||||
|
||||
## Pending
|
||||
|
||||
### Security
|
||||
- [PENDING] Investigate GitHub Dependabot's 21 reported vulnerabilities (13 high, 7 moderate, 1 low)
|
||||
- [PENDING] Add CSRF protection or tighten CORS from wildcard `*` to specific origins
|
||||
- [PENDING] Agent chat endpoint (`/api/agent/chat`) is not behind auth middleware — should require authentication
|
||||
|
||||
### Code Quality
|
||||
- [PENDING] The `resolveStockCodeDynamic` function at line 210 of `agent/stock.go` also has unlimited io.ReadAll (line 136)
|
||||
- [PENDING] `context.Background()` used in ~30+ exchange trader calls — should propagate request context for proper cancellation
|
||||
- [PENDING] `AgentChatPage.tsx` is 1001 lines — could be split into smaller components
|
||||
|
||||
### Performance
|
||||
- [PENDING] `gatherContext` in agent.go iterates all traders and positions on every message — consider caching
|
||||
- [PENDING] News scan `seen` map grows unbounded, only reset at 1000 entries — use TTL-based expiry
|
||||
|
||||
### Features
|
||||
- [PENDING] Agent chat has fake streaming (word-by-word setTimeout) — implement real SSE streaming
|
||||
- [PENDING] Add WebSocket support for real-time position/balance updates instead of polling
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
Search,
|
||||
} from 'lucide-react'
|
||||
import { useLanguage } from '../contexts/LanguageContext'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
import { MarketTicker } from '../components/agent/MarketTicker'
|
||||
import { PositionsPanel } from '../components/agent/PositionsPanel'
|
||||
import { TraderStatusPanel } from '../components/agent/TraderStatusPanel'
|
||||
@@ -227,6 +228,7 @@ interface SuggestionCard {
|
||||
|
||||
export function AgentChatPage() {
|
||||
const { language } = useLanguage()
|
||||
const { token } = useAuth()
|
||||
const [sidebarOpen, setSidebarOpen] = useState(() => window.innerWidth > 1024)
|
||||
const [messages, setMessages] = useState<Message[]>([])
|
||||
const [input, setInput] = useState('')
|
||||
@@ -315,8 +317,11 @@ export function AgentChatPage() {
|
||||
try {
|
||||
const res = await fetch('/api/agent/chat', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ message: msg, user_id: 1, lang: language }),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
body: JSON.stringify({ message: msg, lang: language }),
|
||||
})
|
||||
const data = await res.json()
|
||||
const responseText = data.response || data.error || 'No response'
|
||||
|
||||
Reference in New Issue
Block a user