OWASP Top 10 For Flutter - M1: Mastering Credential Security in Flutter

Over the years, I have been developing applications, and mobile app security is often underestimated. Since I am a passionate Flutter and Security developer, I thought it might be helpful to share my experience and research the OWASP top 10 de-facto standards to mitigate security issues common in mobile applications. That's why I decided to write 10 articles to cover the top 10 OWASP security vulnerabilities.

Cover

Majid Hajian - Azure & AI advocate@Microsoft, Dart & Flutter community leader, Organizer@FlutterVikings, http://flutterengineering.io author

In this comprehensive guide, we explore M1—Improper Credential Usage—from the OWASP Mobile Top 10, focusing on Flutter application development, and see how you can address different scenarios and protect your apps.

OWASP Top 10 For Flutter - M1 - Mastering Credential Security in Flutter

What is M1: Improper Credential Usage?

Improper Credential Usage refers to improperly handling, storing, and transmitting authentication credentials, API keys, tokens, or sensitive information that can be exploited if exposed. This vulnerability often occurs when sensitive credentials are stored insecurely (for example, in plain text within the code or unencrypted local storage) or transmitted over unprotected channels.

When credentials are mismanaged, attackers can reverse-engineer mobile applications to extract these secrets, gaining unauthorized access to backend services and user data. This vulnerability is not just about weak passwords; it encompasses the entire lifecycle of credential management—from creation and storage to usage and eventual rotation.

According to the OWASP Mobile Top 10 documentation , improper credential usage is a critical risk that can lead to significant breaches, impacting user privacy and the integrity of mobile applications.

How Does Improper Credential Usage Happen?

There are multiple cases where issues may occur; let's get into each of them and learn to see if they look familiar.

Insecure Storage Practices

One common way this vulnerability manifests is through insecure storage practices. In the rush to build features, many developers might hardcode API keys or authentication tokens directly into their source code. Consider this simplified example:

class AppConfig {
  // Insecure practice: Storing the API key directly in the source code.
  static const String apiKey = 'YOUR_INSECURE_API_KEY';
}

The API key is embedded within the app’s code in this example. When the app is compiled, reverse-engineering techniques can be applied to extract this key. Tools exist that can decompile APKs or analyze binary code, making this a significant risk.

Stay tuned for my reverse engineering article, which is being published soon, to learn more about how you can find these keys.

Caching Sensitive Data in Memory

Another potential pitfall is caching sensitive information in memory during runtime without proper safeguards. Flutter apps may cache user tokens or other sensitive data for performance reasons. However, if this data is not managed correctly, it could be exposed through debugging tools or if the app’s memory is dumped during a crash.

For example, holding a user token in a global variable might seem convenient but can pose a risk if an attacker finds a way to inspect the app’s runtime memory.

Accidental Exposure Through Debug Logs

Even if you are careful where you store your credentials, logging them during development for debugging purposes can be dangerous. If logs are not properly sanitized or shipped to a remote logging service, sensitive data can leak through logs.

void debugLogin(String token) {
  // Dangerous: Logging the token can expose it if logs are intercepted
  print('User token: $token');
}

This doesn't need to be on the device itself. Sometimes, these keys are mistakenly reported to error reporting systems such as Sentry or Firebase Crashlytics or even stored on the device without encryption.

Local File Storage without Encryption

Sometimes, developers write sensitive data to local files on the device for persistence. While robust, Flutter’s file-handling capabilities do not inherently encrypt the data written to these files. Without additional encryption measures, data stored in files can be read by unauthorized apps or through direct access to the device’s file system.

import 'dart:io';
import 'package:path_provider/path_provider.dart';

Future<void> writeTokenToFile(String token) async {
  final directory = await getApplicationDocumentsDirectory();
  final file = File('${directory.path}/token.txt');
  // Insecure: Writing token to a file in plain text
  await file.writeAsString(token);
}

Even if the credentials are not part of your source code, saving them in a file without encryption exposes them if the device is compromised.

Unencrypted Storage

Many developers turn to the shared_preferences package for its simplicity when saving small pieces of data. However, this package stores data in plain text on the device, meaning that if an attacker gains physical access or exploits vulnerabilities in the device’s operating system, they could easily retrieve these values.

import 'package:shared_preferences/shared_preferences.dart';

Future<void> storeToken(String token) async {
  SharedPreferences prefs = await SharedPreferences.getInstance();
  await prefs.setString('authToken', token); // Insecure if token is sensitive
}

Even if you’re not embedding the credentials directly in your source code, storing them in Shared Preferences creates a risk. The storage's plain-text nature means there is no built-in encryption, making it a vulnerable point in your application.

Insecure Transmission

Even if credentials are stored securely, transmitting them over insecure channels poses another risk. The credentials could be intercepted via man-in-the-middle (MITM) attacks if data is sent over HTTP instead of HTTPS. For example, consider this network request using an insecure endpoint:

import 'package:http/http.dart' as http;

Future<void> login(String username, String password) async {
  final response = await http.post(
    Uri.parse('http://example.com/api/login'),
    body: {
      'username': username,
      'password': password,
    },
  );
  // The credentials are transmitted in plain text over HTTP
}

In this snippet, the absence of HTTPS means that sensitive credentials are exposed during transit, making it easier for attackers to capture them.

Code Exposure in Public Repositories

Another vector for improper credential usage is the accidental exposure of secrets in public code repositories. Developers might inadvertently push configuration files containing sensitive information to GitHub or other version control systems. Even if the repository is later made private or the keys are revoked, attackers might have already archived the credentials.

Code Exposure in Public Repositories - Diagram

The Impact on Flutter Applications

For Flutter developers, improper credential usage can have severe implications. Let’s examine the different dimensions of impact:

  • Compromised User Data: When credentials are exposed, attackers can gain unauthorized access to backend services, allowing them to read, modify, or delete user data. This violates user privacy and may result in a loss of trust and potential legal consequences under data protection regulations.

  • Unauthorized API Access: API keys and tokens are often the gatekeepers to essential backend functionalities. If these keys are compromised, an attacker can misuse the API to perform unauthorized operations. This can lead to manipulating business logic, fraudulent transactions, or even complete service disruption.

  • Financial and Reputational Damage: Organizations that suffer from data breaches often face significant economic losses from direct damages and the cost of incident remediation. Additionally, the reputational damage from a security breach can be long-lasting, affecting user retention and overall business performance.

  • Increased Maintenance and Compliance Costs: Recovering a security breach involves substantial effort. Developers need to rotate keys, fix vulnerabilities, and possibly rebuild parts of the app. This increased workload can delay new features and lead to higher long-term maintenance costs. Moreover, non-compliance with industry standards might lead to penalties and fines.

The Impact on Flutter Applications - Diagram

Mitigation Strategies and Best Practices

Managing credentials securely in Flutter isn’t just about avoiding hardcoded secrets—it's about establishing a comprehensive strategy encompassing every step of the credential lifecycle. Let's see what are the possibilities.

Mitigation Strategies and Best Practices - Diagram

1. Navigating Platform-Specific Storage Differences

Flutter apps run on both iOS and Android, and each platform offers its secure storage mechanisms: iOS has Keychain, and Android offers Keystore. A common challenge is ensuring your credential storage code works seamlessly across platforms without compromising security.

Use the flutter_secure_storage package. It abstracts away the platform-specific details and allows you to store sensitive data encrypted.

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

final FlutterSecureStorage secureStorage = FlutterSecureStorage();

// Store credential securely
Future<void> saveCredential(String key, String value) async {
  try {
    await secureStorage.write(key: key, value: value);
    print('Credential saved securely.');
  } catch (error) {
    print('Error saving credential: $error');
  }
}

// Retrieve credential securely
Future<String?> loadCredential(String key) async {
  try {
    String? value = await secureStorage.read(key: key);
    return value;
  } catch (error) {
    print('Error reading credential: $error');
    return null;
  }
}

This unified API ensures that whether your app runs on iOS or Android, credentials are stored using the best available security features of the respective platforms.

2. Overcoming Rapid Prototyping and Development Pressures

Under the pressure of rapid development, developers might use quick-and-dirty solutions—such as hardcoding credentials or using less secure methods shared_preferences—to speed up prototyping. However, these shortcuts can leave your app vulnerable when it scales to production.

import 'package:shared_preferences/shared_preferences.dart';

Future<void> insecureStoreToken(String token) async {
  SharedPreferences prefs = await SharedPreferences.getInstance();
  // Insecure storage: data is stored in plain text.
  await prefs.setString('apiToken', token);
}

Even during the prototyping phase, use secure storage from the start to instill good habits. This minimizes technical debt when transitioning to production.

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

final FlutterSecureStorage secureStorage = FlutterSecureStorage();

Future<void> storeTokenSecurely(String token) async {
  await secureStorage.write(key: 'apiToken', value: token);
  print('Token stored securely.');
}

3. Mitigating Risks of Inadequate Developer Awareness

Developers unaware of security best practices might inadvertently expose sensitive information—for example, through debug logs or by pushing configuration files with credentials to version control.

void logSensitiveData(String token) {
  // Risk: Logging tokens in debug output can expose them.
  print('DEBUG: User token is $token');
}

There are two ways you can mitigate this issue:

  • Use Environment Variables: Instead of hardcoding credentials, manage them through environment variables using the flutter_dotenv package. This keeps sensitive data out of your source code.

import 'package:flutter_dotenv/flutter_dotenv.dart';

Future<void> main() async {
  // Load environment variables from the .env file
  await dotenv.load(fileName: ".env");
  String apiKey = dotenv.env['API_KEY'] ?? '';
  runApp(MyApp(apiKey: apiKey));
}
  • Sanitize Logs in Production: Ensure that the debugging of sensitive information is disabled or sanitized in production builds.

4. Managing Credential Lifecycles: Rotation and Expiry

Using long-lived credentials increases the risk of exploitation. In case of compromise, implementing token rotation and short-lived credentials—such as JSON Web Tokens (JWTs)—minimizes the exposure window.

Scenario: Handling Token Refresh

The app should automatically refresh a token using a secure process when it expires.

import 'dart:convert';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart' as http;

final FlutterSecureStorage secureStorage = FlutterSecureStorage();

Future<bool> refreshToken() async {
  final String? currentRefreshToken = await secureStorage.read(key: 'refreshToken');
  if (currentRefreshToken == null) {
    print('No refresh token found.');
    return false;
  }

  final Uri url = Uri.parse('https://yourapi.com/api/refresh');
  try {
    final response = await http.post(
      url,
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode({'refreshToken': currentRefreshToken}),
    );

    if (response.statusCode == 200) {
      final Map<String, dynamic> data = jsonDecode(response.body);
      final String newJwt = data['jwt'];
      await secureStorage.write(key: 'jwt', value: newJwt);
      print('Token refreshed successfully.');
      return true;
    } else {
      print('Failed to refresh token: ${response.statusCode}');
      return false;
    }
  } catch (error) {
    print('Error during token refresh: $error');
    return false;
  }
}

This mechanism ensures that even if an attacker intercepts a token, its usefulness is limited by its short lifespan.

5. Enhancing Security During Data Transmission

Even with secure local storage, transmitting credentials over insecure channels exposes them to interception. All network communications should occur over HTTPS, and additional measures like certificate pinning can further enhance security.

import 'package:http/http.dart' as http;

Future<void> secureLogin(String username, String password) async {
  final Uri url = Uri.parse('https://secure.example.com/api/login');
  try {
    final response = await http.post(
      url,
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode({
        'username': username,
        'password': password,
      }),
    );
    
    if (response.statusCode == 200) {
      print('Login successful.');
    } else {
      print('Login failed with status: ${response.statusCode}');
    }
  } catch (error) {
    print('Error during login: $error');
  }
}

To further protect against man-in-the-middle attacks, consider implementing certificate pinning. This additional step verifies that your app communicates only with trusted servers.

6. Code Obfuscation and Environment Separation

Another layer of defense is to make it harder for attackers to reverse engineer your app. Flutter provides support for limited code obfuscation (it is mostly just minification!) during the build process, which can help mask the logic of your code by renaming function and class names making it more challenging to navigate during reverse engineering process.

When building your Flutter app for release, you can enable obfuscation with the following command:

flutter build apk --obfuscate --split-debug-info=/<project>/<directory>

In addition, separating sensitive configuration from your main codebase using environment variables or external configuration files further reduces exposure. Tools like flutter_dotenv allow you to manage configuration settings without embedding them directly into the source code.

7. Integrating Runtime Application Self-Protection (RASP)

Runtime Application Self-Protection (RASP) is an assertive security technology that monitors your application in real-time to detect, alert, and even block malicious activities as they occur. RASP embeds security controls within the application to analyze its behavior during execution and respond to threats dynamically.

How RASP Can Help

  1. Real-Time Threat Detection:

    1. RASP continuously monitors the app’s runtime behavior to identify suspicious actions, such as abnormal API calls, repeated failed login attempts, or attempts to tamper with the app’s execution. This allows your app to react immediately to potential threats.

  2. Automated Response:

    1. Upon detecting a threat, RASP can trigger automated responses—like terminating a session, logging the event, or alerting a backend system—thus minimizing the window of vulnerability.

  3. Enhanced Forensics:

    1. By logging detailed information about runtime events, RASP can help you understand how an attack was attempted, providing valuable insights for further strengthening your app's security.

  4. Additional Layer of Defense:

    1. Even if vulnerabilities exist in other parts of your application, RASP serves as a last line of defense by ensuring that malicious behavior is caught and mitigated during execution.

One excellent tool for this purpose is freeRASP, a Flutter package that brings RASP capabilities directly into your project. freeRASP helps detect runtime threats, logs them, and even allows you to trigger automated responses to mitigate potential damage.

Integrating freeRASP into Your Flutter App

Integrating freeRASP is straightforward. The package provides an initialization function where you can set up a callback for when a threat is detected. Below is an example of how to integrate freeRASP in your Flutter application.

Step 1: Add freeRASP to Your Project

Add freeRASP to your pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
  freerasp: ^1.0.0  # Check for the latest version on pub.dev

Then, run flutter pub get to install the package.

Step 2: Initialize freeRASP in Your Main Function

Set up freeRASP during the initialization phase of your application. This ensures that threat monitoring starts as soon as your app runs.

import 'package:flutter/material.dart';
import 'package:freerasp/freerasp.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Initialize freeRASP with a callback to handle detected threats
  await freeRASP.initialize(
    onThreatDetected: (threatInfo) {
      // Handle the threat: log it, alert the user, or trigger mitigation actions.
      print("Threat Detected: ${threatInfo.message}");
      // For example, you might want to invalidate the current session or logout the user.
    },
  );

  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Secure Flutter App',
      home: HomeScreen(),
    );
  }
}

In this example, freeRASP is initialized before the app starts. The onThreatDetected callback provides you with a threatInfo object that contains details about the detected threat. You can use this information to log events, notify users, or take immediate action to protect your app.

Step 3: Monitor and React to Threats in Critical Areas

While freeRASP continuously monitors your app in the background, you might also want to check for threats at critical junctures—such as during user authentication or before accessing sensitive data. To perform an on-demand check, you can call freeRASP’s functions directly within your app logic.

import 'package:freerasp/freerasp.dart';

Future<void> secureOperation() async {
  // Optionally check for threats before performing a critical operation
  bool threatPresent = await freeRASP.isThreatPresent();
  if (threatPresent) {
    print("Operation halted due to detected threat.");
    // Take appropriate measures, such as blocking access or alerting the user
    return;
  }
  
  // Continue with the secure operation if no threats are detected
  // ... your secure code here ...
}

This example demonstrates how you might integrate an additional security check before executing sensitive operations, reinforcing the protection provided by freeRASP.

Implementing a Secure Credential Management Flow

Let’s build a more complete example that shows how a Flutter app can handle secure credential management from login to token rotation. This example assumes a backend that issues a JWT and a refresh token.

Step 1: Secure Login and Token Storage

First, we need to implement a login method that securely stores both the JWT and the refresh token.

import 'dart:convert';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart' as http;

final FlutterSecureStorage secureStorage = FlutterSecureStorage();

Future<bool> login(String username, String password) async {
  final Uri url = Uri.parse('https://secure.example.com/api/login');
  try {
    final response = await http.post(
      url,
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode({
        'username': username,
        'password': password,
      }),
    );

    if (response.statusCode == 200) {
      // Parse response to extract tokens
      final Map<String, dynamic> data = jsonDecode(response.body);
      final String jwt = data['jwt'];
      final String refreshToken = data['refreshToken'];

      // Store tokens securely
      await secureStorage.write(key: 'jwt', value: jwt);
      await secureStorage.write(key: 'refreshToken', value: refreshToken);
      print('Login successful, tokens stored securely.');
      return true;
    } else {
      print('Login failed with status: ${response.statusCode}');
      return false;
    }
  } catch (error) {
    print('Error during login: $error');
    return false;
  }
}

Step 2: Using the Token in API Requests

Once the JWT is stored, it can be used to authenticate further API requests. Here’s an example of how to attach the token to a request:

Future<http.Response> fetchUserData() async {
  final String? jwt = await secureStorage.read(key: 'jwt');
  if (jwt == null) {
    throw Exception('JWT not found. User might not be logged in.');
  }

  final Uri url = Uri.parse('https://secure.example.com/api/userdata');
  final response = await http.get(
    url,
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer $jwt',
    },
  );

  return response;
}

Step 3: Handling Token Expiry and Refresh

JWTs are designed to expire after a short period. When a token expires, the app should use the stored refresh token to request a new JWT.

Future<bool> refreshJwt() async {
  final String? refreshToken = await secureStorage.read(key: 'refreshToken');
  if (refreshToken == null) {
    print('Refresh token not found.');
    return false;
  }

  final Uri url = Uri.parse('https://secure.example.com/api/refresh');
  try {
    final response = await http.post(
      url,
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode({'refreshToken': refreshToken}),
    );

    if (response.statusCode == 200) {
      final Map<String, dynamic> data = jsonDecode(response.body);
      final String newJwt = data['jwt'];
      // Update the stored JWT
      await secureStorage.write(key: 'jwt', value: newJwt);
      print('JWT refreshed successfully.');
      return true;
    } else {
      print('Failed to refresh JWT. Status: ${response.statusCode}');
      return false;
    }
  } catch (error) {
    print('Error refreshing JWT: $error');
    return false;
  }
}

Putting It All Together

Improving credential security is not about relying on a single solution; it’s about building a defense-in-depth strategy that layers multiple protective measures.

Putting It All Together - Overview Diagram

For Flutter developers, this involves:

  • Adopting Secure Storage: Replace insecure storage practices with robust solutions like flutter_secure_storage.

  • Ensuring Secure Communications: Always use HTTPS and consider implementing certificate pinning to protect data in transit.

  • Implementing Token Management: Use JWTs with short lifespans and secure refresh mechanisms to minimize exposure.

  • Enhancing Code Security: Utilize obfuscation and environment variable management to keep sensitive data out of the codebase.

  • Monitoring at Runtime: Consider RASP tools to dynamically detect and respond to credential misuse.

By addressing each layer of the credential lifecycle—from storage to transmission and renewal—you create a resilient application architecture against external attacks and internal oversights.

Conclusion

M1: Improper Credential Usage poses a serious risk to mobile security, especially for Flutter apps. By leveraging secure storage, enforcing HTTPS, rotating tokens, and integrating runtime monitoring with tools like freeRASP, you can protect sensitive data and build lasting trust with your users. Embrace these best practices to create robust, secure applications, and stay tuned for the next installment in our OWASP Mobile Top 10 series, where we continue to explore actionable strategies to safeguard your mobile apps.

Last updated

Was this helpful?