API keys exposed in a developer platform
Background
A seed-stage developer tools startup was preparing to open their public beta. Their product let developers connect external services via a visual workflow builder. Under the hood, it stored API keys and OAuth tokens on behalf of users to execute automations.
Before flipping the switch on self-serve signups, they wanted us to review the key storage and delivery pipeline.
The finding
API keys were generated server-side and stored in S3. When a user needed their key — to configure a CLI or local environment — the app generated a pre-signed URL pointing to the key file and redirected the user to it.
The problem was the S3 key naming scheme: keys/{user_id}/{service_name}.json. The
user_id was the same integer ID that appeared in the app's URL structure and API
responses — not a secret. The pre-signed URLs expired after 24 hours, but the underlying
file was permanently accessible to anyone who constructed the path.
An attacker with a valid account could enumerate key paths for any other user and retrieve their credentials for every connected service. Given that many users connected high-value services (Stripe, Sendgrid, GitHub), the blast radius was significant.
CVSS score: 9.3 (Critical) — unauthenticated read access to credentials after initial URL construction.
How we fixed it
Immediate remediation:
- Regenerate all existing key files at randomized, non-enumerable UUID-based paths
- Enforce session validation server-side before issuing any pre-signed URL
- Set S3 bucket policy to deny direct public access entirely
Longer-term:
- Encrypt key files at rest using envelope encryption, with per-user KMS keys
- Add audit logging for every key access event
- Move to short-lived credential injection at execution time rather than user-retrievable static keys
We reviewed all three changes and confirmed the exploit path was fully closed before the public beta launched.
Outcome
The vulnerability was caught and fixed before any real users accessed the beta. The founding team rewrote their credentials architecture based on our recommendations. The final design stores no long-lived secrets in S3 at all — credentials are injected at execution time using ephemeral tokens, eliminating the underlying risk class.
All client details are anonymized. Sector and finding type are accurate.
Have a similar problem?
Tell us about your scope. We'll respond within two business days.