Security hardening

- JWT: validate signing algorithm (prevent alg confusion)
- Login: rate limiting (10 attempts per 5 min per IP)
- Request body: 10MB size limit (prevent DoS)
- WebSocket: require JWT auth (token query param or cookie)
- Daemon endpoints: require admin role (not just any user)
- io.LimitReader on all request body decoding
This commit is contained in:
2026-05-26 22:51:33 +02:00
parent 2de92b0375
commit 4f3113199b
5 changed files with 90 additions and 7 deletions
+41 -1
View File
@@ -2,7 +2,9 @@ package api
import (
"encoding/json"
"io"
"net/http"
"sync"
"time"
"github.com/google/uuid"
@@ -12,6 +14,8 @@ import (
"markdownhub/internal/git"
)
const maxBodySize = 10 * 1024 * 1024 // 10MB
func writeJSON(w http.ResponseWriter, status int, v interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
@@ -20,12 +24,47 @@ func writeJSON(w http.ResponseWriter, status int, v interface{}) {
func decodeBody(r *http.Request, v interface{}) error {
defer r.Body.Close()
return json.NewDecoder(r.Body).Decode(v)
limited := io.LimitReader(r.Body, maxBodySize)
return json.NewDecoder(limited).Decode(v)
}
// Simple rate limiter for login
var loginAttempts = struct {
sync.Mutex
m map[string][]time.Time
}{m: make(map[string][]time.Time)}
func isRateLimited(ip string) bool {
loginAttempts.Lock()
defer loginAttempts.Unlock()
now := time.Now()
cutoff := now.Add(-5 * time.Minute)
// Clean old entries
valid := loginAttempts.m[ip][:0]
for _, t := range loginAttempts.m[ip] {
if t.After(cutoff) {
valid = append(valid, t)
}
}
loginAttempts.m[ip] = valid
return len(valid) >= 10 // max 10 attempts per 5 minutes
}
func recordLoginAttempt(ip string) {
loginAttempts.Lock()
defer loginAttempts.Unlock()
loginAttempts.m[ip] = append(loginAttempts.m[ip], time.Now())
}
// ─── Auth ────────────────────────────────────────────────────────────────────
func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
ip := r.RemoteAddr
if isRateLimited(ip) {
writeJSON(w, 429, map[string]string{"error": "too many attempts, try again later"})
return
}
var req struct {
Email string `json:"email"`
Password string `json:"password"`
@@ -43,6 +82,7 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
"SELECT id, password_hash, is_admin, totp_secret FROM users WHERE email = ?", req.Email,
).Scan(&id, &hash, &isAdmin, &totpSecret)
if err != nil || !auth.CheckPassword(hash, req.Password) {
recordLoginAttempt(ip)
writeJSON(w, 401, map[string]string{"error": "invalid credentials"})
return
}