The app trusts any certificate signed by a CA in the device trust store. An attacker who can add a CA (a corporate proxy, a malicious profile, a compromised device) or who obtains a mis-issued certificate can read and change the app's HTTPS traffic.
At a glance
- Why it matters
- On hostile networks the attacker sees tokens, personal data and API calls, and can change responses to the app. It also makes reverse engineering of your API trivial, which speeds up finding other weaknesses such as IDOR.
- How to fix it
- Pin the public key of your API's certificate (or its issuing CA) with at least one backup pin and a rotation plan, using the platform's network security configuration where possible. Fail closed when the pin does not match, and never ship debug trust settings in release builds.
Description
Certificate pinning has the app verify not just that a server's TLS certificate is valid, but that it matches a specific expected certificate or public key, rather than accepting any certificate a trusted certificate authority has issued. Without pinning, an app trusts the device's entire certificate store, which means anything able to get a trusted (or device-installed) certificate accepted, a compromised network, a corporate proxy, or a malicious profile installed on the device, can intercept the app's HTTPS traffic without the app being able to tell the difference.
Impact
On a network the user does not fully trust, airport Wi-Fi, a compromised access point, a corporate inspection proxy with its own root certificate installed, traffic between the app and its backend can be intercepted and read, and in an active attack, modified in transit. This exposes session tokens, credentials and any sensitive data the app sends or receives, and can allow an attacker to inject or alter responses the app then trusts and acts on.
How we find it
- We intercept the app's traffic with a proxy presenting our own certificate (Burp Suite, mitmproxy or similar) and check whether the app accepts the connection; an app without pinning accepts it exactly as it would accept the real server's certificate.
- We test at the network layer as well as through a proxy, since some implementations pin correctly for one connection path (a web view, for instance) while a native network layer used elsewhere in the same app does not.
- Where pinning is present, we check its implementation for common weaknesses: pinning to a leaf certificate that expires and cannot be rotated, or a debug/development bypass that was never removed from the release build.
Fix
Implement certificate or public-key pinning for connections to your own backend, pinning to a key or a small set of keys you control (or an intermediate certificate) rather than the leaf certificate alone, so rotation does not require an app update. Ensure any debug bypass for pinning is compiled out of release builds, not merely disabled by a runtime flag.
// OkHttp: pin the connection to a known public key hash
val certificatePinner = CertificatePinner.Builder()
.add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.add("api.example.com", "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=") // backup pin
.build()
val client = OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build()
References
- OWASP Mobile Top 10 (2024): M5, Insecure Communication
- CWE-295: Improper Certificate Validation
- OWASP Mobile Application Security Testing Guide (MASTG): Network Communication