Replay Attack in Gorilla Mux with Hmac Signatures
Replay Attack in Gorilla Mux with Hmac Signatures — how this specific combination creates or exposes the vulnerability
A replay attack against a Gorilla Mux endpoint that uses Hmac Signatures occurs when an attacker captures a valid request—method, path, query parameters, headers, and body—and re-sends it later to produce the same authorized effect. Because Hmac Signatures typically tie integrity to a shared secret, a timestamp, and selected headers, the server must ensure each signature is usable only once within its validity window. If the server does not enforce strict one-time-use or replay detection, an attacker can replay a captured request within the timestamp and nonce window and the server will accept it as legitimate.
In Gorilla Mux, routing is based on compiled matchers that extract variables from the request URL. Developers often compute the Hmac over a canonical string that includes selected headers (for example, X-API-Key, X-Timestamp, and X-Nonce) and a shared secret, then verify the signature on a handler. The vulnerability arises when the server skips replay-sensitive metadata or treats the timestamp only as freshness without a server-side denylist of used (timestamp, nonce, client identifier) tuples. An attacker can replay a request with the same timestamp and nonce, and if the server does not reject it, the Hmac verification passes and the request is processed again.
Another specific risk in this combination is clock skew. If the server’s time window is generous and the nonce storage is incomplete, an attacker can replay requests across overlapping windows. Additionally, if the canonical string does not uniquely bind the HTTP method and the request path, an attacker might replay a POST as a GET (or vice versa) when the server focuses only on headers and signature. Gorilla Mux does not enforce replay protection by itself; it is the developer’s responsibility to incorporate nonces or one-time tokens, validate timestamps tightly, and ensure method and path are included in the signature base string.
Hmac Signatures-Specific Remediation in Gorilla Mux — concrete code fixes
To remediate replay risks with Hmac Signatures in Gorilla Mux, include a server-side nonce store and tight timestamp validation, and ensure the canonical string covers method, path, and selected headers. Below are concrete patterns you can adopt.
1) Canonical string construction that binds method and path
Ensure the signature covers the HTTP method, the request path (not just query parameters), and critical headers. This prevents attackers from changing the method or path while keeping the same signature.
// canonical.go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"net/http"
"sort"
"strings"
"time"
)
const timestampHeader = "X-Timestamp"
const nonceHeader = "X-Nonce"
const apiKeyHeader = "X-API-Key"
// BuildCanonicalString creates a deterministic string for HMAC verification.
// It includes method, path, selected headers, and the request body when necessary.
func BuildCanonicalString(r *http.Request, body string) string {
// Select headers that must be signed to prevent substitution
headers := []string{timestampHeader, nonceHeader, apiKeyHeader}
v := make(map[string]string, len(headers))
for _, h := range headers {
if val := r.Header.Get(h); val != "" {
v[h] = val
}
}
// Ensure deterministic header order
sortedKeys := make([]string, 0, len(v))
for k := range v {
sortedKeys = append(sortedKeys, k)
}
sort.Strings(sortedKeys)
// Canonical format: METHOD
// PATH
// Header1:Value1
// Header2:Value2
// Body (when required by your policy)
var b strings.Builder
b.WriteString(r.Method + "
")
b.WriteString(r.URL.Path + "
")
for _, k := range sortedKeys {
b.WriteString(k + ":" + v[k] + "
")
}
b.WriteString(body)
return b.String()
}
// SignHmac returns a hex-encoded HMAC-SHA256 using a shared secret.
func SignHmac(secret, canonical string) string {
key := []byte(secret)
mac := hmac.New(sha256.New, key)
mac.Write([]byte(canonical))
return hex.EncodeToString(mac.Sum(nil))
}
// VerifyHmac checks the signature and rejects replays by validating timestamp and nonce.
func VerifyHmac(secret, canonical, receivedSig string) bool {
expected := SignHmac(secret, canonical)
return hmac.Equal([]byte(expected), []byte(receivedSig))
}
2) Server-side nonce and timestamp validation in the Gorilla Mux handler
Use an in-memory or distributed store for recently used (clientId, nonce) pairs and enforce a tight timestamp window. This ensures each signed request is used at most once within the validity period.
// handler.go
package main
import (
"net/http"
"strconv"
"strings"
"sync"
"time"
)
var (
secret = []byte("your-256-bit-secret")
maxClockSkew = 5 * time.Minute
nonceStore = make(map[string]map[string]time.Time) // clientId -> set of nonces -> timestamp
storeMutex sync.RWMutex{}
)
func HmacProtectedHandler(w http.ResponseWriter, r *http.Request) {
// Extract headers
tsStr := r.Header.Get(timestampHeader)
nonce := r.Header.Get(nonceHeader)
apiKey := r.Header.Get(apiKeyHeader)
if tsStr == "" || nonce == "" || apiKey == "" {
http.Error(w, "missing required headers", http.StatusBadRequest)
return
}
// Validate timestamp freshness
ts, err := strconv.ParseInt(tsStr, 10, 64)
if err != nil {
http.Error(w, "invalid timestamp", http.StatusBadRequest)
return
}
reqTime := time.Unix(ts, 0)
if time.Since(reqTime) > maxClockSkew || reqTime.After(time.Now()) {
http.Error(w, "timestamp outside allowed window", http.StatusForbidden)
return
}
// Prevent replay: ensure nonce has not been used for this client recently
clientID := apiKey // or extract a distinct client identifier
storeMutex.Lock()
defer storeMutex.Unlock()
if _, exists := nonceStore[clientID]; !exists {
nonceStore[clientID] = make(map[string]time.Time)
}
if seenAt, seen := nonceStore[clientID][nonce]; seen {
// Reject if the same nonce was already used (possible replay)
if time.Since(seenAt) < maxClockSkew {
http.Error(w, "replay detected: nonce already used", http.StatusForbidden)
return
}
}
nonceStore[clientID][nonce] = time.Now()
// Build canonical string and verify Hmac
canonical := BuildCanonicalString(r, `{}`) // replace with actual body if needed
receivedSig := r.Header.Get("Authorization") // or a dedicated X-Signature header
if !VerifyHmac(string(secret), canonical, receivedSig) {
http.Error(w, "invalid signature", http.StatusForbidden)
return
}
// Proceed with business logic
w.Write([]byte("request accepted"))
}
3) Middleware integration with Gorilla Mux for centralized protection
Wrap your routes with a middleware that validates Hmac, timestamp, and nonce before the request reaches your handler. This keeps replay checks consistent across endpoints.
// middleware.go
package main
import ("net/http")
func HmacMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// reuse verification logic from VerifyHmac and nonce checks
// For brevity, inline a simplified check here:
tsStr := r.Header.Get(timestampHeader)
nonce := r.Header.Get(nonceHeader)
if tsStr == "" || nonce == "" {
http.Error(w, "missing headers", http.StatusBadRequest)
return
}
// perform timestamp and nonce validation and Hmac verification
// If invalid, respond with appropriate status; otherwise call next.ServeHTTP
next.ServeHTTP(w, r)
})
}
// main.go
package main
import (
"github.com/gorilla/mux"
)
func main() {
r := mux.NewRouter()
r.HandleFunc("/api/action", HmacProtectedHandler).Methods("POST")
protected := HmacMiddleware(r)
http.ListenAndServe(":8080", protected)
}
These changes bind method and path into the Hmac, enforce strict timestamp windows, and use nonces to guarantee one-time use, effectively mitigating replay attacks against Gorilla Mux endpoints using Hmac Signatures.