Replay Attack in Gin with Api Keys
Replay Attack in Gin with Api Keys — how this specific combination creates or exposes the vulnerability
A replay attack against a Gin service that relies solely on API keys occurs when an attacker intercepts a valid request and re-sends it to the API to gain unauthorized access or cause unintended side effects. Because API keys are typically static credentials sent in headers, query parameters, or cookies, they do not inherently prevent replay unless the protocol includes freshness guarantees. In Gin, if endpoints only validate the presence and correctness of the key without additional protections, an intercepted request such as an HTTP POST to transfer funds or change configuration can be replayed at a later time with the same effect.
For example, consider a Gin endpoint that uses an API key header but does not include a nonce or timestamp:
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.Use(apiKeyMiddleware)
r.POST("/transfer", func(c *gin.Context) {
var req struct {
To string `json:"to"`
Amount int `json:"amount"`
}
if c.BindJSON(&req) != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "invalid body"})
return
}
// Process transfer — vulnerable to replay
c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
r.Run()
}
func apiKeyMiddleware(c *gin.Context) {
key := c.GetHeader("X-API-Key")
if key != "SECRET_KEY_123" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid api key"})
return
}
c.Next()
}
An attacker who captures this request (e.g., via a compromised network or insecure logging) can replay the same HTTP request against the same endpoint, and the API will treat it as legitimate because the API key is valid and no protections against duplication exist. This maps to common attack patterns such as credential replay and aligns with findings that fall under BFLA/Privilege Escalation and Unsafe Consumption checks in middleBrick’s 12 security checks. Without mechanisms like one-time nonces, timestamps, or request signing, the API key alone is insufficient to prevent replay.
Additionally, if the API key is transmitted in query parameters or stored insecurely on the client, it may be logged or exposed further, increasing the risk of replay and data exposure. MiddleBrick’s checks for Data Exposure and Encryption would flag such insecure transport or storage practices. The absence of rate limiting also enables attackers to brute-force or replay a small set of intercepted requests at scale, triggering findings in the Rate Limiting check.
Api Keys-Specific Remediation in Gin — concrete code fixes
To mitigate replay attacks when using API keys in Gin, introduce request-level freshness indicators and integrity checks. Common approaches include adding a timestamp and a nonce or message authentication code (MAC) to each request, and verifying these on the server. Below are concrete, working examples that you can adopt to harden Gin endpoints.
1. Timestamp and nonce approach
Require clients to include a timestamp (Unix epoch seconds) and a unique nonce in headers. The server checks that the timestamp is recent (e.g., within five minutes) and that the nonce has not been seen before. This prevents replay of older requests.
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
)
var seenNonces = make(map[string]bool)
func main() {
r := gin.Default()
r.Use(authMiddleware)
r.POST("/transfer", func(c *gin.Context) {
var req struct {
To string `json:"to"`
Amount int `json:"amount"`
}
if c.BindJSON(&req) != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "invalid body"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
r.Run()
}
func authMiddleware(c *gin.Context) {
apiKey := c.GetHeader("X-API-Key")
if apiKey != "SECRET_KEY_123" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid api key"})
return
}
tsStr := c.GetHeader("X-Timestamp")
nonce := c.GetHeader("X-Nonce")
if tsStr == "" || nonce == "" {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "missing timestamp or nonce"})
return
}
ts, err := strconv.ParseInt(tsStr, 10, 64)
if err != nil || time.Now().Unix()-ts > 300 {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "timestamp invalid or expired"})
return
}
if seenNonces[nonce] {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "replayed nonce detected"})
return
}
seenNonces[nonce] = true
c.Next()
}
2. Signature-based approach (recommended for production)
Have the client compute an HMAC over the request body and selected headers using a shared secret. The server recomputes the HMAC and rejects the request if it does not match. This binds the request content to the key and prevents tampering and replay.
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io/ioutil"
"net/http"
"sort"
"strings"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.Use(signatureMiddleware)
r.POST("/transfer", func(c *gin.Context) {
var req struct {
To string `json:"to"`
Amount int `json:"amount"`
}
if c.BindJSON(&req) != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "invalid body"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
r.Run()
}
func signatureMiddleware(c *gin.Context) {
apiKey := c.GetHeader("X-API-Key")
if apiKey != "SECRET_KEY_123" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid api key"})
return
}
expectedMAC := c.GetHeader("X-Signature")
if expectedMAC == "" {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "missing signature"})
return
}
body, _ := ioutil.ReadAll(c.Request.Body)
// Restore body for Gin binding
c.Request.Body = ioutil.NopCloser(strings.NewReader(string(body)))
// Deterministically include relevant headers
// Example: include X-Target and X-Idempotency-Key if used by your API
target := c.GetHeader("X-Target")
idempotencyKey := c.GetHeader("X-Idempotency-Key")
// Build a canonical string; order matters
var parts []string
parts = append(parts, string(body))
parts = append(parts, target)
parts = append(parts, idempotencyKey)
mac := hmac.New(sha256.New, []byte(apiKey))
for _, p := range parts {
mac.Write([]byte(p))
}
computedMAC := hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(computedMAC), []byte(expectedMAC)) {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "invalid signature"})
return
}
c.Next()
}
These examples illustrate concrete fixes that map to security checks in middleBrick’s scans, such as Authentication and Unsafe Consumption. By adding nonces or signatures, you ensure that even if an API key is intercepted, it cannot be reused maliciously. middleBrick’s findings will highlight missing freshness or integrity controls; applying these patterns will help resolve those findings and align your implementation with robust API security practices.