Security

Secrets in Flutter: Where Developers Leak API Keys (and How to Fix It)

Flutter apps are especially prone to leaking secrets. Everything compiles into a client-side binary — there's no server boundary by default, which means any key you hard-code into your Dart code ships inside the APK or IPA that users download. A motivated attacker can decompile your app and read those keys directly. This is not a theoretical risk; it is how API keys get stolen from mobile apps every week.

This article covers the four most common leak points in Flutter apps, why "deleting it later" doesn't fix it, and how to set up secrets safely. You can also run a free automated scan that checks for hard-coded secrets and gitignore gaps in a few minutes.

Why Flutter apps leak secrets

When you write a Flutter app, your Dart source is compiled to native code and bundled into the app binary. Unlike a web app where the secret can live on a server the user never touches, a mobile app's binary is delivered to the user's device. Anything embedded in that binary — strings, config files, assets — can be extracted. Tools like apktool and strings make this trivial.

The implication: any secret that must remain secret cannot live in your client code at all. It needs to live behind a server, or be exchangeable for a short-lived token. The patterns below are about minimizing what's in the binary and keeping what shouldn't be committed out of version control.

The four most common leak points

1. API keys hard-coded in source

The most common leak: API keys pasted directly into lib/main.dart or a constants file as plain strings. If the repo is ever public, the key is exposed immediately. But even in a private repo, the key ships inside the compiled binary — and decompiling an APK with apktool reveals those strings in minutes. Move runtime secrets to environment config, injected at build time, and never commit real keys.

2. Committed .env files

A .env file created locally for development that never made it into .gitignore ships straight into git history on the first commit. This happens during rushed setup: you create .env, add your keys, run the app, and git add . without checking what's staged. The file is now in history. Deleting it later doesn't remove it — git history retains committed secrets even after removal.

3. Committed Firebase client config

google-services.json (Android) and GoogleService-Info.plist (iOS) are not secret by design — Firebase client config is meant to be embedded in the app. But an ungitignored config file combined with other exposed setup can add up to more access than you intended. These belong in .gitignore, injected at build time or via CI, not committed to the repo.

4. Signing keystores committed "temporarily"

Signing keystores (.jks, .keystore) committed "just to be safe" during a rushed release setup, then never removed. A signing keystore in git history is a serious leak — anyone with repo access can sign updates to your app as you. Keystores must be in .gitignore from the start and stored in a secure secrets manager or CI environment, never in version control.

Why deleting a secret later doesn't fix it

This is the part most developers miss. When you commit a secret and later remove it from the file, the secret still exists in git history — every commit that ever contained it still has it. git rm removes it from the working tree and the next commit, but historical commits retain it. Anyone who clones the repo can check out that old commit and read the key.

The only real fix once a secret has been committed is key rotation: revoke the leaked key and issue a new one. History-rewriting tools (git filter-repo, BFG) can remove the secret from history, but if the repo was ever pushed to a remote that others cloned, you cannot guarantee all copies are cleaned. Rotate the key. Treat any committed secret as compromised.

A safe secrets setup for Flutter

The goal is simple: no real secrets in source, no secret files in git history, and minimal secrets in the compiled binary.

  • Use environment config for runtime secrets. Inject keys at build time via --dart-define or a .env file loaded by flutter_dotenv. The values live in CI or a local non-committed file.
  • Baseline .gitignore before your first commit. Cover .env, *.jks, *.keystore, google-services.json, and GoogleService-Info.plist. A baseline Flutter .gitignore is the single most effective leak prevention.
  • Keep server-side secrets server-side. If a key grants access to billable or sensitive resources (AI APIs, payment processors, databases), it should not be in the client at all. Call through your own backend, which holds the key and proxies the request.
  • Scan before you submit, not after. A source scan catches hard-coded keys and gitignore gaps before they reach a store submission or a public repo.

Run a scan before you commit or submit

If you want to check your Flutter project for hard-coded secrets and gitignore gaps, run a free Launch Score scan — upload your project zip and get a report in a few minutes.

Scan for leaked secrets

Frequently asked questions

How do API keys leak in Flutter apps?

API keys leak in Flutter apps when they are hardcoded in Dart source files, stored in plaintext in SharedPreferences, embedded in environment files that get compiled into the app, or left in platform config files like GoogleService-Info.plist or google-services.json that are not gitignored.

Where should I store API keys in a Flutter app?

API keys should never be stored in client code. Store them on a backend server and have your Flutter app call the server through an authenticated API. For keys that must be in the app (like Firebase config), use code obfuscation and restrict key usage by package name in the provider console.

Will Apple reject my app for having API keys in the code?

Apple may reject your app if API keys are exposed in a way that compromises user security, but more importantly, exposed keys can be scraped and abused. Google Play scans for secrets during review. Use a free Launch Score scan to check before you submit.