Back to lab
Snippet Android 5 min

Snippet: Detecting SSL Pinning Bypass on Android

May 19, 2026

Context

SSL Pinning ensures your app only accepts connections with specific certificates. Attackers use proxies (Burp, Charles) or Frida to circumvent this mechanism. This snippet detects bypass signals at runtime.


1. Proxy / Man-in-the-Middle Detector

Checks whether an HTTP proxy is configured on the device — a strong indicator of an interception attempt.

object ProxyDetector {

    fun isProxySet(): Boolean {
        val proxyHost = System.getProperty("http.proxyHost")
        val proxyPort = System.getProperty("http.proxyPort")
        
        if (!proxyHost.isNullOrBlank() && proxyPort != null) {
            return true
        }
        
        // Check via NetworkInfo (API 29+)
        val defaultProxy = ProxyInfo.buildDirectProxy("", 0)
        return defaultProxy.host?.isNotBlank() == true
    }
}

2. Frida Detector

Frida injects a native library (frida-agent) into the process. We detect it by checking for the presence of known files and by probing ports typically used by Frida Server.

object FridaDetector {

    fun isFridaPresent(): Boolean {
        return checkFridaFiles() || checkFridaPorts() || checkFridaThreads()
    }

    private fun checkFridaFiles(): Boolean {
        val fridaPaths = listOf(
            "/data/local/tmp/frida-server",
            "/data/local/tmp/re.frida.server",
            "/system/bin/frida-server",
        )
        return fridaPaths.any { File(it).exists() }
    }

    private fun checkFridaPorts(): Boolean {
        // Frida Server listens on port 27042 by default
        return try {
            Socket().use { socket ->
                socket.connect(InetSocketAddress("127.0.0.1", 27042), 200)
                true
            }
        } catch (e: Exception) {
            false
        }
    }

    private fun checkFridaThreads(): Boolean {
        return try {
            val maps = File("/proc/self/maps").readText()
            maps.contains("frida") || maps.contains("gum-js-loop")
        } catch (e: Exception) {
            false
        }
    }
}

3. Additional Certificate Validation in OkHttp

Beyond standard pinning, compare the fingerprint of the received certificate with the expected value:

class CertificateValidator : X509TrustManager {

    // SHA-256 fingerprint of the expected public certificate
    private val expectedFingerprint = "AA:BB:CC:DD:..."

    override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
        val serverCert = chain[0]
        val actualFingerprint = serverCert.fingerprint()
        
        if (actualFingerprint != expectedFingerprint) {
            throw CertificateException("Invalid certificate: possible MITM detected")
        }
    }

    private fun X509Certificate.fingerprint(): String {
        val digest = MessageDigest.getInstance("SHA-256")
        val bytes = digest.digest(encoded)
        return bytes.joinToString(":") { "%02X".format(it) }
    }

    override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {}
    override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()
}

4. Integration at App Startup

class App : Application() {

    override fun onCreate() {
        super.onCreate()
        
        if (BuildConfig.DEBUG.not()) {
            runSecurityChecks()
        }
    }

    private fun runSecurityChecks() {
        when {
            FridaDetector.isFridaPresent() -> {
                reportThreat("frida_detected")
                exitProcess(1)
            }
            ProxyDetector.isProxySet() -> {
                reportThreat("proxy_detected")
                // Can terminate or just alert — a product decision
            }
        }
    }

    private fun reportThreat(type: String) {
        // Send event to your security backend
        // Do not block on the main thread
        CoroutineScope(Dispatchers.IO).launch {
            securityApi.reportThreat(ThreatEvent(type = type, timestamp = System.currentTimeMillis()))
        }
    }
}

Considerations

What this snippet does NOT cover:

  • Bypass via framework patches (Xposed, LSPosed) — requires additional detection
  • Advanced rooting with Magisk Delta (DenyList) — use the Play Integrity API
  • Emulators — add an isEmulator() check if needed

Recommendation: Combine this detector with the Play Integrity API (MEETS_DEVICE_INTEGRITY level or higher) for full coverage of compromised environments.


References

Snippet: Detecting SSL Pinning Bypass on Android — APCosta Lab — APCosta