//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, "