Files
go_blog/middleware/csrf.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

94 lines
2.8 KiB
Go

package middleware
import (
"crypto/rand"
"encoding/hex"
"net/http"
"strconv"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
// CSRF protection uses the synchronizer-token pattern on top of the existing
// session store:
// - Safe methods (GET/HEAD/OPTIONS): a per-session token is created on first
// use and exposed to templates / JS so it can be embedded in forms.
// - Unsafe methods (POST/PUT/PATCH/DELETE): the request must carry the token
// either as the "_csrf" form field (regular forms, multipart uploads) or
// in the "X-CSRF-Token" header (AJAX). A mismatch aborts with 403.
//
// The token is bound to the session, so it works for anonymous visitors (e.g.
// the comment form) as well as for logged-in users.
const (
// CSRFFieldName is the form field carrying the token.
CSRFFieldName = "_csrf"
// CSRFHeaderName is the HTTP header carrying the token (AJAX).
CSRFHeaderName = "X-CSRF-Token"
// CSRFSessionKey stores the token server-side.
CSRFSessionKey = "csrf_token"
// CSRFContextKey exposes the token to handlers/templates via c.Set.
CSRFContextKey = "csrf_token"
)
// newCSRFToken returns a 256-bit random hex token. A crypto/rand failure is
// unrecoverable; panic rather than degrade the defense.
func newCSRFToken() string {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
panic("csrf: failed to read random bytes: " + err.Error())
}
return hex.EncodeToString(b)
}
// csrfTokensEqual compares two tokens in constant time.
func csrfTokensEqual(a, b string) bool {
if len(a) != len(b) {
return false
}
var v byte
for i := 0; i < len(a); i++ {
v |= a[i] ^ b[i]
}
return v == 0
}
// CSRFProtect validates unsafe requests against the per-session CSRF token.
// It must be registered after the sessions middleware.
func CSRFProtect() gin.HandlerFunc {
return func(c *gin.Context) {
session := sessions.Default(c)
token, _ := session.Get(CSRFSessionKey).(string)
switch c.Request.Method {
case http.MethodGet, http.MethodHead, http.MethodOptions, http.MethodTrace:
// Safe method: make sure a token exists and hand it to the
// template layer.
if token == "" {
token = newCSRFToken()
session.Set(CSRFSessionKey, token)
_ = session.Save()
}
c.Set(CSRFContextKey, token)
c.Next()
return
}
// Unsafe method: require a matching token.
supplied := c.PostForm(CSRFFieldName)
if supplied == "" {
supplied = c.GetHeader(CSRFHeaderName)
}
if token == "" || supplied == "" || !csrfTokensEqual(token, supplied) {
c.Header("Cache-Control", "no-store")
c.Header("Content-Type", "text/plain; charset=utf-8")
c.String(http.StatusForbidden, "403 Forbidden: CSRF token missing or invalid ("+strconv.Quote(c.Request.Method)+" "+c.Request.URL.Path+")")
c.Abort()
return
}
c.Set(CSRFContextKey, token)
c.Next()
}
}