MediumReact Native App Security (Part 6): Preventing MITM Attacks with SSL Pinning | by Rohan Kumar Singh | Medium
Sitemap
Rohan Kumar Singh

Rohan Kumar Singh

Senior Software Engineer at TO THE NEW | OTT Specialist | Conviva Video Analytics Expert | Banking App Development

React Native App Security (Part 6): Preventing MITM Attacks with SSL Pinning

7 min readJul 3, 2026

--

Press enter or click to view image in full size

The Gap Between the App and the Server

In Part 4, App Check ensured only your real app can reach your backend. In Part 5, we encrypted what the app stores locally. Both of those layers are on the device.

There’s a third surface neither of them covers: the network in between.

When your app sends a request, it travels through the user’s Wi-Fi router, their ISP, potentially several CDN nodes, before reaching your server. At any point along that path, an attacker with the right position can intercept the traffic — not by breaking your app, not by reading your files, but by sitting between your app and your server and quietly reading everything that passes through.

This is a Man-in-the-Middle attack. And the standard TLS your app uses by default is not enough to stop the most practical version of it.

We’ll look at:

  • Why TLS alone doesn’t protect against all MITM scenarios
  • What certificate pinning and public key pinning actually do differently
  • How to implement SSL pinning in React Native using react-native-ssl-public-key-pinning
  • How to extract your pin hash without getting it wrong
  • Backup pins and certificate rotation
  • The debugging gotcha that trips up almost everyone

Why TLS Alone Isn’t Enough

TLS does two things: it encrypts the connection, and it verifies the server’s identity using a certificate signed by a trusted Certificate Authority (CA). Your device ships with a list of trusted CAs baked in — hundreds of them — and any certificate signed by any of those CAs will be accepted as valid.

That’s the problem. An attacker doesn’t need to steal your certificate. They just need to get a certificate signed by any trusted CA, or install a rogue CA on the device. Both are real attacks:

  • Proxy tools like Charles Proxy and Proxyman install their own CA on your device during setup. Once that CA is trusted, they can issue certificates for your domain and intercept all HTTPS traffic transparently. Developers do this intentionally for debugging — but attackers can do it on a victim’s device too.
  • Compromised or rogue CAs have been caught issuing fraudulent certificates for domains they shouldn’t be able to certify. The CA system has a history of trust failures.

TLS verifies the certificate is signed by someone on a list of hundreds of trusted entities. SSL pinning narrows that to: signed by this specific certificate or this specific public key.

Certificate Pinning vs Public Key Pinning

These are often used interchangeably but they’re meaningfully different:

Certificate pinning pins the full certificate. It’s straightforward to implement but brittle — when your certificate expires and you rotate it, your pin is wrong and all pinned clients break until they update.

Public key pinning pins the public key inside the certificate, not the certificate itself. When you rotate your certificate (new expiry, new issuer), as long as you reuse the same key pair, the pin is still valid. This is the approach worth using in production.

What MITM Looks Like, With and Without Pinning

Press enter or click to view image in full size

Implementation with react-native-ssl-public-key-pinning

The most straightforward library for React Native is react-native-ssl-public-key-pinning — it uses OkHttp on Android and custom URLSession handling on iOS, and it intercepts at the native level before any JS sees the response.

npm install react-native-ssl-public-key-pinning
cd ios && pod install

Initialise it once at app startup, before any network calls go out:

import { initializeSslPinning } from 'react-native-ssl-public-key-pinning';

await initializeSslPinning({
'api.yourapp.com': {
includeSubdomains: true,
publicKeyHashes: [
'your-primary-pin-base64-sha256=',
'your-backup-pin-base64-sha256=',
],
},
});

A few things to get right here:

  • Two hashes, minimum. The primary hash is your current certificate’s public key. The backup hash is your next key pair — the one you’ll rotate to. Without a backup, a certificate rotation breaks all pinned clients until they update.
  • includeSubdomains: true extends pinning to all subdomains of the domain you specify. If your app talks to api.yourapp.com and cdn.yourapp.com, one entry covers both.
  • Call this before your Axios instance, fetch client, or any HTTP abstraction initializes — otherwise early requests go out unpinned.

Extracting Your Pin Hash Correctly

The pin hash is the SHA-256 fingerprint of the SubjectPublicKeyInfo (SPKI) from your certificate — not the full certificate hash. Getting the wrong value here means pinning silently fails or breaks all connections.

Get Rohan Kumar Singh’s stories in your inbox

Join Medium for free to get updates from this writer.

From a .pem certificate file:

openssl x509 -in certificate.pem -pubkey -noout |
openssl pkey -pubin -outform DER |
openssl dgst -sha256 -binary |
base64

From a live server (the most reliable approach since it hits exactly what your app will see):

openssl s_client -connect api.yourapp.com:443 -servername api.yourapp.com < /dev/null 2>/dev/null |
openssl x509 -pubkey -noout |
openssl pkey -pubin -outform DER |
openssl dgst -sha256 -binary |
base64

The output is your pin hash. Add the = padding at the end if the base64 output is missing it. Copy it exactly — a single character mismatch means every request from a pinned build fails with an SSL error.

Always verify the hash you extracted matches what you put in the config before shipping a pinned build. The fastest way to do that is a simple test against your staging API before merging.

Certificate Rotation Without Breaking Clients

Certificate rotation without a backup pin is one of the most common self-inflicted production incidents in mobile security. The sequence:

Press enter or click to view image in full size

The safe rotation sequence:

  1. At deploy time, add your next key pair’s hash as a backup pin alongside your current primary.
  2. Ship the update — all clients now have both pins.
  3. When the current certificate expires, rotate to the new key pair. All pinned clients already have its hash as a backup, so nothing breaks.
  4. Ship another update promoting the new hash to primary and adding the next-next backup.

The key constraint: rotate your certificate, but reuse or pre-register your public key. Public key pinning makes this tractable in a way certificate pinning doesn’t.

The Debugging Gotcha

Once SSL pinning is active, tools like Charles Proxy, Proxyman, and Flipper’s network inspector stop working. Their certificates don’t match your pin, and every intercepted request fails with an SSL error.

Handle this explicitly based on environment:

import { initializeSslPinning } from 'react-native-ssl-public-key-pinning';

const setupNetworkSecurity = async () => {
if (__DEV__) {
console.warn('[Security] SSL pinning disabled in dev build');
return;
}

await initializeSslPinning({
'api.yourapp.com': {
includeSubdomains: true,
publicKeyHashes: [
process.env.SSL_PIN_PRIMARY,
process.env.SSL_PIN_BACKUP,
],
},
});
};

Two things here:

  • Pinning is disabled entirely in __DEV__ builds, so your debugging tools work during development.
  • The pin hashes come from environment variables, not hardcoded strings. This keeps the hashes out of version control and means you can rotate them by updating your CI/CD environment without touching source code.

Never disable pinning based on anything other than the build type. An attacker-controlled device that fakes a “debug” condition is a real attack vector if you use runtime flags instead.

What Pinning Can’t Do

SSL pinning stops network interception between your app and your server. It doesn’t protect against:

  • Compromise on the device itself. If a Frida hook intercepts the decrypted data after your networking layer has already unpinned and processed it, pinning never saw the attack. This is why freeRASP (Part 3) and pinning are complementary, not alternatives.
  • Server-side breaches. Once the request reaches your backend, pinning is done. Data security from that point is the server’s responsibility.
  • Pinning bypass scripts. Frida and Objection have modules specifically for unpinning pinned apps. On a rooted/jailbroken device, these bypass your pinning implementation entirely. This is why detecting root/jailbreak (Part 2) and runtime hooks (Part 3) are prerequisite layers, not optional extras.

No single layer makes the others unnecessary. The whole point of the series is that these layers catch different things.

What’s Next

In Part 7, we’ll move up the stack to fraud detection — transaction monitoring, behavioural signals, and the infrastructure-level patterns that separate a legitimate high-value transaction from an automated attack, in apps where both look identical at the request level.

Final Thoughts

TLS tells your app that someone with a valid certificate is on the other end. SSL pinning tells your app that your server specifically is on the other end. For an app that handles authentication, payments, or sensitive user data, the difference between those two guarantees matters.

Pinning is also one of the easiest security layers to ship incorrectly — wrong hash format, missing backup pin, forgetting to disable it in dev builds. The implementation is simple once you understand what you’re pinning and why. Getting those details right is the whole job.

--

--

Written by Rohan Kumar Singh

Senior Software Engineer at TO THE NEW | OTT Specialist | Conviva Video Analytics Expert | Banking App Development