mar. Nov 29th, 2022

Recently we published a blog post about a potential security issue caused by ua-parser-js, a dependent package that is used in the popular testing framework Karma, which in turn is the default choice for Kotlin/JS and Kotlin Multiplatform applications targeting JS.

In the post, we recommended that you may want to consider locking packages you depend on to a certain version. Here, we want to share with you how you can accomplish that with Kotlin/JS. We recommend everyone to add the snippets below to their Kotlin projects targeting JavaScript.

How npm dependencies are managed

Dependencies from npm are specified in terms of their package name and version (or range). When you execute a Gradle task in a Kotlin/JS project, the plugin downloads and installs dependencies that are required for this particular task (following Gradle’s principle of Task Configuration Avoidance). However, when a dependency version is specified as a range instead of a specific version, this can cause these (possibly transitive) dependencies to be updated automatically.

The package manager used by Kotlin/JS, Yarn, provides a mechanism to lock versions and consequently ensure consistency across multiple installations. It does this via an auto-generated file named yarn.lock, which stores the exact version of all installed dependencies.

For a normal JavaScript project built with Yarn, you use the Yarn CLI to add, upgrade, and remove dependencies. The package manager then takes care of updating the yarn.lock file accordingly, and uses its content during subsequent installations.

However, Kotlin/JS applications specify their dependencies in the build.gradle(.kts) file, instead of using a CLI tool. To still make use of version locking, you need to add some additional Gradle configuration for your project. Let’s see how that is done.

Persisting yarn.lock in your Kotlin/JS projects

To persist the yarn.lock file of your project, add the following Gradle configuration to make sure that all Gradle tasks use the same set of dependencies:

rootProject.plugins.withType(org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin::class.java) {
rootProject.the<org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension>().disableGranularWorkspaces()
}

Add the following snippet to ensure that after running the kotlinNpmInstall Gradle task, a copy of the auto-generated yarn.lock file is stored in the root of the project (named yarn.lock.bak).

Make sure to add the yarn.lock.bak file to your version control system.

Every time your build process calls kotlinNpmInstall, your build then uses the stored yarn.lock.bak in place of an auto-generated yarn.lock, locking the used dependencies in place:

tasks.register(« backupYarnLock ») {
dependsOn(« :kotlinNpmInstall »)

doLast {
copy {
from(« $rootDir/build/js/yarn.lock »)
rename { « yarn.lock.bak » }
into(rootDir)
}
}

inputs.file(« $rootDir/build/js/yarn.lock »).withPropertyName(« inputFile »)
outputs.file(« $rootDir/yarn.lock.bak »).withPropertyName(« outputFile »)
}

val restoreYarnLock = tasks.register(« restoreYarnLock ») {
doLast {
copy {
from(« $rootDir/yarn.lock.bak »)
rename { « yarn.lock » }
into(« $rootDir/build/js »)
}
}

inputs.file(« $rootDir/yarn.lock.bak »).withPropertyName(« inputFile »)
outputs.file(« $rootDir/build/js/yarn.lock »).withPropertyName(« outputFile »)
}

tasks.named(« kotlinNpmInstall »).configure {
dependsOn(restoreYarnLock)
}

To validate that your yarn.lock.bak is equal to the yarn.lock file generated from a fresh install, add the following validateYarnLock task to your Gradle build file:

tasks.register(« validateYarnLock ») {
dependsOn(« :kotlinNpmInstall »)

doLast {
val expected = file(« $rootDir/yarn.lock.bak »).readText()
val actual = file(« $rootDir/build/js/yarn.lock »).readText()

if (expected != actual) {
throw AssertionError(
« Generated yarn.lock differs from the one in the repository.  » +
« It can happen because someone has updated a dependency and haven’t run `./gradlew :backupYarnLock –refresh-dependencies`  » +
« afterwards. »
)
}
}

inputs.files(« $rootDir/yarn.lock.bak », « $rootDir/build/js/yarn.lock »).withPropertyName(« inputFiles »)
}

The snippets above ensure that your dependencies are version-locked. In addition to this, you can disable the execution of Yarn’s lifecycle scripts. This prevents your dependencies from executing code during their installation:

allprojects {
tasks.withType<KotlinNpmInstallTask> {
args += « –ignore-scripts »
}
}

Moving forward

We recommend everyone to add the snippets above to your Kotlin/JS projects to make sure that any dependencies you have – either directly or transitively – are version-locked.

We are also working on providing direct support for persistent yarn.lock files and fine-grained control over Yarn’s lifecycle scripts in Kotlin/JS and Kotlin Multiplatform projects – we suggest following KT-34014 for updates. Our aim is also to make sure that projects maintain compatibility with Gradle’s Task Configuration Avoidance, when the set of dependencies may differ based on the individual Gradle task.

By admin

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *