LogoLogo
HomeArticlesCommunity ProductsPremium ProductsGitHubTalsec Website
  • Introduction
  • articles
    • OWASP Top 10 For Flutter – M6: Inadequate Privacy Controls in Flutter & Dart
    • Simple Root Detection: Implementation and verification
    • OWASP Top 10 For Flutter - M5: Insecure Communication for Flutter and Dart
    • OWASP Top 10 For Flutter – M4: Insufficient Input/Output Validation in Flutter
    • OWASP Top 10 For Flutter – M3: Insecure Authentication and Authorization in Flutter
    • OWASP Top 10 For Flutter – M2: Inadequate Supply Chain Security in Flutter
    • OWASP Top 10 For Flutter - M1: Mastering Credential Security in Flutter
    • Hook, Hack, Defend: Frida’s Impact on Mobile Security & How to Fight Back
    • Emulators in Gaming: Threats and Detections
    • Exclusive Research: Unlocking Reliable Crash Tracking with PLCrashReporter for iOS SDKs
    • 🚀A Developer’s Guide to Implement End-to-End Encryption in Mobile Apps 🛡️
    • How to Block Screenshots, Screen Recording, and Remote Access Tools in Android and iOS Apps
    • Flutter Security 101: Restricting Installs to Protect Your App from Unofficial Sources
    • How to test a RASP? OWASP MAS: RASP Techniques Not Implemented [MASWE-0103]
    • How to implement Secure Storage in Flutter?
    • User Authentication Risks Coverage in Flutter Mobile Apps | TALSEE
    • Fact about the origin of the Talsec name
    • React Native Secure Boilerplate 2024: Ignite with freeRASP
    • Flutter CTO Report 2024: Flutter App Security Trends
    • Mobile API Anti-abuse Protection with AppiCrypt®: A New Play Integrity and DeviceCheck Alternative
    • Hacking and protection of Mobile Apps and backend APIs | 2024 Talsec Threat Modeling Exercise
    • Detect system VPNs with freeRASP
    • Introducing Talsec’s advanced malware protection!
    • Fraud-Proofing an Android App: Choosing the Best Device ID for Promo Abuse Prevention
    • Enhancing Capacitor App Security with freeRASP: Your Shield Against Threats 🛡️
    • Safeguarding Your Data in React Native: Secure Storage Solutions
    • Secure Storage: What Flutter can do, what Flutter could do
    • 🔒 Flutter Plugin Attack: Mechanics and Prevention
    • Protecting Your API from App Impersonation: Token Hijacking Guide and Mitigation of JWT Theft
    • Build secure apps in React Native
    • How to Hack & Protect Flutter Apps — Simple and Actionable Guide (Pt. 1/3)
    • How to Hack & Protect Flutter Apps — OWASP MAS and RASP. (Pt. 2/3)
    • How to Hack & Protect Flutter Apps — Steal Firebase Auth token and attack the API. (Pt. 3/3)
    • freeRASP meets Cordova
    • Philosophizing security in a mobile-first world
    • 5 Things John Learned Fighting Hackers of His App — A must-read for PM’s and CISO’s
    • Missing Hero of Flutter World
Powered by GitBook
LogoLogo

Company

  • General Terms and Conditions

Stay Connected

  • LinkedIn
  • X
  • YouTube
On this page
  • From HTTP to HTTPS
  • TLS 101: Handshake, Trust Stores & Default Validation
  • Certificate Pinning
  • Securing Real‑Time Channels (WebSockets)
  • Protecting Data Inside the Tunnel
  • Platform‑Level Enforcement & HSTS
  • Testing, Monitoring & Resilience
  • Advanced Runtime Protections
  • Conclusion & What’s Next

Was this helpful?

  1. articles

OWASP Top 10 For Flutter - M5: Insecure Communication for Flutter and Dart

PreviousSimple Root Detection: Implementation and verificationNextOWASP Top 10 For Flutter – M4: Insufficient Input/Output Validation in Flutter

Last updated 4 days ago

Was this helpful?

In this fifth installment of our OWASP Top 10 series for Flutter developers, we shift focus to M5: Insecure Communication.In earlier parts, we tackled , , , and , each playing a crucial role in app security. But M5 strikes at the core of how mobile apps operate: how data moves in and out of your Flutter app.

Picture this, you’re in a cozy café, laptop open, integrating a new feature. You join the free Wi‑Fi, hit Run, and data starts to flow. What you don’t see is someone on that same network quietly capturing every byte, login tokens, profile calls, even payment info, because it’s traveling like postcards through a crowded street. That’s the heart of insecure communication.

This article isn't just about fixing bugs, it's about understanding how data is exposed, how attackers think, and how you can prevent silent breaches before they start.Then, let's get started

What Is “Insecure Communication”?

It’s every moment when your data can be watched, intercepted, or altered while in transit.Insecure communication shows up any time the bytes leaving your Flutter app—or your Dart backend—can be read, replayed, or rewritten by someone who wasn’t supposed to see them. It’s not just about typing https:// in your URLs. It’s about every transport your app relies on and every trust decision your code makes along the way.

Remember that café scene? If your request flies in the clear, the stranger at the next table can read or even modify it. And even when we think we’re being careful, we sometimes stub out certificate checks during testing, accept sketchy proxies during debugging, or log sensitive payloads in places we shouldn’t. These shortcuts pull us right back into danger—even if we’re technically using HTTPS.

Core Channels

Now that we’ve seen what insecure communication looks like in practice, let’s break down the channels where your data flows. We’ll get to code shortly, but first, here’s where risks tend to hide.These are the lifelines of your app. If any of them are exposed, everything else downstream is vulnerable.REST / GraphQLAlways use https://. Never put credentials or tokens in query strings—send them in headers or the body.

final uri = Uri.https('api.yourdomain.com', '/profile');

final res = await http.post(uri, headers: {
  'Authorization': 'Bearer $token',
  'Content-Type': 'application/json',
});

WebSocketsUse wss://, never ws://. Apply the same certificate validation and pinning logic as your REST layer.

If you’re using GraphQL subscriptions with graphql_flutter, confirm the client connects over wss:// and inherits your validation logic. No exceptions.

Raw TCP & gRPCEncryption is not optional. For raw sockets, wrap in SecureSocket. For gRPC, use ChannelCredentials.secure() and pin the server cert like you would for any HTTPS call.

// gRPC – secure channel in Dart
import 'package:grpc/grpc.dart';

final channel = ClientChannel(
  'api.yourdomain.com',
  port: 443,
  options: const ChannelOptions(
    credentials: ChannelCredentials.secure(),
  ),
);

SMS One-Time CodesAvoid them where possible, they’re vulnerable to SIM swaps and silent interception. If you must use them, expire them quickly and add detection for suspicious activity.Bluetooth & NFCPairing doesn’t mean encryption. Use BLE Secure Connections for Bluetooth, and encrypt NFC payloads end-to-end.

Who’s Listening?

No matter which transport you choose, if your app sends data in the clear—or trusts the wrong certificate—you’re vulnerable. Here are the common actors waiting to take advantage:

  • Passive listeners on public or compromised networks

  • Active man-in-the-middle attackers who intercept or inject traffic

  • Rogue access points (evil-twin Wi-Fi, hijacked routers)

  • On-device malware or tools running on rooted/jailbroken phones

  • Enterprise or MDM-pushed root CAs that override your app’s trust settings

Even one misconfigured transport can expose your entire session.Next, we’ll dive into the most common and preventable mistake: letting http:// endpoints sneak into production.

From HTTP to HTTPS

The fastest way to lose user trust, and data, is to let a single http:// endpoint slip into production. Before we talk about certificate pinning or advanced validation, we need to eliminate the most basic mistake: allowing unencrypted traffic in the first place.

Why Plain HTTP Is a Silent Killer

Using http:// is like taping your house key to the front gate—anyone walking by can grab it. Traffic moves in cleartext: tokens, cookies, form data, even search queries. All of it is readable to anyone on the same network, or logged by any proxy in the chain.And worse? A man-in-the-middle doesn’t even have to “break” encryption—because there isn’t any.The fix isn’t glamorous, but it’s non-negotiable:Encrypt every byte in transit and refuse to speak plaintext, ever.

Get a Certificate (Two Paths)

If you’re running a Dart backend—whether with shelf, HttpServer, or another server framework—you need a TLS certificate to serve HTTPS. This allows your Flutter app to connect securely, validate the server’s identity, and lay the foundation for things like certificate pinning.

Let’s look at two common paths: one for production deployments, and one for local development. You can also get certificates from a cloud provider or third-party CA—more on that below.

Option 1: Trusted Certificate (Let’s Encrypt or Third-Party)

For production, use a publicly trusted certificate from:

  • Your cloud provider (e.g., AWS ACM, Google Managed Certs, Azure App Service)

  • A third-party certificate authority like DigiCert, GlobalSign, or ZeroSSL

Here’s how to set one up using Let’s Encrypt and Certbot on Ubuntu:

sudo apt update
sudo apt install certbot
sudo certbot certonly --standalone -d api.yourdomain.com

To test auto-renewal:

sudo certbot renew --dry-run

Certificates will be saved to:

/etc/letsencrypt/live/api.yourdomain.com/├── fullchain.pem
  └── privkey.pem

You’ll reference these in Dart using SecurityContext() when starting your server.

💡 If you’re using a cloud platform (e.g., AWS, GCP, Azure), they may handle TLS termination for you. In that case, your Dart backend only sees HTTPS traffic forwarded as HTTP from the load balancer.

Option 2: Self-Signed Certificate (for Local Development)

For local testing and emulator development, a self-signed cert works fine. It’s not trusted by browsers or real devices—but that’s okay in dev. You’ll still benefit from full HTTPS support in your backend and Flutter app.To generate one using OpenSSL:

openssl req -x509 -nodes -days 365 \
  -newkey rsa:2048 -keyout localhost.key \
  -out localhost.crt -subj "/CN=localhost"

This creates two files:

  • localhost.crt — the certificate

  • localhost.key — the private key

Save these in your project directory or anywhere accessible by your Dart server.

Serve HTTPS in Dart with Shelf

Here’s how to use either cert type in a Dart backend with the shelf package:

import 'dart:io';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart';
import 'package:shelf_router/shelf_router.dart';

// Configure routes.
final _router =
    Router()
      ..get('/', _rootHandler)
      ..get('/echo/<message>', _echoHandler);

Response _rootHandler(Request req) {
  return Response.ok('Hello, World!\n');
}

Response _echoHandler(Request request) {
  final message = request.params['message'];
  return Response.ok('$message\n');
}

SecurityContext getSecurityContext() {
  // Bind with a secure HTTPS connection
  final chain =
      Platform.script
          .resolve('certificates/localhost.crt')
          .toFilePath(); // Point to the localhost cert
  final key =
      Platform.script
          .resolve('certificates/localhost.key')
          .toFilePath(); // Point to the localhost key

  return SecurityContext()
    ..useCertificateChain(chain)
    ..usePrivateKey(
      key,
      password: 'dartdart',
    ); // You can set a password or leave it empty if not used
}

⚠️ If you're using a self-signed cert, your Flutter app may reject the connection unless you override validation during development (more on that in section 4).

Here is now how we run our application with https supported.

void main(List<String> args) async {
  // Use localhost for local testing.
  final ip =
      InternetAddress
          .loopbackIPv4; // This ensures the server binds to localhost.

  // Configure a pipeline that logs requests.
  final _handler = Pipeline().addMiddleware(logRequests()).addHandler(_router);

  // Use port 443 for HTTPS.
  final port = int.parse(Platform.environment['PORT'] ?? '443');
  final server = await serve(
    _handler,
    ip,
    port,
    securityContext: getSecurityContext(),
  );
  print('Server listening on https://localhost:$port');
}

While this seems to be more backend related, but we can also make sure our Flutter is adhering to best practices.

Lock Your App to HTTPS Only

Ensuring that your app refuses to connect to any HTTP endpoints is crucial. This prevents your app from accidentally leaking sensitive data over unencrypted channels. Let’s configure both Android and iOS to enforce HTTPS-only communication and block any insecure traffic.

Android gives you granular control over cleartext (non-HTTPS) traffic through Network Security Config. This is where you tell the app to only use HTTPS for production traffic, while allowing cleartext only for certain cases (like local development).

  1. Create or modify the network_security_config.xml file in your project:

  2. Path: android/app/src/main/res/xml/network_security_config.xml.

<network-security-config>
  <domain-config cleartextTrafficPermitted="false">
    <domain includeSubdomains="true">api.yourdomain.com</domain>
  </domain-config>
</network-security-config>
  • cleartextTrafficPermitted="false": This ensures that cleartext traffic (i.e., http://) is blocked for all domains.

  • The domain field specifies that only secure traffic (https://) is allowed for api.yourdomain.com and its subdomains.

  1. Reference the network_security_config.xml file in your AndroidManifest.xml:

  2. Path: android/app/src/main/AndroidManifest.xml.

Inside the <application> tag, add:

<applicationandroid:networkSecurityConfig="@xml/network_security_config">
    <!-- Other app configurations --></application>

This step ensures that your app adheres to the defined network security rules.


Need to Allow http://10.0.2.2 for Local Development (Emulator)?For local development on Android, the emulator uses http://10.0.2.2 as the host for local services. To allow cleartext traffic for this during debugging, add a second domain-config block under the <network-security-config>:

<network-security-config>
  <domain-config cleartextTrafficPermitted="false">
    <domain includeSubdomains="true">api.yourdomain.com</domain>
  </domain-config>

  <!-- Allow cleartext traffic for emulator --><domain-config cleartextTrafficPermitted="true">
    <domain includeSubdomains="true">10.0.2.2</domain>
  </domain-config>
</network-security-config>

let me give you a quick tip, wrap this configuration in a debug-only resource to ensure that it doesn't end up in production builds.

iOS: App Transport Security (ATS)

iOS enforces secure connections by default through App Transport Security (ATS), blocking any cleartext (HTTP) traffic. However, sometimes you might need to allow insecure connections during development, especially for local testing or non-production services.

  1. Modify your Info.plist file (located at ios/Runner/Info.plist):

Add or modify the following ATS settings:

<key>NSAppTransportSecurity</key>
<dict>
  <key>NSAllowsArbitraryLoads</key>
  <false/> <!-- Disable arbitrary loads by default --><!-- Optional dev exception for localhost --><key>NSExceptionDomains</key>
  <dict>
    <key>localhost</key>
    <dict>
      <key>NSExceptionAllowsInsecureHTTPLoads</key><true/> <!-- Allow insecure traffic only for localhost --><key>NSIncludesSubdomains</key><true/>
    </dict>
  </dict>
</dict>

This configuration:

  • Disables arbitrary loads (HTTP connections) for production builds.

  • Allows insecure connections (HTTP) only for localhost, useful when you're testing locally during development.

  1. Test your setup:

  2. Build a release version of your app (via flutter build ios), then try hitting an http:// URL. You should see it fail as expected—indicating that your app is correctly enforcing HTTPS.

The One‑Line Secure Client in Flutter

You don’t need complex setups to ensure secure HTTP requests. Dart’s http package uses default TLS validation, so you can trust it right out of the box. Here’s how you make a secure request with it:

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

final res = await http
    .get(Uri.https('api.yourdomain.com', '/status'))
    .timeout(const Duration(seconds: 10));

print('Status ${res.statusCode}');

This simple line of code ensures that:

  • The request will only succeed if the connection is encrypted (via HTTPS).

  • If the server presents an invalid or expired certificate, Dart will throw a handshake exception, preventing the connection from being established.

  • Your app will fail closed (securely) rather than silently accepting a downgraded insecure connection.

If you need additional layers of security, like certificate pinning or advanced validation, you can wrap this client using IOClient from the http/io_client.dart package, giving you finer control over certificate handling.

TLS 101: Handshake, Trust Stores & Default Validation

Before diving into certificate pinning or enforcing strict policies, it’s essential to understand how TLS (Transport Layer Security) works in Dart and Flutter by default. TLS is the foundation of secure communication, and it’s crucial to know how your app handles it when performing HTTPS requests.

How Dart’s HttpClient Validates Certificates by Default

When you call http.get(...) or use Dart’s HttpClient, Dart automatically performs a standard TLS handshake to ensure the connection is secure:

  1. ClientHello: Your app initiates the handshake by suggesting which protocol versions (e.g., TLS 1.2, TLS 1.3) and cipher suites it supports.

  2. ServerHello & Certificate: The server responds with its chosen protocol and cipher suite, along with its certificate chain.

  3. Validation: Dart’s HttpClient performs the following checks on the server’s certificate:

    • Certificate Chain: Dart ensures that the certificate is linked to a trusted root authority in your app’s trust store.

    • Expiry Check: Dart checks if the certificate is expired.

    • Hostname Matching: Dart ensures that the hostname you requested matches the certificate’s Subject Alternative Name (SAN) or Common Name (CN).

  4. Key Exchange & Encryption: If the certificate passes all checks, Dart and the server exchange session keys and establish an encrypted connection.

The Simple, Secure Call

Under the hood, Flutter’s http package uses IOClient, which delegates to the same HttpClient logic. The simplest, most secure call looks like this:

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

final response = await http.get(Uri.parse('https://api.myapp.com/data'));
// If the certificate is invalid, this throws a handshake exception.

With this setup, you don’t need any additional code to validate certificates. Just use https:// for secure communication and ensure you don’t disable certificate validation callbacks.

Understanding SecurityContext in Dart

  • Store certificates to validate servers (e.g., SSL/TLS certificates).

  • Store a private key for your server when acting as a server.

  • Control which TLS protocols to use (like enforcing TLS 1.3).

  • Manage certificate pinning to ensure only specific certificates or public keys are trusted.

You can create and configure a SecurityContext in Dart to use it with an HttpClient or HttpServer when making or accepting HTTPS requests.For example, here’s how you load a certificate chain and a private key into a SecurityContext:

import 'dart:io';

SecurityContext getSecurityContext() {
  final context = SecurityContext();

  // Load certificate chain (e.g., full chain in PEM format)
  context.useCertificateChain('path_to_fullchain.pem');

  // Load private key (e.g., for a server)
  context.usePrivateKey('path_to_privatekey.pem', password: 'your_password');

  return context;
}
  • useCertificateChain: Loads the certificate chain from a file (in PEM format). The certificate chain can include the server's certificate and any intermediate certificates up to a trusted root certificate.

  • usePrivateKey: Loads the private key used for the server's certificate. This key is crucial for secure communication, as it enables the server to prove its identity.

💡 Tip: For local development, you may use self-signed certificates for testing. Just ensure the server trusts them by adding client.badCertificateCallback or using an assert() for dev-mode certificates.

Why SecurityContext Matters

In production, you should never disable certificate verification. Doing so opens the door to severe security risks, such as man-in-the-middle (MITM) attacks, where an attacker could intercept and modify your traffic.SecurityContext provides a secure, flexible, and powerful way to manage SSL/TLS connections in Dart. By configuring it properly, you ensure your app can securely connect to remote servers while avoiding common pitfalls.

Enforcing TLS Versions

You can enforce the use of specific TLS versions (e.g., TLS 1.3) by configuring the SecurityContext. This is useful to make sure your app only uses the most secure and up-to-date protocols.

SecurityContext context = SecurityContext()
  ..useTls13 = true;  // Enforce only TLS 1.3

You can also define ALPN (Application-Layer Protocol Negotiation) to ensure certain protocols are used, like HTTP/2:

context.setAlpnProtocols(['h2', 'http/1.1'], false); // Prefer HTTP/2, fall back to HTTP/1.1

This ensures that your app negotiates the best available protocol for secure communication.

Safe Dev-Mode Overrides for Self-Signed Certificates

During development, you may need to connect to a local server with a self-signed certificate (common for testing). Instead of disabling validation globally (which is dangerous), you can apply a scoped override that only activates for your local development server and only in debug builds.Here’s how you can safely trust your local dev server during testing:

import 'dart:io';

HttpClient createDevHttpClient() {
  final client = HttpClient();
  assert(() {
    // DEBUG ONLY: Trust our local dev server at 10.0.2.2 (emulator)
    client.badCertificateCallback = (cert, host, port) {
      return host == '10.0.2.2';
    };
    return true;
  }());
  return client;
}
  • The assert() ensures that this override only happens in debug mode (i.e., during development). The override will be stripped out in release builds, preventing any accidental trust issues.

  • client.badCertificateCallback allows your app to trust the server, even if the certificate is self-signed, but only if the host is 10.0.2.2 (the default for local development in Android emulators).

Why “Trust-All” Is a Recipe for Disaster

It might seem tempting to fix the CERTIFICATE_VERIFY_FAILED error by blindly accepting all certificates, like so:

client.badCertificateCallback = (_, __, ___) => true;

However, this is extremely dangerous. What you’ve just done is disable all certificate validation. This effectively turns HTTPS into plain HTTP, leaving your app wide open to man-in-the-middle (MITM) attacks. Any attacker could present a fake certificate, and your app would blindly trust it.It’s like leaving your front door wide open and assuming no one will walk in. You won’t see the attacker coming, and they can capture everything you send or receive.

Certificate Pinning

Certificate pinning adds an extra layer of security to your app by hard-wiring trust. Instead of relying on the OS trust store to validate certificates, pinning ensures that your app only trusts the specific certificate or public key it was shipped with.

This makes it much harder for attackers to intercept or manipulate traffic, even if they manage to install a rogue certificate authority (CA) on the device.

Why and When to Use Certificate Pinning

  • Extra Security Layer: Pinning ensures that your app will only trust a specific certificate or public key for a particular domain.

  • When to Use: Pinning is essential for apps that handle sensitive data, like those in finance, healthcare, or any domain that makes your app a target.

  • Downsides: Pinning requires operational overhead. You need to rotate pins before the certificate changes, or users will lose connectivity. Always ship a backup pin to survive certificate renewals.

  • When Not to Use: Pinning isn’t required for every app, especially if the server’s certificate is expected to remain stable and not change often. But it’s a strong defensive measure if you need to minimize risk.

Why should you choose Dynamic TLS Pinning over the static certificate pinning?

Implementation of certificate pinning will usually use certificates hard-coded in applications. This approach will enforce both the rebuild of an application and the update for users when the hardcoded certificate is about to expire or is revoked. In applications that are pinning multiple certificates, this enforcement may occur too often.

Export the Server Certificate and SPKI Fingerprint

The first step in pinning is obtaining the certificate or public key you’ll pin to. You can extract the server’s certificate and its SPKI fingerprint using OpenSSL.

  1. Dump the server's certificate (replace api.yourdomain.com with your target domain):

# Fetch the leaf certificate from the server
openssl s_client -connect api.yourdomain.com:443 -servername api.yourdomain.com </dev/null \
  | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > server_cert.pem
  1. Generate the SPKI hash (the public key’s SHA-256 hash):

# Generate the public key hash (SPKI)
openssl x509 -in server_cert.pem -noout -pubkey \
  | openssl pkey -pubin -outform der \
  | openssl dgst -sha256 -binary \
  | openssl base64
  1. Save the Base64 hash: This is the SPKI fingerprint you’ll pin.

Manual Pinning with SecurityContext

Once you have the certificate or SPKI hash, you can manually configure your app to only trust this certificate.

  1. Add server_cert.pem to your assets and declare it in pubspec.yaml:

flutter:assets:- assets/server_cert.pem
  1. Create a pinned HttpClient that uses your certificate:

import 'dart:io';
import 'package:flutter/services.dart' show rootBundle;
import 'package:http/io_client.dart';
import 'package:http/http.dart' as http;

Future<http.Client> createPinnedClient() async {
  final ctx = SecurityContext(withTrustedRoots: false);
  final pem = await rootBundle.load('assets/server_cert.pem');
  ctx.setTrustedCertificatesBytes(pem.buffer.asUint8List());

  final ioClient = HttpClient(context: ctx)
    ..badCertificateCallback = (_, __, ___) => false; // Reject anything not in ctx
  return IOClient(ioClient);
}

Future<void> fetchSecure() async {
  final client = await createPinnedClient();
  final res = await client.get(Uri.https('api.yourdomain.com', '/data'));
  print(res.body);
  client.close();
}
  • setTrustedCertificatesBytes() ensures only the certificates you’ve added are trusted.

  • badCertificateCallback is used to reject any certificate not in the pinned certificate list.

Pinning by Fingerprint in a Callback

If you prefer to pin using the SPKI fingerprint instead of the full certificate, you can use the certificate’s hash directly in the badCertificateCallback.

import 'dart:convert';
import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:http/io_client.dart';

const expected = 'AbCdEfGhIjKlMnOp...'; // The Base64 SPKI hash

HttpClient fingerprintClient() {
  final c = HttpClient();
  c.badCertificateCallback = (X509Certificate cert, String host, int port) {
    if (host != 'api.yourdomain.com') return false;
    final hash = sha256.convert(cert.der).bytes;
    return base64.encode(hash) == expected;  // Compare with expected SPKI hash
  };
  return c;
}
  • This approach avoids storing the full certificate and directly compares the certificate's hash against the expected value.

  • Wrap the HttpClient in IOClient and use it with the http package just like before.

Using http_certificate_pinning for Less Boilerplate

import 'package:http_certificate_pinning/secure_http_client.dart';

Future<http.Client> buildPinned(List<String> hashes) async =>
    SecureHttpClient.build(
      hashes.map((h) => h.replaceAll(':', '').toUpperCase()).toList(),
      shaType: SHA.SHA256,
      allowInvalidCertificates: false,
    );

This package abstracts away much of the manual setup and makes pinning easier to implement.

Rotation Strategy and Gotchas

  1. Ship at least two pins: Always have both the current and next certificate pinned, so you can smoothly rotate certificates.

  2. Schedule renewals: Coordinate with your backend dev-ops to ensure the new certificate is live before the old one expires. Test failure scenarios by pointing your app to a server with an unpinned certificate to make sure it refuses to connect.

  3. Obfuscate your pins if you’re worried about reverse engineering, but remember that an attacker with full device control can still bypass pinning. Pinning raises the bar, but it’s not a bulletproof shield.

  4. Proactively rotate pins: Set up calendar reminders or CI/CD hooks to rotate pins before they expire.

With certificate pinning in place, your app will refuse to connect to impostor servers, even if they present a seemingly valid certificate. Pinning ensures that only the expected certificate or public key is trusted, adding defense in depth to your app’s security.

Securing Real‑Time Channels (WebSockets)

When your app needs instant updates—whether it's for chat, live dashboards, payments, or presence data—you’ll likely use WebSockets. They keep the connection alive and feel magical for real-time interactions. But don’t forget: ws:// is unencrypted and sends everything in plain text.Short take: Treat WebSockets exactly like HTTPS requests—TLS is mandatory, pinning and validation carry over, and never silently fall back to an insecure URL.

ws:// vs wss://

  • ws://: WebSockets over plain HTTP—insecure, unencrypted, and prone to MITM (Man-In-The-Middle) attacks. Anyone on the same network can read or inject data.

  • wss://: WebSockets over TLS—secure, encrypted, and ensures server identity and data integrity, just like HTTPS.

When working with real-time traffic, remember: it’s as sensitive as REST traffic. Always use wss://, especially when transmitting sensitive data (chat, payments, user info). Never send sensitive data via ws://.

Secure WebSocket Client in Flutter (No Extra Packages)

The web_socket_channel package, part of the Flutter standard toolbox, supports WebSocket connections and allows you to pass a custom HttpClient—so you can reuse the pinning logic from Section 5.Here’s how you can create a secure WebSocket connection with certificate pinning:

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

HttpClient _pinnedClient() {
  // Reuse createPinnedClient() from Section 5 or build your own
  final client = HttpClient();
  client.badCertificateCallback = (cert, host, port) {
    // Reject unless host is our server AND fingerprint matches
    return false; // Replace with your real certificate check
  };
  return client;
}

void connectRealtime() {
  final channel = IOWebSocketChannel.connect(
    Uri.parse('wss://realtime.yourdomain.com/socket'),
    customClient: _pinnedClient(), // Enforces pinning and trust rules
    protocols: ['json'],           // Optional sub‑protocols
  );

  channel.stream.listen(
    (data) => print('Got data: $data'),
    onError: (e) => print('WebSocket error: $e'),
    onDone: () => print('WebSocket closed with code: ${channel.closeCode}'),
  );

  // Send a heartbeat to keep the connection alive
  channel.sink.add('{"type":"ping"}');
}

Key Rules for WebSocket Security

  1. Never deploy ws://—always use wss:// for production. Strip out any ws:// references in your release configurations.

  2. If the server uses a self-signed certificate in staging, ensure it’s only trusted during debug builds. Production must fail closed if the certificate is invalid.

  3. Treat handshake failures as fatal—don’t auto-retry insecure WebSocket connections. Never fall back to ws://.

  4. Apply the same timeout and retry backoff logic you use for REST API calls. Just because the WebSocket is open doesn’t mean it’s healthy.

Backend Setup with Dart (dart_frog or shelf)

If you’ve followed Section 3 and your backend already serves HTTPS with a valid certificate, WebSocket connections inherit the same secure setup. Here’s how to handle WebSocket upgrade requests in a Dart server (using shelf or dart_frog):

// Inside a shelf handler
if (WebSocketTransformer.isUpgradeRequest(request.headers)) {
  final socket = await WebSocketTransformer.upgrade(request);
  socket.listen((event) {
    // Handle incoming data (e.g., JSON)
  });
  return null; // The request has been hijacked by WebSocket
}

As long as the listener runs on port 443 and is using the proper SSL/TLS certificate, the WebSocket will automatically use wss://.

Testing the Failure Mode

To ensure everything works securely, test the failure mode:

  1. Install mitmproxy on a test device and install its root certificate.

  2. Modify your app’s config to point at https://realtime.yourdomain.com.

  3. If your WebSocket pinning is set up correctly, the connection should fail with a handshake error when the invalid certificate is presented.

  4. Your app should show a user-friendly error like “Secure connection failed”. Never allow it to silently downgrade to an insecure connection.

With secure WebSocket connections (wss://), your app can safely transmit real-time data just as it does with HTTPS. Pinning certificates and ensuring strong validation reduces the risk of MITM attacks and ensures data integrity.

Protecting Data Inside the Tunnel

While TLS ensures that your data is protected in transit, you still need to be mindful of where you store sensitive information in your app, and how it is handled on the device. Storing secrets in the wrong place or accidentally logging sensitive data can undermine your security. Let’s break down best practices for safe payload design, secure storage, and leak-free logging.

Keep Secrets out of URLs

Never put sensitive data (like tokens) in URLs. Query strings are easily logged in proxies, crash reports, or even screenshots. The following is a bad example:

final uri = Uri.parse('https://api.yourdomain.com/user?token=abc123');
await http.get(uri);

Instead, use headers or JSON bodies to transmit sensitive data:

final uri = Uri.https('api.yourdomain.com', '/user');
await http.post(
  uri,
  headers: {
    'Authorization': 'Bearer abc123',
    'Content-Type': 'application/json',
  },
  body: jsonEncode({'includeSensitive': true}),
);

Tokens or credentials in URLs can easily be captured by intermediate services, proxies, or even browser history.

Explicit Headers and JSON Bodies

Always:

  • Set Content-Type: application/json when sending JSON data.

  • Convert maps to JSON using jsonEncode (never concatenate strings).

  • Capture response.statusCode and handle errors like 401/403 properly by failing closed instead of silently retrying.

final response = await http.post(uri, headers: headers, body: jsonEncode(body));

if (response.statusCode == 401 || response.statusCode == 403) {
  throw Exception('Authentication failed');
}

This ensures your app handles errors in a predictable and secure manner, rather than accidentally exposing sensitive data.

Secure Storage on Device

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

final storage = FlutterSecureStorage(
  aOptions: const AndroidOptions(encryptedSharedPreferences: true),
);

// Store the token securely
await storage.write(key: 'auth_token', value: token);

// Retrieve the token
final token = await storage.read(key: 'auth_token');

// Delete the token on logout
await storage.delete(key: 'auth_token');
  • When to wipe tokens: If the device is rooted, the token could still be extracted from memory. Wipe the token on every "app background" event and rely on refresh tokens to quickly obtain a new one.

  • Don’t store tokens in memory or static variables between sessions. Always reload tokens securely from encrypted storage when needed.

Log and Analytics Hygiene

Sensitive data can easily leak through logs. A stray print(response.body) or verbose logging left in production code is an open invitation for a data leak. Here’s how to keep your logging secure:

  • Remove verbose logging in production using flags like --dart-define=FLUTTER_WEB_LOGS=false or wrap logs inside assert(() { … }()) for development-only logging.

  • Redact sensitive data in crash reporting services (e.g., Crashlytics, Sentry). Configure hooks to automatically redact tokens, emails, GPS coordinates, or anything personally identifying.

HubAdapter().configureScope((scope) {
  scope.addEventProcessor((event, {hint}) {
    final scrubbed = event.copyWith(
      request: event.request?.copyWith(headers: {'Authorization': '[REDACTED]'}),
    );
    return scrubbed;
  });
});

Application-Level Encryption (When TLS Isn’t Enough)

In certain scenarios, especially when regulations or threat models demand that data remain unreadable even on a compromised transport layer, you may want to encrypt the payload itself, in addition to relying on TLS.Here’s an example using the encrypt package for AES encryption:

import 'package:encrypt/encrypt.dart';

final key = Key.fromUtf8('32CharactersLongEncryptionKey!');
final iv = IV.fromLength(16);
final aes = Encrypter(AES(key));

String wrap(String plain) => aes.encrypt(plain, iv: iv).base64;
String unwrap(String b64) => aes.decrypt(Encrypted.fromBase64(b64), iv: iv);
  • Share the encryption key securely with your backend. You can use asymmetric encryption to securely exchange keys (public/private key pairs) or share the key out of band.

  • Use different encryption keys for each user/session, and ensure key rotation occurs regularly.

  • Avoid hardcoding encryption keys directly in the app binary; store them securely.

Platform‑Level Enforcement & HSTS

Even flawless code can be sabotaged by a stray manifest flag or a forgotten server redirect. Lock the doors at the OS and server layers so your app physically cannot talk over cleartext.

Android — Block Cleartext Everywhere

Recent Android versions disable HTTP by default, but one debug tweak can switch it back on. Add a network‑security config that allows only the domains you own, and only over TLS.res/xml/network_security_config.xml

<network-security-config>
  <!-- Disallow HTTP for everything except optional debug hosts -->
  <domain-config cleartextTrafficPermitted="false">
    <domain includeSubdomains="true">api.yourdomain.com</domain>
  </domain-config>

  <!-- DEBUG ONLY: allow the emulator loopback -->
  <debug-overrides>
    <domain-config cleartextTrafficPermitted="true">
      <domain>10.0.2.2</domain>
    </domain-config>
  </debug-overrides>
</network-security-config>

Point your <application> tag at this file:android:networkSecurityConfig="@xml/network_security_config"Ship a release build, hit any http:// URL, and watch it fail fast—exactly what you want.

iOS — Tighten App Transport Security

iOS’s ATS already blocks cleartext. Make sure no earlier testing flag sneaks into production.ios/Runner/Info.plist snippet

<key>NSAppTransportSecurity</key>
<dict>
  <!-- Disallow arbitrary HTTP -->
  <key>NSAllowsArbitraryLoads</key><false/>
  <!-- DEBUG ONLY: localhost exception -->
  <key>NSExceptionDomains</key>
  <dict>
    <key>localhost</key>
    <dict>
      <key>NSExceptionAllowsInsecureHTTPLoads</key><true/>
      <key>NSIncludesSubdomains</key><true/>
    </dict>
  </dict>
</dict>

Server — Tell Browsers “You’re TLS‑Only”

Preloading HSTS ensures even first-time users never hit your API over HTTP. Add the HTTP Strict Transport Security header so any compliant client refuses to downgrade.Nginx example

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
  • max-age: one year in seconds

Testing, Monitoring & Resilience

Building defenses is only half the game; now we prove they work and keep working.

Automated CI Scans

  • Spin up OWASP ZAP or Burp Scanner in Docker each pull‑request.

docker run --rm -t owasp/zap2docker-stable zap.sh -quickurl https://staging.api.yourdomain.com -quickout zap.html

Fail the build on any medium‑ or high‑risk finding.

  • If you own Burp Enterprise, call its REST API the same way; block merges when new TLS or mixed‑content issues appear.

MITM Simulation on Real Devices

Mitmproxy: start a local proxy, install its root cert on emulator, route traffic through 127.0.0.1:8080.Expected results:

  • Any http:// request should fail instantly (platform block).

  • A pinned https:// request should fail handshake because the proxy cert isn’t pinned.

  • Debug‑only overrides (localhost, self‑signed) should still work.

Frida script (advanced): hook dart:io’s _HttpClient and attempt to replace badCertificateCallback. Your pinning logic should continue to reject the injected cert, proving an on‑device attacker can’t bypass it without heavy lifting.

Timeouts, Retries, Fail‑Closed

Never loop forever on a broken TLS handshake; instead:

Future<http.Response> fetch(Uri u) async {
  const timeout = Duration(seconds: 8);
  for (var i = 0; i < 3; i++) {
    try {
      return await http.get(u).timeout(timeout);
    } on TimeoutException catch (_) {
      if (i == 2) rethrow;
      await Future.delayed(Duration(seconds: 2 << i));
    }
  }
  throw Exception('unreachable');
}

If you still can’t connect, surface an error like “Secure connection failed—check Wi‑Fi or VPN” and refuse to downgrade.

Production Monitoring

  • Ingest mobile TLS failures into your backend logs (status code 0 or handshake exception). A sudden spike may mean your cert expired or a captive portal is blocking traffic.

  • Monitor pin‑mismatch events separately; if they rise, someone might be MITM‑ing users or your cert rotation went sideways.

  • Set up an uptime robot that curls your API over HTTP every hour; it must receive a 301/308 redirect or a 403 block. Alert if it ever gets a 200 OK—that means someone accidentally re‑enabled plaintext.

Disaster Drills

Once per quarter flip staging to an invalid certificate and run the app end‑to‑end:

  • Does the UI show a clear, user‑friendly error?

  • Does it refrain from retrying in the clear?

  • Can you ship a hotfix pin update quickly if needed?

  • For bonus points, automate pin mismatch simulation in CI using mocked certs or a TLS-intercepting proxy.

When these drills are boring, you’ve done it right: your pipeline, runtime checks, and human processes all treat insecure communication as an outage, not a warning.Next we’ll add final hardening for compromised devices with runtime detection.

Advanced Runtime Protections

Why runtime protections matter

  • Root / jailbreak removes the OS sandbox; malware can read secure storage or inject hooks into Dart’s TLS stack.

  • Emulators invite dynamic instrumentation—think Frida scripts that patch badCertificateCallback to always return true.

  • Repackaged APKs can disable pinning, add spyware, then re‑sign the bundle and trick users into installing it.

  • Debuggers allow step‑through inspection of memory, exposing encryption keys and personal data.

One detection library will not beat every attacker, but it raises the cost so high that most move on to softer targets.

Integrating FreeRASP step by step

Add the dependency to pubspec.yaml

dependencies:
  freerasp: 

Bootstrap as early as possible—before you fetch tokens or open sockets.

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

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  _startFreeRASP();               // run before anything else
  runApp(MyApp());
}

void _startFreeRASP() {
  final config = TalsecConfig(
    androidConfig: AndroidConfig(
      packageName: 'com.example.app',
      signingCertHashes: ['<SHA256 PLAY‑STORE SIGNATURE>'],
      supportedStores: ['com.android.vending'],
    ),
    iosConfig: IOSConfig(
      bundleId: 'com.example.app',
      teamId: '<APPLE TEAM ID>',
    ),
    watcherMail: 'security@example.com',
  );

  final callbacks = TalsecCallbacks(
    onDeviceRooted:        () => _react('Root / jailbreak detected'),
    onEmulatorDetected:    () => _react('Running in emulator'),
    onDebuggerDetected:    () => _react('Debugger attached'),
    onRepackagingDetected: () => _react('App binary modified'),
    onAppIntegrity:        () => _react('Integrity checksum failed'),
  );

  Talsec.instance.start(config: config, callbacks: callbacks);
}

void _react(String reason) {
  // 1 Log a minimal event to your backend
  // 2 Wipe tokens and sensitive prefs
  // 3 Block UI or force logout
  print('Security event: $reason');
}

Choosing the right response

High‑risk flows (payments, health‑records)

  • Immediately erase secrets, close sockets, and exit or force re‑authentication.

Medium‑risk flows (chat, productivity)

  • Switch to read‑only mode, warn the user, and log the incident.

Audit trail

  • Post a lightweight event (device hash, event type, UTC timestamp) so your SOC can spot trends and decide if a user or region is under attack.

Testing your defense

  • Rooted emulator: launch the app; _react should fire and secrets must be wiped.

  • For extra protection, send anonymized metrics to your SOC or backend security dashboard for analysis.

  • Frida attach: run frida -U -f com.example.app; debugger detection should trigger.

  • Repackaged APK: decompile with Apktool, rebuild, reinstall; app should refuse to run.

  • False‑positive check: release build on clean hardware—no callbacks should trigger.

Conclusion & What’s Next

We started this journey in a bustling café, watching an attacker try to intercept our traffic as we sent our "postcard." Ten sections later, our once-vulnerable postcard has transformed into an armored van: HTTPS everywhere, strict certificate validation, pinning for sensitive use cases, secure WebSockets, airtight payload handling, platform policies that block cleartext, automated scans, hands-on MITM drills, and FreeRASP standing guard on compromised devices.

Handle App Security with a Single Solution! Check out Talsec's premium offer:

Key Takeaways:

  • Encrypt every byte in transit: No http://, no ws://. Always use https:// and wss:// for security.

  • Trust, but verify: Let Dart’s HttpClient handle certificate validation; only implement pinning when you’re ready to manage the rotation playbook.

  • Keep secrets secure: Never store sensitive information in URLs, logs, or plain preferences. Use flutter_secure_storage for safe storage.

  • Test your app like it's in production: Break the build on TLS regressions, and fail loudly on handshake errors.

With these layers of defense in place, if one layer slips, the others catch it. This defense-in-depth approach is at the heart of OWASP M5 for Flutter and Dart developers.

Up next

In Part Six, we move from securing the wire to securing the binary: Reverse Engineering & Code Protection (M6). We'll crack open a Flutter APK/IPA, show how attackers decompile Dart, inject method swizzles, and siphon hard-coded keys. Then, we’ll teach you how to harden your build so they leave empty-handed.See you there!

(free, automated)

Android:

In Dart, is used to configure SSL/TLS settings when establishing a secure connection, such as for HTTPS requests. It’s an essential part of managing certificates and enforcing security protocols.Let’s break it down step-by-step:A object stores and manages the certificates and keys used for secure communication (like HTTPS or gRPC) in your Dart server or client.It can:

Check out Talsec's premium !

If you want to simplify the pinning process, you can use the package, which reduces the amount of boilerplate code needed.

Check out Talsec's ultimate solution for secure communication between app and backend:

I have written a lot about this in But let's have a quick review here too.Avoid using insecure storage solutions like SharedPreferences for sensitive data. SharedPreferences stores data in plaintext, making it vulnerable to extraction. Instead, use the OS key-store via flutter_secure_storage for secure data storage.

Build a release IPA and run curl inside the app’s WebView or via http.get; it should error instantly.

includeSubDomains: catch everything, even

preload: after seven clean days submit to so all browsers hard‑code HTTPS for you.

Even with TLS, pinning, and platform blocks in place, everything collapses if the app runs on a rooted phone, inside an emulator, or under a live debugger. At that point an attacker can dump memory, switch off pinning at runtime, or extract tokens directly from disk. To close that last gap I add one more guardrail: on‑device tamper detection (, , and more) that shuts down sensitive flows the moment something looks wrong.

Assume hostile devices: Implement runtime checks like to halt sensitive flows at the first sign of tampering.

Let’s Encrypt
Network Security Config
SecurityContext
SecurityContext
Dynamic Certificate Pinning
http_certificate_pinning
AppiCrypt
M1 article.
http://api.yourdomain.com
cdn.yourdomain.com
hstspreload.org
RASP+
AppiCrypt
Malware Detection
Dynamic TLS Pinning
Secret Vault
M1: Improper Credential Usage
M2: Inadequate Supply Chain Security
M3: Insecure Authentication/Authorization
M4: Insufficient Input/Output Validation
root detection
jailbreak detection
hook detection
FreeRASP
Cover

Majid Hajian - Azure & AI advocate, Dart & Flutter community leader, Organizer, author

@Microsoft
@FlutterVikings
http://flutterengineering.io
https://x.com/mhadaily