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() } }