Third-Party Package Context Code Execution

ID

kotlin.android_third_party_package_context_code_execution

Severity

high

Resource

Injection

Language

Kotlin

Tags

CWE:470, CWE:829, CWE:94, MASWE-0085, OWASP:2021:A03, PCI-DSS:6.5.1, android, code-injection, dynamic-loading

Description

Loading and executing code from third-party packages or external sources without proper verification allows attackers to inject arbitrary code that executes with the app’s permissions and security context. Research shows at least 1 in 50 popular Android apps are vulnerable to these attacks.

Rationale

Code injection through third-party packages enables:

  • Arbitrary code execution with app’s permissions

  • Data theft from app’s private storage

  • Account compromise and credential theft

  • Malware installation and propagation

  • Bypass of app security controls

Two main attack vectors exploit unsafe code loading:

  • Package Squatting: Attackers claim unclaimed package names that apps expect to exist

  • Code Injection: Attackers tamper with DEX/APK files loaded from untrusted sources

Using package names or file paths from external input to select code creates unsafe reflection vulnerabilities.

This detector identifies unsafe code loading patterns, such as:

createPackageContext with Dangerous Flags

Using CONTEXT_INCLUDE_CODE plus CONTEXT_IGNORE_SECURITY flags in createPackageContext without verification, and using class loading with that package context allows the execution of potentially malicious code.

// VULNERABLE: Package squatting attack
fun loadPlugin(packageName: String) {
    // Loose package name matching
    if (packageName.startsWith("com.myapp.plugin.")) {
        // FLAW: Dangerous flags without signature verification
        val flags = Context.CONTEXT_INCLUDE_CODE or
                    Context.CONTEXT_IGNORE_SECURITY
        val context = createPackageContext(packageName, flags)
        val classLoader = context.classLoader// FLAW

        // Execute untrusted code
        val pluginClass = classLoader.loadClass("$packageName.PluginMain")
        pluginClass.newInstance()
    }
}

An attacker can create a malicious app com.myapp.plugin.malicious that executes in victim’s context.

DexClassLoader from External Storage

Loading code from world-readable/writable storage:

// VULNERABLE: External storage
fun loadDynamicCode() {
    // FLAW: Loading from external storage (attackers can replace file)
    val dexPath = File(Environment.getExternalStorageDirectory(), "plugin.apk")
        .absolutePath
    val optimizedDir = getDir("dex", 0).absolutePath

    // FLAW: No integrity verification
    val classLoader = DexClassLoader(dexPath, optimizedDir, null, javaClass.classLoader)
    val clazz = classLoader.loadClass("com.plugin.Main")
}

PathClassLoader from Network

Downloading and executing code without verification:

// VULNERABLE: Network download without verification
suspend fun loadFromNetwork() {
    // FLAW: HTTP download (MITM attack possible)
    val url = "http://example.com/plugin.dex"
    val dexBytes = downloadFile(url)

    // FLAW: No signature or hash verification
    val classLoader = InMemoryDexClassLoader(
        ByteBuffer.wrap(dexBytes),
        javaClass.classLoader
    )
}

Remediation

  1. Verify Package Signatures

    Always verify package signature before loading code:

    // SECURE: Signature verification
    fun loadPlugin(packageName: String) {
        // Exact package name (not pattern matching)
        if (packageName == "com.myapp.plugin.official") {
    
            // Verify signature matches
            val pm = packageManager
            if (pm.checkSignatures(packageName, this.packageName) ==
                PackageManager.SIGNATURE_MATCH) {
    
                // Use CONTEXT_RESTRICTED (safer) or verify before CONTEXT_INCLUDE_CODE
                val context = createPackageContext(packageName, Context.CONTEXT_RESTRICTED)
    
                // Safe to use classLoader after verification
                val classLoader = context.classLoader
                val pluginClass = classLoader.loadClass("$packageName.PluginMain")
            } else {
                throw SecurityException("Package signature mismatch")
            }
        }
    }
  2. Use Internal Storage Only

    Load code from protected internal storage:

    // SECURE: Internal storage with verification
    fun loadDynamicCode() {
        // Load from internal storage (protected by app sandbox)
        val dexFile = File(filesDir, "plugin.apk")
    
        if (!dexFile.exists()) {
            return
        }
    
        // Verify file integrity
        if (!verifyFileHash(dexFile, EXPECTED_HASH)) {
            throw SecurityException("Plugin integrity check failed")
        }
    
        val optimizedDir = getDir("dex", 0).absolutePath
        val classLoader = DexClassLoader(
            dexFile.absolutePath,
            optimizedDir,
            null,
            javaClass.classLoader
        )
    }
    
    private fun verifyFileHash(file: File, expectedHash: String): Boolean {
        val digest = MessageDigest.getInstance("SHA-256")
        val actualHash = digest.digest(file.readBytes())
            .joinToString("") { "%02x".format(it) }
        return actualHash == expectedHash
    }
  3. Implement Digital Signature Verification

    // SECURE: Verify code signature
    private fun verifyCodeSignature(apkFile: File): Boolean {
        val packageInfo = packageManager.getPackageArchiveInfo(
            apkFile.absolutePath,
            PackageManager.GET_SIGNATURES
        )
    
        packageInfo?.signatures?.forEach { signature ->
            val md = MessageDigest.getInstance("SHA-256")
            val certHash = md.digest(signature.toByteArray())
            if (certHash.contentEquals(EXPECTED_CERT_HASH)) {
                return true
            }
        }
        return false
    }
  4. Use HTTPS with Certificate Pinning

    If loading from network (discouraged by Google Play):

    // SECURE: HTTPS with verification
    suspend fun loadFromNetworkSecurely() {
        // HTTPS with certificate pinning
        val url = "https://secure.example.com/plugin.dex"
        val dexBytes = downloadWithCertificatePinning(url)
    
        // Verify digital signature
        if (!verifyDexSignature(dexBytes)) {
            throw SecurityException("DEX signature verification failed")
        }
    
        // Save to internal storage as read-only (Android 14+ requirement)
        val tempFile = File(filesDir, "temp_plugin.dex")
        tempFile.writeBytes(dexBytes)
        tempFile.setReadOnly()
    
        val classLoader = DexClassLoader(
            tempFile.absolutePath,
            getDir("dex", 0).absolutePath,
            null,
            javaClass.classLoader
        )
    }
  5. Prefer Static Alternatives

    The best option is to avoid dynamic code loading entirely: Use Android App Bundles for modular features, include all code directly in APK, and use Play Feature Delivery for on-demand modules.

References