diff --git a/frontend/admin/src/lib/device-fingerprint.test.ts b/frontend/admin/src/lib/device-fingerprint.test.ts index fa37da5..ef8a77b 100644 --- a/frontend/admin/src/lib/device-fingerprint.test.ts +++ b/frontend/admin/src/lib/device-fingerprint.test.ts @@ -3,7 +3,6 @@ import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest' import { getDeviceFingerprint, clearDeviceFingerprint, - type DeviceFingerprint, } from './device-fingerprint' describe('device-fingerprint', () => { @@ -99,21 +98,10 @@ describe('device-fingerprint', () => { describe('browser detection', () => { it('should detect browser from user agent', () => { - // 模拟不同的 User-Agent - const testCases = [ - { ua: 'Mozilla/5.0 Chrome/120.0', expected: 'Chrome' }, - { ua: 'Mozilla/5.0 Firefox/120.0', expected: 'Firefox' }, - { ua: 'Mozilla/5.0 Safari/120.0', expected: 'Safari' }, - { ua: 'Mozilla/5.0 Edge/120.0', expected: 'Edge' }, - { ua: 'Mozilla/5.0 Opera/120.0', expected: 'Opera' }, - ] - - testCases.forEach(({ ua, expected }) => { - // 注意:实际测试中 navigator.userAgent 是只读的 - // 这里主要验证函数能正常工作 - const fingerprint = getDeviceFingerprint() - expect(fingerprint.device_browser).toBeTruthy() - }) + // 注意:实际测试中 navigator.userAgent 是只读的 + // 这里主要验证函数能正常工作 + const fingerprint = getDeviceFingerprint() + expect(fingerprint.device_browser).toBeTruthy() }) }) diff --git a/frontend/admin/src/lib/http/index.test.ts b/frontend/admin/src/lib/http/index.test.ts index 7d5abed..3d43735 100644 --- a/frontend/admin/src/lib/http/index.test.ts +++ b/frontend/admin/src/lib/http/index.test.ts @@ -1,8 +1,6 @@ import { describe, expect, it } from 'vitest' import * as httpIndex from './index' -import * as client from './client' -import * as authSession from './auth-session' import * as errors from '@/lib/errors' describe('lib/http/index', () => { diff --git a/internal/api/handler/user_handler.go b/internal/api/handler/user_handler.go index 07a3aaf..d96df7d 100644 --- a/internal/api/handler/user_handler.go +++ b/internal/api/handler/user_handler.go @@ -185,6 +185,22 @@ func (h *UserHandler) UpdateUser(c *gin.Context) { return } + // Authorization: only self or admin can update user profile + currentUserID := c.GetInt64("user_id") + isAdmin := false + if roles, ok := c.Get("user_roles"); ok { + for _, role := range roles.([]*domain.Role) { + if role.Code == "admin" { + isAdmin = true + break + } + } + } + if currentUserID != id && !isAdmin { + c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "permission denied"}) + return + } + var req struct { Email *string `json:"email"` Nickname *string `json:"nickname"` diff --git a/internal/api/middleware/cors.go b/internal/api/middleware/cors.go index 5544e93..328c192 100644 --- a/internal/api/middleware/cors.go +++ b/internal/api/middleware/cors.go @@ -10,11 +10,22 @@ import ( ) var corsConfig = config.CORSConfig{ - AllowedOrigins: []string{"*"}, - AllowCredentials: true, + AllowedOrigins: []string{}, // 默认为空,必须显式配置 + AllowCredentials: false, // 默认关闭凭证,必须显式启用 +} + +// init 在包初始化时检测危险的 CORS 配置组合 +func init() { + // 检测危险的通配符 + Credentials 组合 + for _, origin := range corsConfig.AllowedOrigins { + if origin == "*" && corsConfig.AllowCredentials { + panic("CORS 配置错误: AllowedOrigins 包含 '*' 且 AllowCredentials 为 true 是危险组合") + } + } } func SetCORSConfig(cfg config.CORSConfig) { + // 注意:显式配置危险组合时不会panic,但生产环境应避免使用 corsConfig = cfg } diff --git a/internal/repository/device.go b/internal/repository/device.go index 590220a..3251526 100644 --- a/internal/repository/device.go +++ b/internal/repository/device.go @@ -236,10 +236,11 @@ func (r *DeviceRepository) ListAll(ctx context.Context, params *ListDevicesParam if params.IsTrusted != nil { query = query.Where("is_trusted = ?", *params.IsTrusted) } - // 按关键词筛选(设备名/IP/位置) + // 按关键词筛选(设备名/IP/位置)- 转义 LIKE 特殊字符 if params.Keyword != "" { - search := "%" + params.Keyword + "%" - query = query.Where("device_name LIKE ? OR ip LIKE ? OR location LIKE ?", search, search, search) + escapedKeyword := escapeLikePattern(params.Keyword) + pattern := "%" + escapedKeyword + "%" + query = query.Where("device_name LIKE ? OR ip LIKE ? OR location LIKE ?", pattern, pattern, pattern) } // 获取总数 diff --git a/internal/service/auth.go b/internal/service/auth.go index a8aa093..20bc12f 100644 --- a/internal/service/auth.go +++ b/internal/service/auth.go @@ -783,13 +783,16 @@ func (s *AuthService) RefreshToken(ctx context.Context, refreshToken string) (*L } // Token Rotation: 使旧的 refresh token 失效,防止无限刷新 + // 安全敏感修复:黑名单写入失败时必须 fail closed if s.cache != nil { blacklistKey := tokenBlacklistPrefix + claims.JTI // TTL 设置为 refresh token 的剩余有效期 if claims.ExpiresAt != nil { remaining := time.Until(claims.ExpiresAt.Time) if remaining > 0 { - _ = s.cache.Set(ctx, blacklistKey, "1", 5*time.Minute, remaining) + if err := s.cache.Set(ctx, blacklistKey, "1", 5*time.Minute, remaining); err != nil { + return nil, fmt.Errorf("token revocation failed: %w", err) + } } } }