flawopen.com/Incidents/capital-one-ssrf-imds-july-2019
Capital One SSRF: AWS IMDSv1 Credential Theft
The DOJ complaint alleged a misconfigured WAF let SSRF reach AWS IMDSv1 and expose temporary role credentials and S3 data affecting about 106M. Require tokens and scope roles.[10], [2]
The DOJ criminal complaint alleged a chain like this: Imagine a receptionist who fetches documents from addresses you provide. A request relay lets someone point that receptionist toward an internal office. There, the receptionist can obtain a temporary access badge for the workload; the badge opens only the rooms allowed by that workload's role. The mistake was trusting that only staff could reach the office, even though the receptionist could be directed there.[10], [8]
Core Concepts & Subsystem Terms
SSRF- Server-side request forgery (
CWE-918): making a server send a request of the attacker's choosing, so the request carries the server's network position and trust.[9] Web application firewall- A filter in front of a web application. The criminal complaint alleged Capital One's misconfigured WAF could relay requests to internal addresses.[10]
IMDSv1- When IMDSv1 is enabled, metadata requests do not require a token. The IAM security-credentials path for a role returns temporary credentials associated with that role.[7], [8]
IMDSv2- IMDSv2 uses a session token obtained with a PUT request. Set
HttpTokens=requiredto reject tokenless IMDSv1 requests; this blocks many SSRF relays that can issue only a plain GET, but it is not a complete SSRF defense.[7], [6] IAM role credentials- Temporary AWS keys issued to an instance. Whatever the role may do, anyone holding the keys may also do until they expire.[8]
Framework mappings
- CWE reference for the weakness described in this guide: CWE-918
- D3-OTF Outbound Traffic Filtering — This guide's outbound network restriction limits which destinations the service or host can reach.
Incident Timeline
An external researcher reports the exposed data through Capital One's responsible disclosure programme.[2]
Capital One announces the incident and the FBI arrests a suspect in Seattle.[1]
Step-by-Step Attack Flow
The firewall relays a crafted request
The criminal complaint alleges that a misconfigured web application firewall could be made to send requests to attacker-chosen addresses, including internal ones.[10]
Data leaves in bulk
Credit card application data from 2005 to early 2019 was copied: about 100 million people in the US and 6 million in Canada, including about 140,000 US Social Security numbers and 80,000 linked bank account numbers.[2]
Source Code: Flaw vs. Secure Implementation
Illustrative Flask code, not Capital One's configuration. The hostname allowlist is the primary application control. The required egress proxy must enforce destination policy against the resolved address at connection time, deny private and link-local ranges including instance metadata, and apply the same checks to every request and retry; verify that behavior in the proxy configuration. Redirects are not followed. A separate resolve-then-connect check in the application would leave a DNS-rebinding race.[9], [5], [6]
import requests
from flask import Flask, request
app = Flask(__name__)
@app.get("/fetch")
def fetch():
# The server fetches whatever URL the caller supplies.
url = request.args["target"]
resp = requests.get(url, timeout=5)
return resp.text
from urllib.parse import urlsplit
import requests
from flask import Flask, abort, request
app = Flask(__name__)
ALLOWED_HOSTS = {"images.example-cdn.com", "api.partner.example"}
EGRESS_PROXY = "http://egress-proxy.internal:3128"
# Configure this proxy to enforce destination policy after DNS resolution.
@app.get("/fetch")
def fetch():
parts = urlsplit(request.args.get("target", ""))
if parts.scheme != "https" or parts.hostname not in ALLOWED_HOSTS:
abort(400, "destination not allowed")
if parts.port not in (None, 443) or parts.username or parts.password:
abort(400, "destination not allowed")
resp = requests.get(parts.geturl(), timeout=(2, 5), allow_redirects=False, proxies={"https": EGRESS_PROXY}, trust_env=False)
if resp.is_redirect:
abort(502, "upstream redirect refused")
return resp.text
Engineering & System Hardening Checklist
- ✓Route URL fetches through an egress proxy and enforce the host allowlist and IP-range policy at the connection boundary: connect only to the address that was checked, and apply the policy to retries too. Disable redirects or revalidate every hop; blocking only the literal 169.254.169.254 is insufficient.[9], [4]
- ✓Set
HttpTokens=requiredon every applicable instance and verify existing instances, since account defaults do not automatically update each instance. ChooseHttpPutResponseHopLimitfor the workload's network path; AWS warns that a value of 1 can cause container issues. The token requirement rejects IMDSv1 requests; hop limit is a separate network-path setting, not the control that blocks a tokenless GET.[6], [7] - ✓The criminal complaint alleges the SSRF path exposed the instance role. Scope that role to the workload's required actions and resources so a credential exposure cannot grant unrelated S3 access.[10], [8]
- ✓FlawOpen detection guidance: alert when temporary role credentials are used outside the instance network; investigate with workload and cloud audit context.
- ✓In code review, treat any path where a user influences a URL the server fetches like one where a user influences a SQL query, and require an allowlist test for it.[9], [5]
Sources
- U.S. Department of Justice — Seattle tech worker arrested for data theft involving large financial services company (29 July 2019)
- Capital One — Information on the 2019 cybersecurity incident
- OCC — civil money penalty against Capital One over the 2019 incident (August 2020)
- Why is SSRF so dangerous in cloud environments?
- Can fetch() cause SSRF?
- AWS — Configure the Instance Metadata Service options
- AWS — Use the Instance Metadata Service to access instance metadata
- AWS — Retrieve security credentials from instance metadata
- OWASP — Server Side Request Forgery Prevention Cheat Sheet
- DOJ — United States v. Paige Thompson, criminal complaint (29 July 2019)