Cross Site Request Forgery in Gin with Hmac Signatures
Cross Site Request Forgery in Gin with Hmac Signatures — how this combination creates or exposes the vulnerability
Cross Site Request Forgery (CSRF) leverages the trust a web application places in an authenticated user’s browser. In Go frameworks such as Gin, developers sometimes rely solely on HMAC-based signatures (for example, an HMAC-SHA256 of request data) to provide integrity and authenticity. While HMAC signatures can protect against tampering in some scenarios, they do not automatically prevent CSRF when the signature mechanism is not tied to a per-user, per-session binding and when endpoints accept state-changing requests without verifying the request origin.
Consider a Gin endpoint that processes a money transfer using an HMAC signature to ensure the payload has not been altered. If the signature is computed over parameters like account and amount but does not include a per-user token or a same-site/cookie binding, an attacker can craft a malicious HTML page that submits a request with a valid HMAC. The attacker only needs to know or guess the parameters and the signing algorithm; if the victim is authenticated and the endpoint relies only on the HMAC (without a CSRF token or SameSite cookies), the request may be executed with the victim’s privileges. This is because HMAC signatures do not by themselves bind the request to a browser context; they ensure data integrity but not request origin.
A concrete pattern that creates risk looks like this: a client computes an HMAC over JSON or form values using a shared secret, sends it in a header (e.g., X-Signature), and the Gin handler verifies the signature before performing the action. If the handler does not check the Origin or Referer header, does not enforce SameSite cookie attributes, and does not require a per-session CSRF token, an attacker can host a form that replicates the required parameters and signature generation logic (or guesses/obtains a valid signature for known inputs). Because the endpoint trusts the HMAC alone, the forged request is processed as if it were legitimate.
In real-world scenarios, this can map to findings such as those in the OWASP API Top 10 (e.g., Broken Object Level Authorization or missing access controls around state-changing methods). The absence of anti-CSRF measures alongside HMAC-based integrity checks means the API’s unauthenticated or weakly bound attack surface remains vulnerable to forged requests. Attack patterns like those seen in CVE-related web exploits illustrate how forged POST requests can perform actions without the user’s intent when origin verification is omitted.
To understand the exposure, think of HMAC signatures as ensuring that the payload has not been modified, but not ensuring that the request is intentionally initiated by the user. Without additional context — such as binding the signature to a per-session token, enforcing SameSite attributes, validating Origin, or requiring a separate CSRF token — the API remains exposed to CSRF even if every request carries a valid HMAC.
Hmac Signatures-Specific Remediation in Gin — concrete code fixes
To mitigate CSRF in Gin while using HMAC signatures, combine cryptographic integrity checks with anti-CSRF tokens and secure cookie practices. Below are concrete, realistic examples that demonstrate a safer approach.
1. Include a per-session CSRF token in the HMAC and validate it server-side
Generate a cryptographically random token per session, store it server-side (or in a signed, HttpOnly cookie), and include it in the HMAC computation. This binds the signature to the user’s session, preventing attackers from forging valid requests without the token.
// Example: server-side token generation and HMAC with CSRF token
package main
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"math/big"
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
func generateCSRFToken() (string, error) {
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b := make([]byte, 32)
for i := range b {
n, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
if err != nil {
return "", err
}
b[i] = letters[int(n.Int64())%len(letters)]
}
return string(b), nil
}
func computeHMAC(data, secret, csrfToken string) string {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(data + csrfToken))
return hex.EncodeToString(mac.Sum(nil))
}
func main() {
r := gin.Default()
r.Use(func(c *gin.Context) {
// On session creation, set a CSRF token in a signed cookie and also in secure session store
csrfToken, _ := generateCSRFToken()
// In practice, store csrfToken in server-side session associated with a session ID cookie
c.Set("csrfToken", csrfToken)
c.SetCookie("session_id", "session-abc123", 3600, "/", "", false, true)
c.SetCookie("csrf_token", csrfToken, 3600, "/", "", false, true)
c.Next()
})
r.POST("/transfer", func(c *gin.Context) {
var payload struct {
Account string `json:"account"`
Amount string `json:"amount"`
}
if err := c.BindJSON(&payload); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "invalid body"})
return
}
csrfToken, exists := c.Get("csrfToken")
if !exists {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "missing csrf token"})
return
}
// Recompute expected signature from request body + CSRF token
expectedMAC := computeHMAC(payload.Account+payload.Amount, "my-secret-key", csrfToken.(string))
providedMAC := c.GetHeader("X-Signature")
if !hmac.Equal([]byte(expectedMAC), []byte(providedMAC)) {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "invalid signature"})
return
}
// Additional origin/referer checks
origin := c.GetHeader("Origin")
if origin == "" {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "missing origin"})
return
}
// Optionally validate origin against a whitelist
// Proceed with the action, knowing the request is authenticated and CSRF-protected
c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
r.Run()
}
2. Set SameSite and Secure cookie attributes and validate Origin/Referer
Ensure session and CSRF cookies use SameSite=Strict or Lax, Secure, and HttpOnly. Additionally, validate Origin or Referer headers on sensitive endpoints to defend against cross-origin forged requests.
// Example: secure cookie settings and origin validation in Gin
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.Use(func(c *gin.Context) {
// Set secure cookies for session and CSRF token
c.SetCookie("session_id", "session-abc123", 3600, "/", "example.com", true, true) // Secure: true, HttpOnly: true
c.SetCookie("csrf_token", "random-csrf-value", 3600, "/", "example.com", true, true)
c.Next()
})
r.POST("/transfer", func(c *gin.Context) {
// Validate Origin to defend against CSRF from external sites
origin := c.GetHeader("Origin")
referer := c.GetHeader("Referer")
allowedOrigin := "https://myapp.example.com"
if origin != allowedOrigin && referer != allowedOrigin {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "invalid origin"})
return
}
// Verify signature and other checks here...
c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
r.Run()
}
3. Best-practice summary for Gin APIs using HMAC
- Include a per-session CSRF token inside the HMAC computation so the signature is bound to the user’s session.
- Use SameSite=Strict or Lax, Secure, and HttpOnly cookie attributes for session and CSRF cookies.
- Validate Origin and/or Referer headers on state-changing endpoints as an additional layer.
- Do not rely on HMAC alone to prevent CSRF; treat it as integrity protection, not an anti-CSRF mechanism.
- Apply these measures to endpoints identified in the scan findings, especially those flagged under Authentication, BOLA/IDOR, and Unsafe Consumption checks.