Open Source Security

Kotlin Multiplatform Supply Chain Risks

Kotlin Multiplatform ships one codebase to JVM, iOS, Android, JS, and native targets. The supply chain surface expands in specific ways worth tracking.

Shadab Khan
Security Engineer
6 min read

Kotlin Multiplatform (KMP) went stable for multiplatform libraries in November 2023 and reached Kotlin 2.0 in May 2024. The appeal is obvious: write shared business logic once, ship it to Android, iOS, server JVM, JavaScript, and native desktop targets. What is less obvious is that each target is a distinct supply chain surface. A KMP project that ships to five platforms is pulling dependencies from five ecosystems — Maven Central for JVM, CocoaPods or SPM for iOS, npm for JavaScript, Gradle plus Kotlin/Native toolchain for native. The supply chain math is additive in ways that take teams by surprise.

The shared source tree hides the per-target dependency set

A KMP project's shared module declares common dependencies, but the actually-resolved set differs per target. kotlinx.coroutines on JVM pulls from Maven Central. The same declaration on iOS pulls a different artifact via Kotlin/Native. The same on JS resolves against npm-adjacent registries.

The implication: when a supply chain incident hits one of these registries, only some of your targets are affected. An npm incident affects the JS target but not Android or iOS. A Maven Central incident affects the Android and server JVM targets. An audit that only checks Maven Central misses the other halves.

A defensible KMP SBOM enumerates all target-specific dependency graphs, not just the common one.

Expect/actual declarations are a trust boundary

expect/actual is the KMP mechanism for declaring a platform-specific implementation of a common API. A common module declares expect fun encrypt(data: ByteArray): ByteArray and each target provides a concrete actual. The concrete implementations can be radically different — Android uses JCA, iOS uses CommonCrypto, JS uses Web Crypto API.

From a supply chain perspective, the actual side is where platform-specific dependencies enter the picture, and where platform-specific vulnerabilities manifest. A library published with a good JVM actual and a weak JS actual is a mixed supply chain risk that pure-JVM auditing will miss.

Audit practice: for any KMP library consumed, check what platforms it targets and review each actual implementation.

CocoaPods integration pulls native iOS dependencies

KMP projects targeting iOS typically integrate with CocoaPods to pull in native Swift/Objective-C libraries. The iOS target then has two dependency worlds: Kotlin-side dependencies resolved via Gradle, and Objective-C dependencies resolved via Podfile.

Both need to be in the SBOM. The Podfile typically lives in iosApp/ or a similar subdirectory and is easy to miss if the CI pipeline only runs Gradle. A CycloneDX SBOM generator that only introspects Gradle will emit an incomplete picture for KMP-iOS.

Recent KMP versions also support Swift Package Manager in addition to CocoaPods. The same audit issue applies — SPM dependencies are separately resolved and need separate tracking.

Kotlin/Native toolchain is a large trusted dependency

Kotlin/Native compiles to native binaries using a toolchain that pulls in LLVM, platform-specific libraries, and some amount of glue code. The toolchain itself is downloaded during the first build and cached. Versions of Kotlin/Native have specific toolchain versions pinned.

The supply chain implication: the Kotlin/Native toolchain is part of the trusted compute base. A compromised toolchain distribution would affect every Kotlin/Native build. Kotlin 2.0 tightened the toolchain distribution signing, which is net positive for posture, but the attack surface still exists.

Pin the Kotlin version in gradle.properties. Track toolchain-distribution hashes if your build reproducibility posture requires it.

JS target security is often overlooked

The KMP JavaScript target outputs JavaScript that runs in browser or Node environments. KMP-JS consumers pull dependencies from npm (for interop libraries) and from Kotlin-specific distribution points. Both should be in scope for SCA scans.

In practice, teams that adopt KMP for the JVM and iOS targets often forget the JS target in audit scope. If your KMP library publishes a JS distribution, the security posture of that distribution is your concern even if the team mostly thinks of KMP as mobile-plus-server.

Build-time code generation in KMP is privileged

Like every modern Kotlin/Java ecosystem, KMP uses annotation processors, Kotlin symbol processing (KSP), and Gradle plugins that run at build time with developer privileges. KMP-specific generators (e.g., for expect/actual scaffolding) expand the set. Every such generator is a compile-time code execution point and should be audited with the same rigor as any build-time dependency.

KSP processors in particular are less widely reviewed than their Java equivalents and are underweighted in most audit processes.

Gradle version catalogs are the recommendation

For KMP projects, Gradle version catalogs (introduced 7.4, GA 8.0) provide a central place to declare versions across all target-specific source sets. This mirrors the benefit of .NET's Central Package Management — single source of truth, easier audit, simpler policy enforcement.

A libs.versions.toml:

[versions]
kotlin = "2.0.20"
coroutines = "1.9.0"
ktor = "3.0.0"

[libraries]
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }

Referenced from Gradle Kotlin DSL:

sourceSets {
    commonMain.dependencies {
        implementation(libs.coroutines.core)
        implementation(libs.ktor.client.core)
    }
}

This is the baseline; KMP projects not using it have worse auditability than those that do.

CI pipelines for KMP should run per-target

A JVM-only CI that runs tests and audits on the JVM target gives you a partial posture. A KMP-correct CI runs the test suite for each target on an appropriate runner — JVM tests on Linux/macOS, iOS tests on macOS, JS tests on Node, native tests on each supported native target. Supply chain audits should run in each context.

Concrete: add matrix jobs for each target. The cost is modest. The blind spot without them is meaningful.

How Safeguard Helps

Safeguard's KMP support enumerates per-target dependency graphs (JVM-Maven, iOS-CocoaPods, iOS-SPM, JS-npm, native toolchain) into a single unified SBOM and tracks vulnerabilities per target. Griffin AI correlates expect/actual implementations and flags mismatches in dependency trust — for example, a library whose JVM actual is well-vetted but whose JS actual pulls from a sparsely-audited npm package. Policy gates can enforce per-target requirements (JS target must not consume unsigned npm packages, for instance) without requiring separate scanning pipelines per target. For KMP teams shipping real multiplatform code, Safeguard reduces the per-target audit fan-out to a single supply chain view.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.