Files
go_blog/middleware/csrf_test.go
T
kevin 38cd09f723 fix: 安全加固,修复 P0/P1 安全漏洞
P0(高危):
- 新增全局 CSRF 中间件(同步器令牌),覆盖全部 30 个表单与 AJAX 请求
- 修复附件上传/列表/删除越权(IDOR),增加 admin/上传者/文章作者所有权校验
- 登录/注册成功后会话轮换,修复会话固定
- 会话密钥改用 crypto/rand 生成,配置缺失 secret 时拒绝启动

P1(中危):
- session 与 comment_uid cookie 增加 Secure/SameSite 标志
- 新增安全响应头:CSP、X-Content-Type-Options、X-Frame-Options、HSTS 等
- 新增 web.trusted_proxies 配置,修复 X-Forwarded-For 伪造
- 修复浏览量记录 goroutine 访问已回收 gin.Context 的数据竞争

补充 17 个安全回归测试(middleware/handlers),go test -race 全绿
2026-08-19 12:33:09 +08:00

155 lines
4.4 KiB
Go

package middleware
import (
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
)
func newCSRFTestRouter() *gin.Engine {
gin.SetMode(gin.TestMode)
r := gin.New()
store := cookie.NewStore([]byte("test-secret"))
r.Use(sessions.Sessions("test_session", store))
r.Use(CSRFProtect())
r.GET("/form", func(c *gin.Context) {
c.String(http.StatusOK, "TOKEN="+c.GetString(CSRFContextKey))
})
r.HEAD("/form", func(c *gin.Context) {
c.String(http.StatusOK, "TOKEN="+c.GetString(CSRFContextKey))
})
r.POST("/action", func(c *gin.Context) {
c.String(http.StatusOK, "ok")
})
return r
}
// tokenFromForm performs GET /form with the given session cookie and returns
// the issued CSRF token plus the (possibly new) session cookie.
func tokenFromForm(t *testing.T, r *gin.Engine, sessionCookie string) (token, cookie string) {
t.Helper()
req := httptest.NewRequest(http.MethodGet, "/form", nil)
if sessionCookie != "" {
req.Header.Set("Cookie", sessionCookie)
}
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("GET /form: status = %d, want 200", w.Code)
}
body := w.Body.String()
const prefix = "TOKEN="
if !strings.HasPrefix(body, prefix) {
t.Fatalf("GET /form: unexpected body %q", body)
}
token = strings.TrimPrefix(body, prefix)
cookie = w.Header().Get("Set-Cookie")
return token, cookie
}
func postAction(r *gin.Engine, sessionCookie, token string, useHeader bool) *httptest.ResponseRecorder {
form := url.Values{}
if !useHeader {
form.Set(CSRFFieldName, token)
}
req := httptest.NewRequest(http.MethodPost, "/action", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
if sessionCookie != "" {
req.Header.Set("Cookie", sessionCookie)
}
if useHeader {
req.Header.Set(CSRFHeaderName, token)
}
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
return w
}
func TestCSRFTokenIssuedOnGET(t *testing.T) {
r := newCSRFTestRouter()
token, cookie := tokenFromForm(t, r, "")
if token == "" {
t.Fatal("expected a token to be issued on GET")
}
if !strings.Contains(cookie, "test_session=") {
t.Fatalf("expected session cookie to be set, got %q", cookie)
}
// A second GET with the same session must return the same token.
token2, _ := tokenFromForm(t, r, cookie)
if token2 != token {
t.Fatalf("token changed between requests: %q vs %q", token, token2)
}
}
func TestCSRFPostRejectedWithoutToken(t *testing.T) {
r := newCSRFTestRouter()
_, cookie := tokenFromForm(t, r, "")
w := postAction(r, cookie, "", false)
if w.Code != http.StatusForbidden {
t.Fatalf("POST without token: status = %d, want 403", w.Code)
}
}
func TestCSRFPostRejectedWithWrongToken(t *testing.T) {
r := newCSRFTestRouter()
_, cookie := tokenFromForm(t, r, "")
w := postAction(r, cookie, "bogus-token", false)
if w.Code != http.StatusForbidden {
t.Fatalf("POST with wrong token: status = %d, want 403", w.Code)
}
}
func TestCSRFPostRejectedWithoutSession(t *testing.T) {
r := newCSRFTestRouter()
// No prior GET: no session, no token issued.
w := postAction(r, "", "some-token", false)
if w.Code != http.StatusForbidden {
t.Fatalf("POST without session: status = %d, want 403", w.Code)
}
}
func TestCSRFPostAcceptedWithFormField(t *testing.T) {
r := newCSRFTestRouter()
token, cookie := tokenFromForm(t, r, "")
w := postAction(r, cookie, token, false)
if w.Code != http.StatusOK {
t.Fatalf("POST with valid token: status = %d, want 200 (body: %s)", w.Code, w.Body.String())
}
}
func TestCSRFPostAcceptedWithHeader(t *testing.T) {
r := newCSRFTestRouter()
token, cookie := tokenFromForm(t, r, "")
w := postAction(r, cookie, token, true)
if w.Code != http.StatusOK {
t.Fatalf("POST with token in header: status = %d, want 200 (body: %s)", w.Code, w.Body.String())
}
}
func TestCSRFSafeMethodsPassWithoutToken(t *testing.T) {
r := newCSRFTestRouter()
// GET and HEAD are registered routes; OPTIONS is not (gin does not
// auto-register it), so it falls to noRoute - but in all cases the CSRF
// middleware itself must not reject with 403.
for _, method := range []string{http.MethodGet, http.MethodHead, http.MethodOptions} {
req := httptest.NewRequest(method, "/form", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code == http.StatusForbidden {
t.Fatalf("%s /form: status = 403, CSRF middleware must not reject safe methods", method)
}
}
}