AI-coded apps

    Did Windsurf put my AWS keys in the app bundle?

    Windsurf editor with an AWS configuration file open, highlighting hardcoded access keys ready to ship inside a mobile app bundle

    A few hours after Windsurf finishes scaffolding an S3 uploader, your AWS access key and secret can be sitting inside the JavaScript bundle of an Android build that is on its way to internal testing. The autocomplete put the keys where they needed to be to make the test pass. The bundler kept them there. Now they ship.

    Short answer

    Windsurf will happily write AKIA... access keys, secret keys, and full credential objects directly into source files, config helpers, and .env files when you ask it to "make S3 work" or "connect to AWS". Those values are not stripped at build time. They land in your APK, AAB, or IPA bundle as plain strings. The real fix is to remove long-term AWS credentials from the client entirely. Use a backend endpoint or Amazon Cognito identity pools with temporary AWS STS credentials.

    What you should know

    • Windsurf treats keys as configuration, not as secrets. When Cascade reads code that already contains a key, it suggests new code in the same style.
    • JavaScript bundles in React Native and Expo apps are easy to read. An attacker pulls index.android.bundle from any installed APK and greps for AKIA.
    • Google Play flags leaked AWS credentials. The Play Console notice reads "Leaked AWS Credentials: Your app(s) expose Amazon Web Services credentials."
    • OWASP tracks this as MASWE-0005. Hardcoded API keys in the app package are a documented mobile security weakness under MASVS-AUTH-1.
    • Long-term IAM user keys should never reach a phone. AWS recommends temporary credentials issued by STS or a Cognito identity pool for any client-side use.
    • Removing the key from a later release does not rotate it. The leaked key is valid until you rotate it in IAM, no matter how clean the next build looks.

    Why does Windsurf put AWS keys directly in the code?

    The short answer is that the AI assistant follows the path of least friction. When you tell Cascade to "wire up S3 uploads for profile pictures", it needs an access key and a secret key to make the SDK initialize. It either reads them from your local .env, asks you to paste them into chat, or writes a placeholder constant that you fill in. Once the keys exist in a file the bundler can see, they become part of your app.

    In practice, three patterns show up the most:

    • A config.ts or aws-config.js file that exports the access key and secret as constants.
    • A .env file read by react-native-config, Expo's extra field, or import.meta.env, and then inlined into the JavaScript bundle at build time.
    • An inline new AWS.S3({ accessKeyId: "AKIA...", secretAccessKey: "..." }) call inside a service module, because the chat asked for "a working snippet".

    The pattern that catches most builders by surprise is the second one. .env feels private because it lives on your machine and is .gitignored. Once the bundler runs, the values are no longer in .env. They are inside the compiled bundle, as plain strings.

    What happens when an AWS key ships in your app bundle?

    The honest answer is that the key is now public. Every user who downloads your build has it. Every researcher who pulls your APK off a third-party mirror has it. Every automated scanner that crawls public app distribution has it. The clock is short.

    The most cited incident pattern in mobile security writing is a dating app whose React Native bundle exposed AWS credentials. Coverage of the case describes an attacker pulling user photos, database backups, and personal information for tens of thousands of users from the S3 bucket those keys had access to. The financial cost reported runs into the millions. The technical mechanism is the part worth remembering: the keys were in index.android.bundle, and a single strings pass found them.

    This matters because the worst version of the leak gives the attacker every permission the IAM user attached to that key has. Uploading garbage to S3 is the lower bound, not the upper bound. If the key is scoped to a development IAM user with AdministratorAccess, the damage scales accordingly.

    How do attackers find the keys inside a compiled APK or IPA?

    The technique is older than most app stores. For an Android APK, an attacker unzips the file, locates assets/index.android.bundle for a React Native build (or the equivalent Hermes bytecode, convertible back to JavaScript with tools like hermes-dec), and runs a grep for AKIA, ASIA, aws_secret, or the constant name used by the code. For an iOS IPA, the same logic applies to the bundled JavaScript inside the .app directory, plus any plist files.

    The HackTricks community guide on React Native applications documents this whole flow with the exact commands. A separate Payatu writeup on reverse engineering React Native goes further and walks through how a Hermes bundle is identified and decompiled. None of this requires specialist tooling.

    The point worth internalizing is that minification does not solve the problem. A string constant inside JavaScript is still a string constant after minification, and a strings pass on a native binary still finds embedded base64-shaped secrets.

    What does Google Play do when it detects leaked AWS credentials?

    Google Play scans uploaded builds and flags leaked AWS credentials with a Play Console policy notice. Developers in a long-running react-native-config issue thread report receiving the exact message: "Leaked AWS Credentials: Your app(s) expose Amazon Web Services credentials." The notice links to Google's security guidance and gives a window to fix the issue. Repeated violations can lead to suspension.

    Apple does not publish an equivalent automated message in App Store Connect, but the App Review Guidelines section on data security applies. If a reviewer finds the key, or a third party reports the leak through Apple's security channel, the build can be pulled. The OWASP MAS controls that map to this are MASVS-AUTH-1 (no authentication material embedded in the app), MASVS-CRYPTO (no hardcoded cryptographic keys), and MASWE-0005 specifically on API keys in the package.

    The limit worth keeping in mind: neither store guarantees detection. Plenty of apps with hardcoded keys ship without a Console notice ever appearing. The store is not your secret scanner.

    What should I tell Windsurf to do instead of hardcoding keys?

    The prompt that works is specific. Instead of "wire up S3 uploads", ask Cascade to generate a setup that follows AWS best practice:

    PatternWhat to ask Windsurf to writeWhere the credential lives
    Backend proxyA server endpoint (POST /upload-url) that returns a presigned S3 URLOn the server, never on the client
    Cognito identity poolA client that authenticates to Cognito and receives temporary STS credentials with scoped IAM permissionsReturned to the client, expires in minutes
    Federated identityA client that exchanges a sign-in token (Google, Apple, custom) for AWS temporary credentials via CognitoSame as above
    Local development onlyA .env.local that is read at dev time and never committed or bundledOn your development machine

    The AWS IAM best practices documentation recommends temporary credentials over long-term access keys for any client-side use case. Cognito identity pools are the canonical way to do this for mobile, but for many apps a backend with a presigned URL endpoint is simpler and faster to ship.

    When you write the prompt to Windsurf, name the pattern: "Use AWS Cognito identity pools to get temporary credentials. Do not embed access keys in the client." Cascade follows the style of nearby code, so the second prompt that matters is "remove every reference to AKIA access keys from this file, including imports and helper functions."

    How can I check my build for leaked secrets before submitting?

    A leak scan on the compiled artifact is the only reliable answer, because the build is what attackers download. The check has three parts:

    1. Decode the bundle. For React Native, unzip the APK, locate assets/index.android.bundle, and either read it directly or run it through hermes-dec if the bundle is in Hermes bytecode. For iOS, extract the IPA, open the .app folder, and inspect the JavaScript bundle and plist files.
    2. Search for known patterns. AWS access keys start with AKIA (long-term) or ASIA (temporary). Secrets are 40 characters of base64-like text. Also search for aws_access_key_id, aws_secret_access_key, and any constant names from your code.
    3. Verify the finding is dead. If a key is found, rotate it in IAM immediately. Do not wait until the next release. The leaked key stays valid until the rotation completes.

    A scanner that runs the same passes automatically saves time when builders ship multiple updates per week. For builders who want an external automated read of an APK, AAB, or IPA before submission, PTKD.com (https://ptkd.com) is one of the platforms focused on pre-submission scanning aligned with OWASP MASVS for AI-coded and no-code apps, including detection of hardcoded credentials in the bundle.

    What to watch out for

    The most common mistake is fixing the symptom and skipping the cause. A developer sees the key, deletes it from the source file, pushes a new release, and considers the issue closed. The key is still valid in IAM. Anyone who downloaded the previous version still has it. Rotate first, then ship the cleaned build.

    A related mistake is treating obfuscation or encryption inside the bundle as a fix. The decryption code has to run on the client, so the decryption key also has to be in the bundle. A reverse engineer with an afternoon will find both. The only real answer is to remove the secret from the client entirely.

    The myth to actively reject: "my app is a small project, no one will reverse it." Automated scanners crawl public app stores looking for cloud credentials precisely because small projects are the easiest targets. A leaked AWS key is currency on a credential market, not a vanity issue.

    Key takeaways

    • Windsurf will write AWS access keys directly into your code unless you tell it not to. Treat the AI as a fast intern who needs an explicit policy: temporary credentials only, no embedded long-term keys.
    • The bundle is public. Anything inside an APK, AAB, or IPA should be treated as known to attackers from the moment the build is published.
    • Use Cognito identity pools or a backend proxy with presigned URLs. Long-term IAM user keys do not belong on a phone, no matter how convenient the snippet looks.
    • If a key has shipped, rotate it in AWS IAM the same hour. Removing it from source does not invalidate the leaked copy.
    • Some teams outsource the pre-submission check, and PTKD.com (https://ptkd.com) is one of the platforms that scan compiled APK, AAB, and IPA files for hardcoded credentials and other MASVS-aligned issues before the build reaches the store.
    • #windsurf
    • #aws
    • #secrets
    • #react-native
    • #owasp-masvs
    • #vibe-coding

    Frequently asked questions

    If I delete the key from my code and ship a new build, am I safe?
    No. Deleting the key from the source file does not invalidate the leaked copy. Anyone who downloaded the earlier APK or IPA still has it, and automated scanners that crawl public app stores already cached it. Rotate the key in AWS IAM the same hour you discover the leak, then ship the cleaned build. The new release matters, but the rotation is what stops further damage.
    Are React Native and Expo apps especially vulnerable to this?
    They are easy targets because the JavaScript bundle is shipped as text inside the package. An attacker unzips the APK, locates index.android.bundle or its Hermes equivalent, and runs a grep for AKIA. Native iOS and Android apps written in Swift or Kotlin compile down to machine code, which is harder to read but still readable. A string constant in any binary is still a string constant.
    Does AWS Cognito remove the need for a backend?
    Often yes, for direct S3 uploads, DynamoDB reads with scoped IAM policies, or authenticated calls to Lambda. Cognito identity pools issue temporary credentials from AWS STS that expire in minutes and can be scoped to the signed-in user. For anything that touches payment providers or third-party APIs that need a server-side secret, a small backend remains the safer pattern.
    Will Google Play always catch a leaked AWS key before I publish?
    No. Google Play scans submitted builds and sometimes posts a Console policy notice titled Leaked AWS Credentials, but developers report apps shipping with hardcoded keys without ever receiving the notice. Treat the store scanner as a backstop, not as your check. Run your own scan on the compiled APK, AAB, or IPA before each submission, especially after Windsurf or Cursor touch an AWS configuration file.
    Can I just encrypt the keys inside the app bundle?
    Encrypting a secret inside a client that also holds the decryption key only adds an inconvenience. A reverse engineer with an afternoon and the right tools recovers both. The OWASP MAS project lists hardcoded cryptographic keys as a separate weakness (MASWE-0013) for exactly this reason. Move the secret off the client, or accept that anyone who downloads the build can read it.

    Keep reading

    Scan your app in minutes

    Upload an APK, AAB, or IPA. PTKD returns an OWASP-aligned report with copy-paste fixes.

    Try PTKD free