A SaaS platform allows users to import product data: POST /api/products/import { "source_url": "https://supplier.com/catalog.json" }. The server fetches the URL, parses the JSON, and imports the products.
The server is an EC2 instance with an IAM role: app-role with permissions including s3:GetObject on arn:aws:s3:::* (overly broad — grants access to all company buckets). No URL validation on the source_url field.
POST /api/products/import { "source_url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/" }. The server fetches the metadata endpoint, receives the role name, returns an import error that includes the response body (“Failed to parse JSON: app-role”). Attacker now knows the role name. Second request with the full credentials URL. Import error returns: AccessKeyId, SecretAccessKey, Token. Attacker has temporary credentials for app-role — and access to all company S3 buckets.The attacker doesn’t need the import to succeed — they need the response body to be returned in an error message. Many APIs helpfully include the server’s response in error messages: “Failed to parse response: [response body here].” This turns SSRF into data exfiltration even when the feature doesn’t work as intended.