HIGH ssrfflaskhmac signatures

Ssrf in Flask with Hmac Signatures

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

Server-Side Request Forgery (SSRF) in Flask applications that use HMAC signatures requires both an endpoint that makes outbound requests and a signature scheme that does not adequately protect the target. When a Flask route accepts user input for a URL and includes that input in an HMAC-signed request to another service, the signature may only cover parts of the request (e.g., selected headers or a subset of parameters) while the actual target URL remains under attacker control. This mismatch allows an attacker to supply a malicious URL that the server follows, while the HMAC check passes because the attacker cannot forge the signature over the attacker-controlled portion if the signing scope is too narrow.

Consider a Flask route that fetches user profile data from a backend API. The route builds a request to a fixed internal service and signs only the method and selected headers, leaving the target URL outside the signed scope. An attacker can provide a URL pointing to the metadata service (e.g., http://169.254.169.254) or an internal Kubernetes service. Because the server trusts its own signature and does not validate the destination, the outbound request proceeds to the attacker-chosen endpoint, exposing internal services and potentially sensitive metadata.

HMAC signatures themselves do not prevent SSRF; they ensure integrity and authenticity of a subset of request data. If the signature does not bind the full request target and critical parameters, an attacker can manipulate the unsigned parts to point to internal or external malicious endpoints. Common patterns that lead to SSRF in this context include: accepting a raw URL or host/port from the client, using string concatenation to build the request target, and failing to validate the destination against an allowlist. Even when the HMAC covers some headers, SSRF can occur if the destination is not also included in the signed scope or if the server follows redirects to uncontrolled locations.

In practice, SSRF with HMAC-signed requests can lead to internal port scanning, interaction with cloud metadata services, or abuse of internal APIs that assume traffic originates from the server. The vulnerability aligns with OWASP API Top 10 categories related to broken object level authorization and improper restrictions on URL or network targets. A scanner that performs unauthenticated black-box testing, such as middleBrick, can detect these issues by observing whether the endpoint allows target URLs that resolve to internal infrastructure and by analyzing whether the signature scope and validation logic sufficiently restrict the destination.

Hmac Signatures-Specific Remediation in Flask — concrete code fixes

To remediate SSRF when using HMAC signatures in Flask, ensure that the target URL and all critical request attributes are included in the signed payload, and enforce strict allowlists for destinations. Never allow an attacker to control the request target without validation, even if the request is HMAC-signed.

Example: Unsafe pattern

import hashlib
import hmac
import requests
from flask import Flask, request, jsonify
import urllib.parse

app = Flask(__name__)
SECRET = b'super-secret-key'

def make_signature(method, url, headers):
    payload = f'{method}:{url}:{headers.get("X-API-Key", "")}'
    return hmac.new(SECRET, payload.encode(), hashlib.sha256).hexdigest()

@app.route('/fetch')
def fetch():
    target = request.args.get('url')  # attacker-controlled
    method = 'GET'
    headers = {'X-API-Key': 'public-key'}
    sig = make_signature(method, target, headers)
    # No validation of target; SSRF possible
    resp = requests.request(method, target, headers=headers)
    return jsonify({'status': 'ok', 'signature': sig})

Example: Safe remediation with signature scope and allowlist

import hashlib
import hmac
import requests
from flask import Flask, request, jsonify

app = Flask(__name__)
SECRET = b'super-secret-key'
ALLOWED_HOSTS = {'api.internal.example.com', 'data.service.example.com'}

def sign_request(method, url, key_id, timestamp):
    # Include method, normalized URL, key id, and timestamp in the signature
    parsed = urllib.parse.urlparse(url)
    normalized = f'{parsed.scheme}://{parsed.netloc}{parsed.path}'
    if parsed.query:
        # sort query parameters for deterministic signature
        qs = '&'.join(sorted(parsed.query.split('&')))
        normalized += '?' + qs
    payload = f'{method}:{normalized}:{key_id}:{timestamp}'
    return hmac.new(SECRET, payload.encode(), hashlib.sha256).hexdigest()

def is_allowed_host(url):
    parsed = urllib.parse.urlparse(url)
    return parsed.netloc in ALLOWED_HOSTS

@app.route('/fetch')
def fetch():
    target = request.args.get('url')
    if not target:
        return jsonify({'error': 'missing url'}), 400
    if not is_allowed_host(target):
        return jsonify({'error': 'disallowed host'}), 403
    method = 'GET'
    key_id = 'public-key'
    ts = '1698765432'
    sig = sign_request(method, target, key_id, ts)
    headers = {'X-API-Key': key_id, 'X-Request-Timestamp': ts, 'X-Signature': sig}
    resp = requests.request(method, target, headers=headers, timeout=5)
    return jsonify({'status': 'ok', 'signature': sig})

Key practices:

  • Include the full normalized request target (scheme, host, path, and sorted query parameters) inside the HMAC payload so the signature binds the destination.
  • Validate the target against an explicit allowlist of hosts or patterns before making the outbound request.
  • Avoid using raw user input to construct the request URL; prefer selecting from predefined endpoints or using path templates with strict parameter validation.
  • Set timeouts and disable redirects or validate redirect targets to prevent open redirects or SSRF via intermediate responses.
  • Treat HMAC as integrity protection for the signed scope only; do not rely on it to restrict the request target if the target is not part of the signed data.

These steps ensure that even with HMAC signatures, an attacker cannot force the server to interact with arbitrary internal or external endpoints. Tools like middleBrick can verify that the endpoint rejects disallowed hosts and that the signature covers the intended request attributes.

Related CWEs: ssrf

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

Frequently Asked Questions

Does HMAC signing prevent SSRF in Flask APIs?
No. HMAC signatures ensure integrity of signed data but do not prevent SSRF unless the target URL and all critical request attributes are included in the signed scope and validated against an allowlist.
What is a robust mitigation for SSRF when using HMAC signatures in Flask?
Include the full normalized request target in the HMAC payload, validate the destination against a strict allowlist of hosts, enforce timeouts, disable uncontrolled redirects, and avoid placing user-controlled input directly into the request URL without validation.