JSTGTECH
← Back to blog

The Capital One breach: an SSRF bug into 100M records

4 min read

In March 2019, an attacker used a server-side request forgery (SSRF) bug in a misconfigured web application firewall to reach a single internal URL — http://169.254.169.254/ — running behind a Capital One-hosted application in AWS. That one request was enough to pull temporary IAM credentials for a role with broad S3 read access, and from there exfiltrate more than 100 million credit applications and 140,000 Social Security numbers (DOJ indictment). No malware, no phishing, no zero-day in AWS itself — just a web app that could be tricked into fetching an internal URL on the attacker’s behalf, and an internal URL that handed over live credentials to anyone who asked. Capital One paid an $80 million OCC fine and settled a $190 million class action over it (Reuters). Seven years later, IMDSv1 — the version that made this possible — is still the default reachable endpoint on plenty of EC2 instances, because “enabled” is not the same as “enforced.”

Root cause

Every EC2 instance can reach a link-local address, 169.254.169.254, which serves the Instance Metadata Service (IMDS) — instance ID, AMI info, user-data, and critically, temporary security credentials for whatever IAM role is attached to the instance. It’s how an EC2 instance gets AWS credentials without anyone hardcoding a key. In the original version of that service, IMDSv1, the endpoint answers any plain HTTP GET request with no authentication step at all — if a request reaches that IP from the instance, it gets an answer. That design was fine as long as nothing running on the box could be tricked into making arbitrary outbound requests on an attacker’s behalf. But web applications routinely do exactly that: fetch a URL from a query parameter, proxy a request, render a remote image, validate a webhook. The former engineer charged in the case had reportedly discovered a misconfigured ModSecurity WAF rule in front of a Capital One web application that could be coerced into requesting an arbitrary URL and returning the response — a textbook SSRF (indictment). Point that SSRF at 169.254.169.254/latest/meta-data/iam/security-credentials/<role-name> and IMDSv1 hands back an access key, secret key, and session token for whatever role the instance was running as — no extra proof that the request came from a legitimate, non-hijacked process.

Blast radius

The stolen role’s permissions, not the SSRF bug itself, set the ceiling on the damage. In this case the role had read access to S3 buckets used for a range of Capital One’s credit-card and loan-application data, compiled from six years of applications going back to 2005 — 106 million individuals across the US and Canada, including Social Security numbers, bank account numbers, and credit scores (Capital One’s disclosure). Stolen IMDS credentials are functionally indistinguishable from legitimate ones to everything downstream — CloudTrail logs the calls as coming from the role, IAM enforces exactly the policy attached to that role, and nothing about the request signature reveals it originated from an SSRF payload rather than the application itself. That’s the general shape of every IMDS-credential-theft incident: the SSRF or RCE is the entry point, but the actual damage is bounded entirely by how over-permissioned the instance role was. A role scoped to one bucket prefix limits an attacker to that prefix; a role with s3:* across the account hands over the account’s data.

Remediation

AWS’s direct answer, shipped in November 2019 a few months after this breach became public, is IMDSv2 — it requires a session to be established with a PUT request that returns a token, and every subsequent metadata request must carry that token in a header (AWS). Critically, IMDSv2’s PUT requires HTTP with no redirects followed and sets a configurable hop limit (default 1) on the response’s TTL — which means a proxy or SSRF payload relaying a simple GET typically can’t complete the handshake or forward the token past one network hop, while a process running natively on the instance can. That single change would have broken the Capital One attack path as described. Enforcement is the part teams skip: IMDSv2 has been opt-in at the instance level since launch, so an unpatched fleet stays exploitable indefinitely unless someone acts. Three concrete steps: set HttpTokens: required (not just optional) on every instance and in every launch template, ideally via an AWS Config rule (ec2-imds-v2-check) that flags drift; use an **SCP or Config rule to deny new instance launches without IMDSv2 enforced**, since a one-time fleet fix doesn’t stop the next Terraform apply from reintroducing IMDSv1; and independently, scope every instance role to least privilege — the hop-limit defense is layer two, not a replacement for making sure a stolen credential is only ever worth as much as the narrowest policy you could get away with attaching.

The bigger lesson

This wasn’t an AWS vulnerability — IMDS behaved exactly as designed, and the WAF misconfiguration was Capital One’s, not Amazon’s. But it’s the canonical case study for why cloud metadata endpoints deserve the same scrutiny as any credential store: anything that can make an outbound HTTP request from inside your VPC is a potential path to 169.254.169.254, and IMDSv1’s design assumed that path would never exist. IMDSv2’s token-and-hop-limit model closes the SSRF-relay case specifically, but it only helps the instances where someone actually flipped HttpTokens to required — which is why the Config rule and the SCP matter as much as the setting itself. If your fleet still allows HttpTokens: optional, the difference between “we have IMDSv2 available” and “we’re actually protected” is one unenforced launch template away from mattering.

Related posts