//go:build security package server_test import ( "bytes" "encoding/json" "fmt" "net/http" "net/http/httptest" "strings" "testing" "github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/handler" "github.com/Wei-Shaw/sub2api/internal/server/middleware" "github.com/Wei-Shaw/sub2api/internal/service" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // TestAPI_Security_BrokenObjectLevelAuthorization 测试IDOR/BOLA漏洞 // OWASP API Top 10 #1: Broken Object Level Authorization func TestAPI_Security_BrokenObjectLevelAuthorization(t *testing.T) { gin.SetMode(gin.TestMode) tests := []struct { name string setup func() (*gin.Engine, map[string]string) method string path string body string userToken string wantStatus int wantErrContain string }{ { name: "用户不应访问其他用户的API Key详情", setup: func() (*gin.Engine, map[string]string) { // 返回测试路由和token映射 router, tokens := setupSecurityTestEnvironment() return router, tokens }, method: http.MethodGet, path: "/api/v1/keys/99999", // 其他用户的key ID userToken: "user_a_token", wantStatus: http.StatusForbidden, wantErrContain: "forbidden", }, { name: "用户不应修改其他用户的API Key", setup: func() (*gin.Engine, map[string]string) { router, tokens := setupSecurityTestEnvironment() return router, tokens }, method: http.MethodPut, path: "/api/v1/keys/99999", body: `{"name": "hacked"}`, userToken: "user_a_token", wantStatus: http.StatusForbidden, wantErrContain: "forbidden", }, { name: "用户不应删除其他用户的API Key", setup: func() (*gin.Engine, map[string]string) { router, tokens := setupSecurityTestEnvironment() return router, tokens }, method: http.MethodDelete, path: "/api/v1/keys/99999", userToken: "user_a_token", wantStatus: http.StatusForbidden, wantErrContain: "forbidden", }, { name: "普通用户不应访问管理员接口", setup: func() (*gin.Engine, map[string]string) { router, tokens := setupSecurityTestEnvironment() return router, tokens }, method: http.MethodGet, path: "/api/v1/admin/users", userToken: "user_token", wantStatus: http.StatusForbidden, wantErrContain: "admin", }, { name: "普通用户不应查看其他用户信息", setup: func() (*gin.Engine, map[string]string) { router, tokens := setupSecurityTestEnvironment() return router, tokens }, method: http.MethodGet, path: "/api/v1/admin/users/2", userToken: "user_token", wantStatus: http.StatusForbidden, wantErrContain: "admin", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { router, tokens := tt.setup() var bodyReader *bytes.Reader if tt.body != "" { bodyReader = bytes.NewReader([]byte(tt.body)) } else { bodyReader = bytes.NewReader([]byte{}) } req := httptest.NewRequest(tt.method, tt.path, bodyReader) req.Header.Set("Authorization", "Bearer "+tokens[tt.userToken]) if tt.body != "" { req.Header.Set("Content-Type", "application/json") } w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, tt.wantStatus, w.Code, "状态码不匹配") if tt.wantErrContain != "" { assert.Contains(t, strings.ToLower(w.Body.String()), strings.ToLower(tt.wantErrContain)) } }) } } // TestAPI_Security_BrokenAuthentication 测试认证漏洞 // OWASP API Top 10 #2: Broken Authentication func TestAPI_Security_BrokenAuthentication(t *testing.T) { gin.SetMode(gin.TestMode) tests := []struct { name string method string path string token string body string wantStatus int wantErrContain string }{ { name: "无效令牌应返回401", method: http.MethodGet, path: "/api/v1/keys", token: "invalid_token_here", wantStatus: http.StatusUnauthorized, wantErrContain: "unauthorized", }, { name: "过期令牌应返回401", method: http.MethodGet, path: "/api/v1/keys", token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MDAwMDAwMDB9.invalid", wantStatus: http.StatusUnauthorized, wantErrContain: "unauthorized", }, { name: "空令牌应返回401", method: http.MethodGet, path: "/api/v1/keys", token: "", wantStatus: http.StatusUnauthorized, wantErrContain: "unauthorized", }, { name: "缺少Authorization头应返回401", method: http.MethodGet, path: "/api/v1/keys", token: "NO_HEADER", wantStatus: http.StatusUnauthorized, wantErrContain: "unauthorized", }, { name: "错误格式的Bearer令牌应返回401", method: http.MethodGet, path: "/api/v1/keys", token: "Basic dXNlcjpwYXNz", // Basic auth instead of Bearer wantStatus: http.StatusUnauthorized, wantErrContain: "unauthorized", }, } router, _ := setupSecurityTestEnvironment() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { req := httptest.NewRequest(tt.method, tt.path, nil) if tt.token != "NO_HEADER" { if tt.token != "" { req.Header.Set("Authorization", "Bearer "+tt.token) } else { req.Header.Set("Authorization", "") } } w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, tt.wantStatus, w.Code) if tt.wantErrContain != "" { assert.Contains(t, strings.ToLower(w.Body.String()), strings.ToLower(tt.wantErrContain)) } }) } } // TestAPI_Security_InputValidation 测试输入验证 // OWASP API Top 10 #6: Unrestricted Access to Sensitive Business Flows func TestAPI_Security_InputValidation(t *testing.T) { gin.SetMode(gin.TestMode) tests := []struct { name string method string path string body string query string wantStatus int validateFn func(t *testing.T, body string) }{ { name: "SQL注入尝试应被阻止或无害化", method: http.MethodGet, path: "/api/v1/admin/users", query: "search='; DROP TABLE users; --", wantStatus: http.StatusBadRequest, }, { name: "XSS尝试应在响应中被转义", method: http.MethodPost, path: "/api/v1/keys", body: `{"name": ""}`, wantStatus: http.StatusOK, validateFn: func(t *testing.T, body string) { // 确保响应中没有未转义的脚本标签 assert.NotContains(t, body, "