- 48 个 Go 文件所有注释(行注释/块注释/行尾注释,含 _test.go)翻译为中文 - 保留技术标识符:SECURITY_TODO(n)、unsafe-inline、sqlite/mysql、路由参数等 - 代码、字符串字面量、日志消息保持英文原文,零逻辑改动 - go build/vet 通过,go test -count=1 ./... 全绿
154 lines
4.4 KiB
Go
154 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 使用给定的会话 Cookie 执行 GET /form,并返回
|
|
// 签发的 CSRF 令牌及(可能更新的)会话 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)
|
|
}
|
|
|
|
// 相同会话的第二次 GET 必须返回相同的令牌。
|
|
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()
|
|
// 没有先前的 GET:无会话,也未签发令牌。
|
|
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 和 HEAD 已注册路由;OPTIONS 未注册(gin 不会自动注册),
|
|
// 因此会落入 noRoute——但在所有情况下,CSRF 中间件本身都不得以 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)
|
|
}
|
|
}
|
|
}
|