HIGH ssrfecho gohmac signatures

Ssrf in Echo Go with Hmac Signatures

Ssrf in Echo Go with Hmac Signatures — how this specific combination creates or exposes the vulnerability

Server-Side Request Forgery (SSRF) in an Echo Go service that uses HMAC signatures can occur when the application validates a signature but does not sufficiently restrict the target URL. An attacker can supply a valid HMAC for a malicious URL, causing the server to make arbitrary outbound requests on its behalf. Because the HMAC is trusted, the server forwards the request without additional checks, enabling internal network scanning, metadata service access (e.g., cloud instance metadata), or SSRF-to-RCE paths when combined with other weaknesses.

In Echo, a typical pattern is to have an endpoint that accepts a URL and an HMAC to prove the client is allowed to request that resource. If the server only verifies the HMAC against a shared secret and then uses http.Get or http.NewRequest to fetch the provided URL, the control flow reaches internal backends that are not directly exposed. Attackers can point the signed request to http://169.254.169.254 (AWS metadata), internal Kubernetes services, or localhost administrative interfaces. The HMAC verification gives the request a veneer of authenticity, bypassing network-level restrictions that would otherwise prevent SSRF.

Compounding the risk, Echo middleware that modifies the request context or URL without revalidating the target can inadvertently expose internal endpoints. For example, if the server rewrites the path or adds query parameters after signature verification, the original intent of the HMAC may no longer align with the final request. This mismatch is common when developers focus on tamper-proofing query strings or headers but fail to treat the target URL as an input that must be constrained by policy. Real-world patterns such as OAuth callbacks or webhook integrations often use signed URLs; if the signing scheme does not bind the HMAC to the full target host and port, SSRF becomes feasible.

Consider an endpoint that fetches user-provided resources after verifying an HMAC. An attacker can supply a signed URL pointing to an internal service that returns sensitive data or accepts injected headers. Even when the response is not returned directly to the user, the server may log or process it in unsafe ways, creating data exposure or injection paths. The presence of HMAC signatures does not eliminate SSRF; it changes the trust boundary. Without explicit allowlists for hosts and ports, and without disabling redirects for externally controlled URLs, the combination of Echo Go routing and Hmac Signatures can amplify SSRF into a critical channel for internal reconnaissance.

Hmac Signatures-Specific Remediation in Echo Go — concrete code fixes

To remediate SSRF in Echo Go when HMAC signatures are used, enforce strict URL allowlisting, avoid forwarding to user-supplied hosts, and bind the HMAC to the full request context including host and port. Below are concrete, working examples that demonstrate secure patterns.

1. Validate host and port before using the signed URL

Parse and check the target URL against an allowlist before making the request. Do not rely on the HMAC alone to enforce reachability.

// secure_fetch.go
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"net/http"
	"net/url"
	"strings"

	"github.com/labstack/echo/v4"
)

var allowedHosts = map[string]bool{
	"api.example.com":      true,
	"internal.service.local": true,
}

func verifyHMAC(secret, message, receivedMAC string) bool {
	h := hmac.New(sha256.New, []byte(secret))
	h.Write([]byte(message))
	expected := hex.EncodeToString(h.Sum(nil))
	return hmac.Equal([]byte(expected), []byte(receivedMAC))
}

func fetchResource(c echo.Context) error {
	rawURL := c.FormValue("url")
	mac := c.FormValue("hmac")
	secret := "my-32-byte-secret-used-for-hmac-validation"

	// Reconstruct the signed message exactly as the client did.
	// Include scheme, host, port (if non-standard), and path.
	parsed, err := url.Parse(rawURL)
	if err != nil || parsed.Scheme != "https" {
		return echo.NewHTTPError(http.StatusBadRequest, "invalid url")
	}

	// Enforce allowlist by host (and optionally port)
	if !allowedHosts[parsed.Hostname()] {
		return echo.NewHTTPError(http.StatusForbidden, "host not allowed")
	}
	// Optionally restrict to specific ports
	if parsed.Port() != "" && parsed.Port() != "443" {
		return echo.NewHTTPError(http.StatusForbidden, "port not allowed")
	}

	// Ensure the HMAC covers the full URL used for the request
	if !verifyHMAC(secret, rawURL, mac) {
		return echo.NewHTTPError(http.StatusUnauthorized, "invalid signature")
	}

	// Disable redirects to prevent open redirects or SSRF chains
	client := &http.Client{
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			return http.ErrUseLastResponse
		},
	}

	req, err := http.NewRequest(http.MethodGet, parsed.String(), nil)
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "request error")
	}
	// Optionally set timeouts and headers
	resp, err := client.Do(req)
	if err != nil {
		return echo.NewHTTPError(http.StatusBadGateway, "upstream error")
	}
	defer resp.Body.Close()

	// Process resp.Body safely; do not forward raw upstream content blindly
	return c.JSON(http.StatusOK, map[string]string{
		"status": resp.Status,
	})
}

func main() {
	e := echo.New()
	e.GET("/fetch", fetchResource)
	e.Logger.Fatal(e.Start(":8080"))
}

2. Include host and port in the HMAC scope to prevent host swapping

Ensure the signed message includes the host and port so that a signature for http://allowed.com/resource cannot be reused for http://evil.com/resource. This binds the token to the intended endpoint.

// hmac_scope.go
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"net/http"
	"net/url"

	"github.com/labstack/echo/v4"
)

func signMessage(secret, method, rawURL string) string {
	h := hmac.New(sha256.New, []byte(secret))
	// Include method and full URL to prevent method swapping and host confusion
	h.Write([]byte(method + ":" + rawURL))
	return hex.EncodeToString(h.Sum(nil))
}

func verifySignedURL(c echo.Context, secret string) (string, error) {
	rawURL := c.FormValue("url")
	mac := c.FormValue("hmac")
	parsed, err := url.Parse(rawURL)
	if err != nil || parsed.Host == "" {
		return "", fmt.Errorf("invalid url")
	}
	expected := signMessage(secret, "GET", rawURL)
	if !hmac.Equal([]byte(expected), []byte(mac)) {
		return "", fmt.Errorf("invalid signature")
	}
	return parsed.String(), nil
}

func proxiedHandler(c echo.Context) error {
	const secret = "my-32-byte-secret-used-for-hmac-validation"
	url, err := verifySignedURL(c, secret)
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "invalid request")
	}

	// Additional policy checks can be applied here, e.g., host denylists,
	// or strict path prefix validation.
	if strings.Contains(url, "169.254.169.254") {
		return echo.NewHTTPError(http.StatusForbidden, "internal hosts forbidden")
	}

	client := &http.Client{
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			return http.ErrUseLastResponse
		},
	}
	req, _ := http.NewRequest(c.Request().Method, url, nil)
	resp, err := client.Do(req)
	if err != nil {
		return echo.NewHTTPError(http.StatusBadGateway, "upstream error")
	}
	defer resp.Body.Close()

	return c.JSON(http.StatusOK, map[string]string{
		"target": url,
	})
}

func main() {
	e := echo.New()
	e.POST("/proxied", proxiedHandler)
	e.Logger.Fatal(e.Start(":8080"))
}

3. Disable redirects and enforce timeouts to limit SSRF impact

Even with allowlisting, SSRF can occur via open redirects or chained endpoints. Disable client-side redirects and set reasonable timeouts to reduce the window for abuse.

// timeout_no_redirects.go
package main

import (
	"context"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"net/http"
	"time"

	"github.com/labstack/echo/v4"
)

func secureClient() *http.Client {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	// Ensure the cancel function is managed appropriately in long-running services
	return &http.Client{
		Timeout: ctx.Deadline.Sub(time.Now()),
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			return http.ErrUseLastResponse
		},
	}
}

func verifyHMAC(secret, message, receivedMAC string) bool {
	h := hmac.New(sha256.New, []byte(secret))
	h.Write([]byte(message))
	expected := hex.EncodeToString(h.Sum(nil))
	return hmac.Equal([]byte(expected), []byte(receivedMAC))
}

func safeFetch(c echo.Context) error {
	rawURL := c.FormValue("url")
	mac := c.FormValue("hmac")
	if !verifyHMAC("super-secret", rawURL, mac) {
		return echo.NewHTTPError(http.StatusUnauthorized)
	}

	parsed, err := url.Parse(rawURL)
	if err != nil || parsed.Host == "" {
		return echo.NewHTTPError(http.StatusBadRequest)
	}

	// Explicitly block internal IPs and cloud metadata
	if parsed.Host == "169.254.169.254" || strings.HasPrefix(parsed.Host, "127.0.0.") {
		return echo.NewHTTPError(http.StatusForbidden, "internal host blocked")
	}

	client := secureClient()
	req, _ := http.NewRequest("GET", parsed.String(), nil)
	resp, err := client.Do(req)
	if err != nil {
		return echo.NewHTTPError(http.StatusBadGateway)
	}
	defer resp.Body.Close()

	return c.JSON(http.StatusOK, map[string]string{"ok": "true"})
}

func main() {
	e := echo.New()
	e.POST("/safe", safeFetch)
	e.Logger.Fatal(e.Start(":8080"))
}

Related CWEs: ssrf

CWE IDNameSeverity
CWE-918Server-Side Request Forgery (SSRF) CRITICAL
CWE-441Unintended Proxy or Intermediary (Confused Deputy) HIGH

Frequently Asked Questions

Why does HMAC verification not prevent SSRF in Echo Go?
HMACs ensure integrity and authenticity of a request but do not restrict what the server is allowed to do. If the server uses the signed URL as an opaque input and forwards it without host/port allowlisting, an attacker can sign a malicious internal URL and trigger SSRF. The signature validates that the request was issued by a party possessing the secret, not that the target is safe.
What must be included in the HMAC scope to prevent host-swapping SSRF in Echo Go?
The HMAC must cover the full URL including scheme, host, and port (e.g., GET:https://api.example.com/resource). This prevents an attacker from swapping hosts while keeping a valid signature. If the scope omits the host, a signed request for https://allowed.com/path could be reused against https://evil.com/path.