
In a significant security revelation, popular Flutter packages flutter_dotenv and dotenv have been found inadequate in protecting sensitive API keys and credentials. Despite their widespread adoption and promises of security, these packages expose critical information in built applications, putting countless Flutter projects at risk. This discovery raises serious concerns about the security practices in mobile and desktop app development using Flutter.
What is .env or Environment Variables?
Environment variables (commonly stored in .env files) are a method of storing configuration settings and sensitive information outside of your application's source code. These variables typically include:
- API keys and tokens
- Database credentials
- Authentication secrets
- Service endpoints
- Feature flags and environment-specific settings
In standard development practices, a .env file looks something like this:
API_KEY=your_secret_api_key_here
DATABASE_URL=mongodb://username:password@host:port/database
JWT_SECRET=super_secret_jwt_signing_key
ENVIRONMENT=production
The concept originates from server-side development, where environment variables are genuinely separated from application code and stored securely on the server's environment.
Why Use Environment Variables?
Developers turn to environment variables for several critical reasons:
1. Security
The primary reason for using .env files is to keep sensitive information like API keys and credentials out of your codebase. This prevents accidental exposure in version control systems like Git and protects valuable secrets from being publicly visible.
2. Environment-specific Configuration
Environment variables allow applications to behave differently based on where they're running:
- Development environments may use test APIs and databases
- Staging environments might connect to pre-production services
- Production environments require live service connections
3. Compliance Requirements
Many security standards and compliance frameworks (like PCI DSS for payment processing) explicitly require segregation of secrets from application code. Environment variables have become the standard way to meet these requirements.
4. Team Collaboration
Using .env files allows teams to work together without sharing sensitive credentials. Developers typically use a .env.example template file in version control, while each developer maintains their own private .env file with real credentials locally.
Common Use Cases for Environment Variables
Environment variables serve numerous practical purposes in modern application development:
Third-Party API Integration
When integrating services like payment processors, mapping APIs, or social media platforms, developers store authentication keys in environment variables:
STRIPE_SECRET_KEY=sk_live_51HG8...
GOOGLE_MAPS_API_KEY=AIzaSyB...
TWITTER_API_KEY=Jhk7Yt...
Database Configuration
Database connection strings often contain sensitive credentials:
DATABASE_URL=postgresql://username:password@hostname:5432/database_name
REDIS_URL=redis://username:password@hostname:6379
Feature Flags and App Configuration
Environment variables can control feature availability or app behavior:
ENABLE_PREMIUM_FEATURES=true
LOG_LEVEL=info
MAX_UPLOAD_SIZE=10485760
Authentication Secrets
JWT tokens, OAuth secrets, and other authentication mechanisms rely on secure keys:
JWT_SECRET=your_super_secret_jwt_signing_key
OAUTH_CLIENT_SECRET=client_secret_value
SESSION_SECRET=random_secure_string
How Environment Variables Should Work
In traditional server environments, environment variables work as intended - they exist outside the application code in the server's operating system environment. When an application runs, it can access these variables without them being embedded in the distributed code.
The process typically works as follows:
- Developer creates a .env file with secret values
- Application code references these variables (e.g.,
process.env.API_KEY
in Node.js) - The development environment loads variables during development
- In production, variables are set in the server environment, not in files
- The application accesses these environment-provided values at runtime
The Flutter Implementation Problem
Here's where the security issue arises with Flutter's dotenv implementations. When using flutter_dotenv
or dotenv
packages, developers include the .env file directly in their application assets. This requires adding the file to pubspec.yaml
:
assets:
- .env
The packages then promise to load these values securely in the application. However, this implementation fundamentally misunderstands the security model of environment variables. By bundling the .env file directly in the app's assets, these packages are essentially including the plaintext secrets in the distributed application.
The Security Vulnerability Exposed
The misguided implementation in these Flutter packages results in a significant security vulnerability. When apps are built and distributed:
Android APK Vulnerability
On Android, anyone can extract the .env file from your APK using basic tools:
- Download and install APK Editor or similar tools
- Open the APK file of your application
- Navigate to the assets directory
- Find and open the .env file in plaintext
Desktop Application Vulnerability
For desktop Flutter applications, the situation is equally concerning:
- Use standard archive extraction tools
- Extract the application package
- Navigate to the assets directory
- Access the .env file and all secrets in plaintext
This vulnerability was highlighted in this Medium article which demonstrated just how easily these supposedly "secure" credentials can be accessed.
Understanding the Implementation Details
To understand why these packages fail at their primary purpose, let's examine how they work:
How flutter_dotenv Works (Insecurely)
The flutter_dotenv
package loads the .env file from your app's assets and provides access to the values through its API:
import 'package:flutter_dotenv/flutter_dotenv.dart';
Future main() async {
// Load the .env file
await dotenv.load();
// Access values
String apiKey = dotenv.env['API_KEY'];
runApp(MyApp());
}
This implementation is problematic because:
- The .env file is physically included in the app bundle
- The file remains in plaintext inside the distributed application
- The package does nothing to encrypt or secure the values
- Anyone who extracts the app can read all your secrets
Code Examination: The False Security
Looking at the implementation of these packages reveals they're simply reading the file from assets:
// Simplified version of what happens in the package
Future<void> load() async {
// Simply loads the raw file from assets
final fileContent = await rootBundle.loadString('.env');
// Parses values into a map
_env = _parseFile(fileContent);
}
// No encryption, no protection, just parsing text
Real-world Security Implications
The consequences of this security oversight can be severe:
API Abuse
With exposed API keys, malicious actors can:
- Make unauthorized API calls on your account
- Quickly exceed your API usage limits or quotas
- Accumulate significant charges on pay-per-use services
- Perform actions that appear to come from your application
Data Breaches
If database credentials are exposed:
- Attackers can access your databases directly
- User data can be compromised
- Regulatory violations may occur (GDPR, HIPAA, etc.)
Service Hijacking
Authentication secrets could allow attackers to:
- Take over service accounts
- Modify configurations
- Deploy malicious code
Verifying the Vulnerability
You can verify this vulnerability in your own Flutter apps:
For Android Apps:
- Build an APK with flutter_dotenv or dotenv implemented
- Install an APK extraction tool like APK Editor Pro
- Extract the APK contents
- Navigate to assets folder
- Locate and open the .env file
You'll immediately see all your "protected" secrets in plaintext.
For Desktop Apps:
- Build your desktop application
- Use standard archive extraction tools
- Navigate to the application resources directory
- Find the assets folder
- Open the .env file with any text editor
The Security Principle Being Violated
This vulnerability stems from a fundamental misunderstanding of environment variables. True environment variables are meant to exist outside the application's distributable code - they're provided by the environment where the code runs.
Mobile and desktop apps differ fundamentally from servers:
- Server code runs in a controlled environment you own
- Mobile/desktop apps run in environments controlled by end users
- You cannot securely store plaintext secrets in code that runs on users' devices
What Developers Should Do Instead
Developers using Flutter should immediately transition away from these insecure packages and adopt more secure approaches:
Short-term Mitigation:
- Remove sensitive information from .env files
- Move API calls to secured backend services
- Implement token-based approaches with limited scopes and permissions
- Rotate any credentials that may have been exposed
Proper Long-term Solutions:
- Backend Proxies: Route sensitive API calls through your own secure backend
- Secure Storage APIs: Use platform-specific secure storage for truly sensitive information
- Build-time Constants: Use build configuration to inject different values per environment
- Code Obfuscation: While not perfect, this adds some protection
Securely Using envied Package
The envied
package takes a fundamentally different approach:
- It generates Dart code at build time based on your .env files
- The .env file itself is never included in the final build
- Values can be obfuscated in the generated code
This provides significantly better security than the plaintext approach of flutter_dotenv.
Industry Best Practices for Mobile API Security
Beyond choosing better packages, consider these fundamental approaches to mobile API security:
1. Server-side Authentication
Implement token-based authentication where your backend validates user identity before providing access to sensitive APIs.
2. API Key Proxying
Never call third-party APIs directly from mobile apps. Route calls through your backend to keep keys secure.
3. Limited-Scope API Keys
When client-side keys are unavoidable, use keys with minimal permissions and rate limits.
4. App Attestation
Use Google Play Services App Check or Apple App Attestation to verify your app hasn't been tampered with.
5. Certificate Pinning
Implement certificate pinning to prevent man-in-the-middle attacks on your API calls.
Conclusion
The security vulnerability in flutter_dotenv
and dotenv
packages represents a significant risk to Flutter applications and their users. Despite their popularity and convenience, these packages fundamentally fail at their primary purpose - keeping sensitive information secure.
Developers should immediately:
- Audit existing applications using these packages
- Remove or rotate any exposed credentials
- Migrate to more secure approaches like the
envied
package - Implement proper API security patterns with backend proxying
The Flutter community deserves better security practices, and awareness of this vulnerability is the first step toward creating more secure applications.
Frequently Asked Questions
Are there any cases where flutter_dotenv is safe to use?
Flutter_dotenv can be used safely for non-sensitive configuration values that don't need to be kept secret, such as feature flags, UI settings, or public URLs. However, it should never be used for API keys, credentials, or any sensitive information.
How can I check if my app is vulnerable?
If your Flutter app uses flutter_dotenv or dotenv packages and includes API keys or credentials in a .env file that's listed in your pubspec.yaml assets, your app is vulnerable. You can verify this by building your app and using APK extraction tools to inspect the assets directory.
What should I do if I've already published an app with exposed credentials?
Immediately rotate all exposed credentials, revoke and replace API keys, change any exposed passwords, and release an updated version of your app using secure credential management. Consider implementing rate limiting and monitoring for any exposed services to detect potential abuse.
Is the envied package completely secure?
While envied is significantly more secure than dotenv packages because it doesn't include plaintext files, it's not a perfect solution. The generated code can still be reverse engineered with sufficient effort. For truly sensitive operations, you should use a secure backend service to handle API calls and never include credentials in client-side code.
Can I use compilation constants as a secure alternative?
Dart's const values and compilation-time constants are better than runtime-loaded .env files, but they can still be extracted through reverse engineering. They offer some protection through obfuscation but shouldn't be considered fully secure for highly sensitive values. The envied package uses this approach with additional obfuscation features.
What's the most secure way to handle API authentication in Flutter?
The most secure approach is to implement a backend service that handles all sensitive API calls. Your Flutter app authenticates with your backend using secure authentication methods, and your backend (which you control) makes the actual API calls using securely stored credentials. This keeps sensitive keys entirely off client devices.
Important Links
- flutter_dotenv Package - The vulnerable package
- dotenv Package - Another vulnerable implementation
- envied Package - A more secure alternative
- Medium Article on the Security Flaw
- Flutter Code Obfuscation Documentation
Looking Ahead: Secure Your API Keys in Flutter
For a comprehensive guide on implementing secure API key management in Flutter using the recommended envied package, check out our follow-up article: How to Securely Implement API Keys in Flutter Using Envied This guide will walk you through the proper implementation of secure credential management in Flutter applications, protecting both your services and your users.