Why is there an AWS key in your build, and who can use it?
You added file uploads one afternoon. The S3 SDK wanted credentials, you had an access key sitting in your AWS console, you pasted it in, the upload worked, you moved on. That was the last time you thought about it.
So is that key in the JavaScript you ship to the browser? And if it is, what does it open? Those are the two questions, and you can answer the first one in about thirty seconds.
An AWS access key looks harmless. Twenty characters, starts with AKIA, paired with a longer secret. It doesn't scream "credential" the way a password does. But to AWS it's a login, and an AWS account is rarely scoped to just one thing. That key is one S3 call away from your whole bucket, and depending on how you set up the IAM user behind it, a lot more than the bucket.
How it ends up in the browser
The usual path is the AWS JavaScript SDK. You want to upload a user's avatar straight to S3, so you reach for @aws-sdk/client-s3 in your frontend, hand it an accessKeyId and secretAccessKey, and call PutObject. It works on your machine, it works in production, and now both halves of a real AWS credential are minified into your client bundle and served to every visitor.
The framework will help you do this without noticing. Mark the key with a NEXT_PUBLIC_ prefix in Next.js or a VITE_ prefix in Vite and the build inlines it into the bundle on purpose. No warning, no review step. The string is just there, sitting next to your component code.
The other common path is a build artifact you forgot about. A .env file copied into the deploy, a config object logged for debugging, an old SDK init left in after you moved the real logic to the server. The key outlives the reason you pasted it.
What a stranger does with it
Finding it is trivial. Open your site, press F12, search the loaded JavaScript for AKIA. Automated scanners do this at scale all day, scraping public bundles for the AKIA pattern, then testing each hit against AWS to see what it can reach. You don't need a person to notice. You need a bot to finish its loop.
Once they have a valid key pair, the first move is reconnaissance. aws sts get-caller-identity tells them whose account it is. Then they probe what the attached IAM policy allows. If you scoped the key tightly to one bucket, the damage stays in that bucket: they list every object, download the lot, overwrite or delete files, run up your bill with traffic. If you did what a lot of people do under time pressure and attached AmazonS3FullAccess or worse, the blast radius is every bucket in the account.
The expensive surprise is usually compute. A key with broad permissions gets used to launch EC2 instances in regions you've never deployed to, mining cryptocurrency on your card until the bill or an AWS abuse notice wakes you up. By then the key has been public for as long as that bundle has been live.
There's no alarm for this. AWS sees API calls signed with valid credentials and treats them as you, the same way a leaked Stripe secret key gets treated as your server. Valid signature, valid request, no flag.
Check it from outside, then fix it
You can look right now, no tools required.
- Open your site in Chrome or Firefox.
- Open developer tools (F12).
- In the Sources tab, search across all loaded files (Ctrl+Shift+F, or Cmd+Option+F on a Mac).
- Search for
AKIA.
A match means the key is public. Treat it as compromised, because you can't know who already pulled it from a bundle that's been cached across CDNs and browsers for weeks.
Two steps to fix, in order.
Deactivate the key in IAM first. In the AWS console, disable or delete the exposed access key before you touch the code. Rotating after you redeploy leaves the old key valid for as long as the old bundle is cached anywhere, which is longer than you think. Then check CloudTrail for calls you didn't make.
Then move the upload off the client. The browser should never hold AWS credentials. Have your server generate a pre-signed URL and hand that to the browser. The browser uploads to the URL; the credentials stay on the server and expire in minutes.
// components/Upload.tsx (shipped to the browser)
const s3 = new S3Client({
region: "us-east-1",
credentials: {
accessKeyId: "AKIA4PED2K7N••••EXMP",
secretAccessKey: "wJalr••••••••••••EXAMPLEKEY",
},
});
await s3.send(new PutObjectCommand({ Bucket, Key, Body }));We scan for AKIA keys from the outside, the same way an attacker's crawler does, and tell you if one is sitting in your bundle. That's the part you can see without touching your backend. We can't read your IAM policy or test what the key actually reaches, so once we flag a leak, deactivate it and check CloudTrail. The scanner finds the door left open. Closing it, and seeing who already walked through, is on you.
Find it before someone else does.
Paste your domain. The grade and issue count are free, and you'll see in a couple of minutes exactly what's reachable from outside.