Bola Idor in Gin with Hmac Signatures
Bola Idor in Gin with Hmac Signatures — how this specific combination creates or exposes the vulnerability
Broken Object Level Authorization (BOLA) in the Gin web framework occurs when an API uses HMAC signatures for request authentication but does not enforce proper resource ownership checks after the signature is validated. HMAC signatures can verify that a request originates from a trusted client and has not been altered, yet they do not by themselves guarantee that the authenticated subject is allowed to access the specific resource identified in the URL or payload.
Consider a Gin endpoint /api/users/:userID/profile. The client computes an HMAC over selected headers and the request path, sends the signature in a custom header (e.g., X-Signature), and the server verifies it before processing the request. If verification succeeds but the server then loads the profile using userID taken directly from the URL without confirming that the authenticated principal owns that userID, a BOLA vulnerability exists. An attacker who knows or guesses another user’s numeric ID can retrieve or modify profiles because ownership is never validated against the identity derived from the HMAC or session context.
This becomes especially risky when HMAC-based schemes use static or predictable keys, include insufficient scope in the signed payload, or omit the target resource identifier inside the signed data. For example, if the HMAC covers only the HTTP method, path, and timestamp—but not the :userID—an attacker can reuse a valid signature across different user IDs. The server will still verify the signature successfully (since the signed components are unchanged) and proceed to operate on the attacker-supplied userID, leading to unauthorized read, update, or deletion of other users’ data.
In Gin, BOLA with HMAC signatures often surfaces in combination with role-based access controls that are applied inconsistently. A developer might check that the caller is authenticated and the signature is valid, then assume that middleware has already enforced ownership. Without an explicit check that the resource’s owner matches the authenticated subject—derived either from claims inside a signed token, a session map, or a lookup tied to the HMAC context—an attacker can traverse IDs horizontally across other users’ records.
Real-world attack patterns mirror findings in the OWASP API Top 10 and common CVEs affecting APIs that rely on identifiers without ownership validation. Even with HMAC integrity, endpoints that expose object IDs directly in URLs remain susceptible to IDOR when the authorization layer skips a per-request ownership assertion. The risk is compounded when responses contain sensitive PII or when mutations allow state changes on other users’ resources, as the API may report success while silently acting on the wrong dataset.
Hmac Signatures-Specific Remediation in Gin — concrete code fixes
To mitigate BOLA in Gin when HMAC signatures are used, ensure that the resource identifier is either included in the signed payload or validated against the authenticated subject before any data access. The signature should cover enough context—such as the user identity or tenant—to prevent signature reuse across different objects. Below are concrete, idiomatic examples showing a secure approach.
Example 1: Include user identity in the signed payload
Embed the user ID (or a user-specific claim) inside the data that is signed. This binds the signature to a specific principal, making it difficult to reuse across IDs.
// server.go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
)
const secretKey = "your-256-bit-secret"
func computeHMAC(payload string) string {
key := []byte(secretKey)
mac := hmac.New(sha256.New, key)
mac.Write([]byte(payload))
return hex.EncodeToString(mac.Sum(nil))
}
func verifyHMAC(c *gin.Context) (bool, int) {
userID := c.Param("userID")
timestamp := c.Query("ts")
receivedSig := c.GetHeader("X-Signature")
if receivedSig == "" || timestamp == "" {
return false, http.StatusBadRequest
}
// Ensure the timestamp is recent to prevent replay (e.g., within 2 minutes)
// omitted for brevity
payload := "GET|/users/" + userID + "|" + timestamp
expected := computeHMAC(payload)
if !hmac.Equal([]byte(expected), []byte(receivedSig)) {
return false, http.StatusUnauthorized
}
return true, http.StatusOK
}
func GetUserProfile(c *gin.Context) {
ok, status := verifyHMAC(c)
if !ok {
c.AbortWithStatusJSON(status, gin.H{"error": "invalid signature"})
return
}
// Authenticated subject derived from request context or token claims
subjectID := c.MustGet("subjectID").(int) // assume set by prior auth middleware
requestedID := c.Param("userID")
uid, err := strconv.Atoi(requestedID)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "invalid userID"})
return
}
// BOLA check: ensure subject can access this resource
if subjectID != uid {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "access denied"})
return
}
// Proceed to fetch profile for uid
c.JSON(http.StatusOK, gin.H{"userID": uid, "profile": "..."})
}
Example 2: Use HMAC to sign a scoped token that includes the resource ID
Issue short-lived tokens that include the target resource ID and validate ownership server-side. The HMAC protects integrity, while the token carries the authorized scope.
// token.go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"net/http"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
)
type TokenPayload struct {
UserID int `json:"uid"`
TargetID int `json:"tid"`
Expiry int64 `json:"exp"`
Signature string `json:"-"`
}
func generateScopedToken(userID, targetID int, secret string) (string, error) {
payload := TokenPayload{
UserID: userID,
TargetID: targetID,
Expiry: time.Now().Add(5 * time.Minute).Unix(),
}
body, _ := json.Marshal(payload)
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
payload.Signature = base64.URLEncoding.EncodeToString(mac.Sum(nil))
token, _ := json.Marshal(payload)
return base64.URLEncoding.EncodeToString(token), nil
}
func VerifyScopedToken(tokenStr, secret string) (*TokenPayload, bool) {
raw, err := base64.URLEncoding.DecodeString(tokenStr)
if err != nil {
return nil, false
}
var payload TokenPayload
if err := json.Unmarshal(raw, &payload); err != nil {
return nil, false
}
if time.Now().Unix() > payload.Expiry {
return nil, false
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(raw) // re-sign the raw bytes excluding the signature field
expected := base64.URLEncoding.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(expected), []byte(payload.Signature)) {
return nil, false
}
return &payload, true
}
func GetScopedProfile(c *gin.Context) {
token := c.Query("token")
subjectID := c.MustGet("subjectID").(int)
payload, ok := VerifyScopedToken(token, secretKey)
if !ok || payload.UserID != subjectID {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "invalid or insufficient scope"})
return
}
// Ensure the resource being accessed matches the token’s target
requestedID, err := strconv.Atoi(c.Param("userID"))
if err != nil || payload.TargetID != requestedID {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "mismatched resource"})
return
}
c.JSON(http.StatusOK, gin.H{"userID": requestedID, "profile": "..."})
}
General hardening practices
- Always include the resource identifier or its owner within the HMAC input to prevent signature reuse across objects.
- Derive the authenticated principal from a trusted source (e.g., JWT subject, session store) and compare it explicitly with the resource owner before data access.
- Use short expirations and replay protection (e.g., nonces or timestamp windows) for HMAC-based tokens or requests.
- Apply consistent authorization checks in centralized middleware or route handlers to avoid gaps where BOLA can occur.
Related CWEs: bolaAuthorization
| CWE ID | Name | Severity |
|---|---|---|
| CWE-250 | Execution with Unnecessary Privileges | HIGH |
| CWE-639 | Insecure Direct Object Reference | CRITICAL |
| CWE-732 | Incorrect Permission Assignment | HIGH |