Unicode Normalization in Gin with Hmac Signatures
Unicode Normalization in Gin with Hmac Signatures — how this specific combination creates or exposes the vulnerability
Unicode normalization inconsistencies become a security risk in Gin when HMAC signatures are computed over string representations of path or query parameters that may appear identical to a human reader but differ in their binary representation. In Go, the standard library does not normalize Unicode by default, so two URLs that canonically represent the same resource—such as one using composed characters and another using decomposed characters—can produce different byte sequences. When such values are included in the data that is signed with an HMAC, the signature will not match across equivalent forms, which can lead to signature mismatch logic that is exploited for bypass or confusion.
Consider an API endpoint in Gin that uses HMAC to sign a combination of selected headers and a query parameter. If the application normalizes the request path or a selected header value on one side but not on the other, or if it compares user-controlled input to a stored canonical form without normalizing both sides, an attacker can supply a specially crafted Unicode string that passes the application’s own equivalence checks but fails the HMAC verification in a way that may be mishandled. Depending on how the application reacts to a bad signature—such as by leaking information or by applying different authorization logic based on signature success—an attacker may be able to bypass intended access controls or cause the application to treat an unsigned or different-signed request as valid.
In practice, this class of issue intersects with the broader BOLA/IDOR and Authentication checks that middleBrick examines. An API that does not enforce strict normalization before signing or verification may allow an authenticated-equivalent but differently encoded request to produce a different HMAC, and middleware may incorrectly interpret signature failures, leading to unintended authorization outcomes. The scanner’s checks for Input Validation and Authentication surface these risks by observing whether the application normalizes and canonicalizes data before cryptographic operations and whether it handles signature mismatches in a consistent, information-leak-free manner.
Real-world examples include query parameters that contain characters such as Latin small letter sharp s (ß) which can be represented as a single code point or as ss in some normalization forms, or composed vs. decomposed accented characters in usernames or identifiers. If the signing logic includes such parameters without normalization, two requests that an API author intends to treat as identical may carry different HMACs. middleBrick’s OpenAPI/Swagger analysis helps identify parameters that flow into authentication or integrity checks, and its runtime tests validate that normalization is applied consistently before signing or verification.
Hmac Signatures-Specific Remediation in Gin — concrete code fixes
To remediate Unicode normalization issues when using HMAC signatures in Gin, ensure that all data used to compute or verify the signature is normalized to a single, canonical form before the cryptographic operation. In Go, use the golang.org/x/text/unicode/norm package to apply NFC or NFD consistently across both request processing and signature verification. Do not rely on implicit behavior or on comparisons that mix normalized and non-normalized strings.
Below is a complete, realistic example of a Gin middleware that computes an HMAC over selected headers and a query parameter after normalization, and a verification handler that checks the signature in a constant-time manner to avoid leaking information via timing differences.
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"net/http"
"sort"
"strings"
"github.com/gin-gonic/gin"
"golang.org/x/text/unicode/norm"
)
// normalize returns the NFC form of the input string.
func normalize(s string) string {
return norm.String(norm.NFC, s)
}
// computeSignature creates an HMAC-SHA256 over the normalized concatenation of
// method, path, selected headers, and a specific query parameter.
func computeSignature(secret, method, path string, headers map[string]string, queryParam string) string {
nMethod := normalize(method)
nPath := normalize(path)
nQuery := normalize(queryParam)
// Normalize header keys and values before inclusion.
var ks []string
for k := range headers {
ks = append(ks, k)
}
sort.Strings(ks)
var b strings.Builder
b.WriteString(nMethod)
b.WriteString("|")
b.WriteString(nPath)
b.WriteString("|")
b.WriteString(nQuery)
for _, k := range ks {
b.WriteString("|")
b.WriteString(normalize(k))
b.WriteString(":")
b.WriteString(normalize(headers[k]))
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(b.String()))
return hex.EncodeToString(mac.Sum(nil))
}
// AuthMiddleware adds an X-Signature header for downstream verification.
func AuthMiddleware(secret string) gin.HandlerFunc {
return func(c *gin.Context) {
path := c.Request.URL.Path
method := c.Request.Method
// Example: include a specific query parameter that must be signed.
queryParam := c.Query("token")
headers := map[string]string{
"X-Custom-Origin": c.GetHeader("X-Custom-Origin"),
}
sig := computeSignature(secret, method, path, headers, queryParam)
c.Set("X-Signature", sig)
c.Next()
}
}
// VerifySignature ensures the provided signature matches the recomputed one.
func VerifySignature(secret string) gin.HandlerFunc {
return func(c *gin.Context) {
expected := c.MustGet("X-Signature").(string)
given := c.GetHeader("X-Signature")
if !hmac.Equal([]byte(expected), []byte(given)) {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid signature"})
return
}
c.Next()
}
}
func main() {
r := gin.Default()
secret := "my-secure-secret"
r.Use(AuthMiddleware(secret))
r.GET("/resource", VerifySignature(secret), func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
r.Run()
}
Key points in this approach:
- Normalize inputs with
norm.NFC(or your chosen form) before inclusion in the signed string; apply it to both keys and values of headers, the path, the query parameter, and the HTTP method. - Use a deterministic ordering for headers (e.g., sorted keys) so that the signed string is canonical regardless of map iteration order.
- Use
hmac.Equalfor comparison to avoid timing side channels when validating signatures. - Keep the secret and signing logic server-side; do not expose the secret to clients or embed it in JavaScript or mobile bundles.
These steps ensure that semantically equivalent requests with different Unicode representations produce identical HMACs, preventing signature mismatch confusion and reducing the attack surface around normalization-based bypasses.