Dev Learning Lab

The labs on this site need JavaScript to run. The same content is written out below.

Learn › Security › Web attacks and defenses

Dependencies and the supply chain

How do libraries I never wrote down get into my program, and who decides their versions?

When you write one line for a library in the build file, the other libraries that library needs (its transitive dependencies) come in with it and run as part of your program. That is why a vulnerability in a library you never chose becomes your problem too. Depending on whether you wrote a version as a range or pinned it, a newly published version may come in automatically, and if you misspell a name by one letter, a completely different package may come in. The path a library takes from being built, through the repository, into your build is called the supply chain, and slipping malicious code in somewhere along that path is a supply chain attack. The library names and vulnerabilities shown here are all made up for the explanation.

Partly checked against real execution · A concept model built with made-up libraries. Which versions come in was checked against the result of actually resolving 15 combinations with Gradle 8.14.3, and the verdicts "vulnerable", "malicious", and "fake" are a made-up storyline

Concepts in this lab

Transitive dependency
A transitive dependency is a library that comes in because a library I wrote directly in the build file (a direct dependency) needs it. The build tool reads the POM that comes with each library and follows what is needed one step after another, so writing one line brings in far more libraries than that. A transitive dependency runs inside my program in exactly the same way as a direct dependency, so its vulnerability becomes a vulnerability of my program. Its name is not visible in the build file, so you have to use a command that prints the dependency tree to check what came in and by which path.
Version ranges and lock files
If you write the version of a dependency not as one fixed version but as a range, such as "the highest version among 2.x", the newest version in that range is chosen every time you build. This has the advantage of receiving fixed versions automatically, but building the same code can bring in different code on different days, and a new version that has a problem also comes in automatically. A lock file is a file that records the versions once decided, including transitive dependencies, and makes later builds download only those versions. If you manage the lock file together with the code, anyone building at any time gets the same result, and a new version comes in only when you update the lock file.
Supply chain attacks and typosquatting
A supply chain attack is an attack that does not hit a program directly but slips bad code into the libraries the program uses or into its build process. One way is to take over the account that publishes a library and upload a new version containing malicious code. Typosquatting is another way: publishing a fake package under a name that differs by one or two letters from a widely used name and waiting for someone to make a typo. The code that comes in runs with the same permissions as my program, so the damage is large. You prepare by pinning versions, checking the names and sources of dependencies, and putting a tool that checks for known vulnerabilities into the build.
CI/CD
CI (continuous integration) means merging code changes often and automatically running a build and tests on every merge to find problems early. CD (continuous delivery or continuous deployment) means using an automated process to get verified code into a deployable state or to deploy it all the way to production. Automating the build, test, and deployment work that people used to do by hand reduces mistakes and shortens the release cycle.

Try these

Running with the default settings

  1. It is the day of the first build. In build.gradle, the version of web-kit is written as 2.+. This means "the highest version among the 2.x versions" (2.0.0, 2.0.1, and 2.1.0 are all 2.x). The only web-kit in the repository right now is 2.0.0, so version 2.0.0 comes in, and json-lite 1.2.0, which web-kit needs, comes in with it.
  2. A few days later, web-kit 2.1.0 is published to the repository. (A made-up situation.) It is a version that someone who took over the publishing account uploaded with malicious code in it. This is a supply chain attack: it targets the path by which libraries come in, not your own code. In the repository, it looks like an ordinary new version.
  3. My code is built again without changing a single character: a version that matches 2.+ is looked up in the repository again. Of 2.0.0 and 2.1.0, it picks the higher one, version 2.1.0. Different code from last time has come into my program.
  4. The build succeeds. There is no error and no warning. web-kit 2.1.0, which contains malicious code, is shipped as part of my program.

Result of this run

Code

// build.gradle (my project, shop-api)
dependencies {
    implementation 'lab.demo:web-kit:2.+'
    implementation 'lab.demo:mail-kit:1.4.0'
}

// All libraries that start with lab.demo are made up for the explanation

Assumptions of this model

This is a model for showing the concept. The libraries that start with lab.demo, their versions, the vulnerabilities, the malicious version, and the fake package are all made up. They have nothing to do with real libraries or real vulnerability numbers. The framing follows the scope of A03 (Software Supply Chain Failures) in the OWASP Top 10:2025: know the versions of not only what you use directly but also the transitive dependencies, keep checking vulnerability advisories, and download from trustworthy sources. The rules that decide versions follow Gradle. For 15 combinations, which versions come in and the path by which json-lite comes in were checked against the result of actually resolving the dependencies with Gradle 8.14.3, using a repository on the PC used to write this lab that holds only the POMs of the made-up libraries (run without a network). The rules confirmed by execution: 2.+ picks the highest of the versions that start with 2., if there is a lock file the version recorded there is downloaded, if a library has several requests the highest version is chosen, and a name that does not exist becomes a "could not be found" error. It was also confirmed that if you change only the version in build.gradle while a lock file exists, an error says it does not match the lock file, so the lock file has to be updated too. Not confirmed by execution: the verdicts "vulnerable", "malicious", and "fake" are a made-up storyline, and the process by which code that came into the build actually runs is not covered. Version rules differ from tool to tool. When there are several requests, Maven picks not the highest version but the version nearest to my project (dependency mediation in the Maven documentation), and npm writes ranges such as ^1.2.0 by default and pins them with package-lock.json. Fake packages with similar names are mainly a problem in repositories that tell packages apart by name alone, and Maven Central verifies ownership of group names, so it is hard to publish a fake under the same group. Here, for the sake of the explanation, the fake is made up to be in the same group.

Previous lab: Rate limiting · Next lab: Hashing, encryption, and signatures

Concepts in this lab

What this lab shows

Settings

Code


      
    

Diagram

Run comparison

Try these