When building a fintech application in Nigeria, choosing Kotlin over Java is an excellent engineering decision. It eliminates entire classes of NullPointerExceptions and reduces boilerplate code. However, from an offensive security perspective, Kotlin and Java compile to the exact same Dalvik Executable (DEX) bytecode format.

If your architecture is flawed, if your secret management is careless, or if your local storage policies are weak, writing the app in Kotlin provides zero defensive advantage. Attackers do not care what language you wrote the app in. They care about what the compiled binary exposes.

At Simpa Labs, we specialize in ripping apart Android applications. We reverse engineer the bytecode, we map the execution flow, and we extract the secrets that developers thought were securely compiled. Here are the top five vulnerabilities we consistently find in Kotlin-based Android applications, and exactly how you must fix them.

1. Hardcoded secrets in Kotlin source and compiled bytecode

There is a persistent myth among junior Android developers that compiling code into an APK file hides the variables. This is unequivocally false. Kotlin code compiles to DEX bytecode inside your APK. Advanced de-obfuscation pipelines can decompile that bytecode back to something incredibly close to your original Kotlin source.

Every single string constant in your Kotlin code—including AWS API keys, Paystack secret tokens, database credentials, and internal staging endpoint URLs—is present in your compiled APK in plaintext. Any attacker who downloads your app from the Google Play Store can extract these strings in seconds.

When a fintech hardcodes a third-party secret key (like an SMS gateway token or a cloud storage key), they hand the keys to their kingdom directly to anyone with a rudimentary understanding of reverse engineering.

// VULNERABLE: hardcoded secrets in Kotlin constants
object Config {
    const val API_KEY = "sk_live_abc123def456"       // Extractable from APK
    const val PAYSTACK_SECRET = "sk_live_xyz789"      // Do not do this
    const val BASE_URL = "https://api-prod.yourapp.com" // Reveals your production API
}

// CORRECT: use BuildConfig variables set from CI/CD environment
// and rotate secrets that end up in any released build immediately
object Config {
    val API_KEY: String = BuildConfig.API_KEY  // Still in APK, but easier to rotate
    // For truly sensitive secrets: fetch from your backend after authentication
    // Never ship payment secret keys in a mobile app binary
}

The ultimate defense against hardcoded secrets is simple: do not put them in the code. Highly sensitive material must be fetched dynamically from your backend API after a user successfully authenticates. If the app needs a key to start up, it should be an anonymous, heavily rate-limited public key with zero administrative privileges.

2. Insecure local storage

Android provides several mechanisms for storing data locally. The most commonly abused is SharedPreferences. SharedPreferences stores data as XML files in the app's internal data directory, typically located at /data/data/com.yourapp/shared_prefs/.

While this directory is isolated from other apps on a non-rooted device, it is completely unprotected on a rooted device. We frequently find highly sensitive authentication JWTs, user session data, and personally identifiable information (PII) stored in SharedPreferences in plaintext.

// VULNERABLE: storing auth token in plaintext SharedPreferences
val sharedPrefs = getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
sharedPrefs.edit().putString("auth_token", token).apply()
// This creates: /data/data/com.yourapp/shared_prefs/app_prefs.xml
// Readable in plaintext on a rooted device

// CORRECT: use EncryptedSharedPreferences (backed by Android Keystore)
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey

val masterKey = MasterKey.Builder(context)
    .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
    .build()

val encryptedPrefs = EncryptedSharedPreferences.create(
    context,
    "secure_prefs",
    masterKey,
    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
encryptedPrefs.edit().putString("auth_token", token).apply()

By using EncryptedSharedPreferences, the actual data written to the XML file is encrypted using an AES-256 key. Crucially, the master encryption key is generated and stored securely inside the Android Keystore. The Keystore is backed by the device's secure hardware (like the Trusted Execution Environment or a Secure Element). Even if an attacker gains root access and steals the encrypted XML file, they cannot extract the master key from the hardware to decrypt it.

Building a Kotlin Android app that handles money or personal data? We test it at the code level.

Book an Android Security Test

3. Weak or bypassable root detection

Root detection attempts to prevent your application from executing on a compromised (rooted) device. This is a common requirement for banking and fintech apps because a rooted device allows an attacker to use advanced dynamic instrumentation frameworks, inspect raw memory, and bypass SSL certificate pinning.

However, most Kotlin root detection implementations we encounter are woefully inadequate. They rely on simple strings or file checks that are easily bypassed by determined attackers using sophisticated hooking engines.

// Common weak root detection (easily bypassed)
fun isRooted(): Boolean {
    // Check 1: file path check - bypassable via MagiskHide or kernel modules
    val suBinary = File("/system/xbin/su")
    if (suBinary.exists()) return true

    // Check 2: package check - bypassable via Zygisk
    val packages = listOf("com.topjohnwu.magisk", "eu.chainfire.supersu")
    packages.forEach { pkg ->
        try {
            packageManager.getPackageInfo(pkg, 0)
            return true
        } catch (e: PackageManager.NameNotFoundException) {}
    }

    return false
}

// Stronger approach: use multiple independent checks
// and perform them at critical points (payment initiation)
// not just at app launch. Also: treat root detection as
// a signal to increase friction, not necessarily to block.
// A determined attacker will bypass any software check.

If your application's security relies entirely on client-side root detection, you have already lost. The ultimate truth of mobile security is that the client is a hostile environment. You cannot trust the binary, you cannot trust the operating system, and you cannot trust the device. Root detection should be used as a layer of friction, but all authorization and business logic must be strictly enforced on your backend API.

4. SSL certificate pinning implementation

SSL pinning ensures that your Kotlin application only communicates with servers holding a specific, trusted cryptographic certificate. It prevents attackers from using Man-in-the-Middle (MitM) proxies to intercept and modify the HTTPS traffic flowing between the mobile app and your backend APIs.

The most common mistake we see is developers attempting to implement custom pinning logic using OkHttp's `CertificatePinner`, but failing to handle certificate rotation properly, leading to complete app outages when a certificate expires. The robust, modern Android approach is to use the Network Security Configuration (NSC) XML file.

<!-- res/xml/network_security_config.xml -->
<network-security-config>
    <domain-config cleartextTrafficPermitted="false">
        <domain includeSubdomains="true">api.yourapp.com</domain>
        <pin-set expiration="2027-01-01">
            <!-- SHA-256 of the Subject Public Key Info (SPKI) -->
            <pin digest="SHA-256">YOUR_PRIMARY_PIN_BASE64==</pin>
            <!-- Backup pin: pin to an intermediate CA or a backup key -->
            <pin digest="SHA-256">YOUR_BACKUP_PIN_BASE64==</pin>
        </pin-set>
    </domain-config>
</network-security-config>
// AndroidManifest.xml: reference the config
// <application android:networkSecurityConfig="@xml/network_security_config">

// Get your SPKI hash:
// openssl s_client -connect api.yourapp.com:443 | \
//   openssl x509 -pubkey -noout | \
//   openssl pkey -pubin -outform der | \
//   openssl dgst -sha256 -binary | \
//   base64

While attackers using advanced memory hooking frameworks can bypass SSL pinning on their own devices, implementing pinning correctly stops massive classes of network-level surveillance and protects non-rooted users on compromised public Wi-Fi networks.

5. WebView security in Kotlin apps

WebViews are frequently used in fintech apps to render complex UI elements, handle 3D Secure payment flows, or display dynamic legal agreements. However, a WebView that loads remote content with JavaScript enabled and JavaScript-to-Kotlin bridges exposed represents a critical attack surface.

If an attacker manages to inject malicious JavaScript into the webpage loaded by the WebView (e.g., via a compromised third-party script), that JavaScript can execute native Kotlin functions if the bridge is configured insecurely.

// VULNERABLE: WebView with dangerous settings
webView.settings.apply {
    javaScriptEnabled = true
    allowFileAccess = true          // Allows JS to read local files
    allowFileAccessFromFileURLs = true  // Allows cross-origin file access
}
webView.addJavascriptInterface(MyBridge(), "AndroidBridge") // Exposes Kotlin methods to JS

// CORRECT: minimal permissions WebView
webView.settings.apply {
    javaScriptEnabled = true  // Only if required
    allowFileAccess = false
    allowFileAccessFromFileURLs = false
    allowUniversalAccessFromFileURLs = false
}
// Only add JavascriptInterface if essential, and annotate
// each exposed method with @JavascriptInterface explicitly
// Never expose methods that access sensitive data or system APIs
Real finding from a Kotlin Android pentest

JWT in SharedPreferences extracted via ADB on non-rooted device

During an assessment of an Android Kotlin app for a Nigerian insurance platform, we found the JWT authentication token stored in SharedPreferences. The app did not enable the FLAG_SECURE window attribute, and the Android backup mechanism was enabled without the allowBackup="false" flag in the manifest. Using standard ADB backup on a non-jailbroken device, we extracted the entire SharedPreferences directory, which contained the live JWT token. We used that token to make authenticated API calls from a different device for 7 days until the token expired. Fix: set android:allowBackup="false" in the manifest and move the token to EncryptedSharedPreferences backed by the hardware Keystore.

Find the flaws in your Kotlin source code before the hackers do.

Scope Your Android Pentest

Frequently asked questions

Does Kotlin code compile to something an attacker can read?

Kotlin compiles to JVM bytecode, which is highly reversible. Proprietary decompilation pipelines and NSA-grade reverse engineering suites can decompile Kotlin bytecode back to readable code that is virtually identical to your original source. Hardcoded strings, API endpoints, and business logic patterns are fully recoverable. Commercial-grade obfuscation makes decompilation harder to read but does not prevent logic mapping.

How do attackers bypass Kotlin root detection?

Most Kotlin root detection implementations check for specific file paths (/su/bin, /system/xbin/su), installed packages (Magisk, SuperSU), or the result of running 'su' as a command. Advanced dynamic instrumentation frameworks can hook these specific check functions at runtime in the device's memory, forcing them to return 'false' regardless of the actual device state. Kernel-level modules can also conceal root from specific app package names entirely.

What is the Android Keystore and should we use it?

Android Keystore is a hardware-backed secure storage system for cryptographic keys. Keys stored in the Keystore cannot be exported from the device, even by root processes. For storing authentication tokens and encryption keys in an Android Kotlin app, EncryptedSharedPreferences (backed by Keystore) is the correct approach. Raw SharedPreferences stores data in plaintext XML files readable by any root-level access.

Do you provide code-level fix recommendations for Kotlin?

Yes. Every finding in our penetration test report includes an engineering-ready fix recommendation with specific Kotlin code examples where applicable. We do not give abstract advice like 'improve input validation.' We show you the specific function, the specific pattern to change, and the corrected implementation.

Related reading

Blog: Kotlin native Android penetration testing guide · Reverse engineering Android fintech apps · Certificate pinning in mobile banking

Blog: Hardcoded API keys in mobile apps · SSL pinning bypass

Services: Mobile app penetration testing · Penetration testing