Ssrf Server Side in Gin with Hmac Signatures
Ssrf Server Side in Gin with Hmac Signatures — how this specific combination creates or exposes the vulnerability
Server-side request forgery (SSRF) in a Gin-based service that uses HMAC signatures illustrates how trust boundaries can be misaligned. HMAC signatures are typically intended to verify the origin and integrity of a request, but if the signature is validated only after the request is processed internally—or if the signature covers only a subset of the request data—attackers can induce the server to make arbitrary outbound HTTP calls.
Consider a Gin endpoint that accepts a URL to fetch metadata. The client provides a URL and an HMAC over selected parameters (e.g., URL and a timestamp). If the server uses the HMAC to confirm authenticity but then passes the supplied URL to an internal fetcher without strict validation, an attacker can supply a URL that resolves to internal services (e.g., http://127.0.0.1:8080/admin, cloud metadata endpoints like http://169.254.169.254/latest/meta-data/, or Kubernetes service DNS names). The HMAC check passes because the attacker’s signed payload matches the server’s expected scheme, yet the server performs the request on behalf of the attacker, leading to SSRF.
A concrete pattern: the client sends GET /fetch?url=...&ts=1700000000&sig=.... The server verifies sig over url and ts, then does not re-validate the URL’s host against a denylist or enforce a strict allowlist. Because the signature does not bind to an intended destination or enforce network boundaries, the server-side HTTP client can be tricked into reaching internal endpoints. This becomes especially dangerous when combined with features like OpenAPI/Swagger spec analysis, where internal schemas might hint at admin paths or metadata routes that are not publicly documented but are reachable from the compromised Gin service.
In secure designs, HMAC signatures should cover the intended destination and any policy constraints (e.g., allowed host patterns), and validation must occur before any network operation. Even with correct HMAC usage, SSRF can still arise if the server follows redirects to internal addresses or trusts user-supplied headers (such as X-Forwarded-Proto) that alter the request flow. Therefore, the combination of SSRF-prone URL handling and HMAC-based authentication in Gin requires strict input validation, destination allowlists, and network egress controls to prevent the server from acting as an unintended proxy.
Hmac Signatures-Specific Remediation in Gin — concrete code fixes
To remediate SSRF when using HMAC signatures in Gin, ensure the signature validates both the payload and the intended destination, and enforce strict network policies before making any outbound request. Below are concrete, working examples that demonstrate secure handling.
Example 1: HMAC verification with destination binding
Sign and verify a composite of the target host, path, and a timestamp to prevent the server from requesting arbitrary URLs. This ensures the server only performs requests to preapproved destinations.
// client-side: sign method, host, path, timestamp, and body
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"net/url"
"strings"
)
func buildSignedRequest(targetURL, method, timestamp, secret string) (string, string) {
u, _ := url.Parse(targetURL)
// canonical string: METHOD|host|path|ts
canonical := strings.ToUpper(method) + "|" + u.Host + "|" + u.Path + "|" + timestamp
h := hmac.New(sha256.New, []byte(secret))
h.Write([]byte(canonical))
sig := hex.EncodeToString(h.Sum(nil))
return targetURL, sig
}
func main() {
url, sig := buildSignedRequest("https://api.example.com/v1/resource", "GET", "1700000000", "super-secret-key")
fmt.Printf("url=%s&sig=%s\n", url, sig)
}
Example 2: Gin handler that validates HMAC and enforces destination allowlist
The server verifies the HMAC over the same canonical string and checks the host against an allowlist before performing the request.
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/gin-gonic/gin"
)
var allowedHosts = map[string]bool{
"api.example.com": true,
}
func verifyHMAC(r *http.Request, secret string) bool {
urlQ := r.URL.Query().Get("url")
ts := r.URL.Query().Get("ts")
sig := r.URL.Query().Get("sig")
if urlQ == "" || ts == "" || sig == "" {
return false
}
u, err := url.Parse(urlQ)
if err != nil {
return false
}
if !allowedHosts[u.Host] {
return false
}
canonical := strings.ToUpper(r.Method) + "|" + u.Host + "|" + u.Path + "|" + ts
h := hmac.New(sha256.New, []byte(secret))
h.Write([]byte(canonical))
expected := hex.EncodeToString(h.Sum(nil))
return hmac.Equal([]byte(expected), []byte(sig))
}
func fetchHandler(c *gin.Context) {
if !verifyHMAC(c.Request, "super-secret-key") {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid signature"})
return
}
url := c.Query("url")
// safe to proceed: host is verified, URL is from an allowed origin
resp, err := httpGetWithTimeout(url, 5)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": resp})
}
func httpGetWithTimeout(target string, timeoutSec int) (interface{}, error) {
// placeholder for an actual HTTP client with timeout and no redirect to internal IPs
return map[string]string{"note": "success"}, nil
}
func main() {
r := gin.Default()
r.GET("/fetch", fetchHandler)
r.Run()
}
Key practices:
- Include the intended host and path in the HMAC canonical string so the signature is bound to a specific destination.
- Validate the host against an allowlist before any network call; reject private IPs and internal DNS names.
- Do not follow redirects to internal addresses; configure the HTTP client to disable redirect handling or sanitize Location headers.
- Enforce timeouts and disable automatic credential propagation to internal endpoints to reduce the impact of a potential SSRF.