Safeguard
Supply Chain Attacks

Gradle build script injection and plugin supply chain att...

How attackers hijack Gradle plugins, typosquat the Plugin Portal, and inject code into build.gradle files to run with full CI trust — and how to stop it.

Aman Khan
AppSec Engineer
8 min read

Every Android app, every Spring Boot service, and most modern Java or Kotlin backends are compiled with Gradle — and every one of those builds starts by executing arbitrary code before a single line of application logic is touched. That's not a bug; it's how Gradle's Groovy and Kotlin DSL are designed to work. A build.gradle file isn't a passive manifest like a lockfile — it's a script, and scripts execute. That single design choice is what makes a Gradle supply chain attack so dangerous: attackers don't need to break your application, they just need to get one line into your build definition or one dependency into your plugin graph, and the CI runner does the rest, with the same credentials, network access, and file permissions your build already has.

This post breaks down how Gradle plugin injection actually happens, what a malicious Gradle plugin looks like once it's running, why build.gradle security is so easy to overlook, and what to actually do about it.

What is a Gradle supply chain attack?

A Gradle supply chain attack is any technique where an adversary gets malicious code executed through your build process rather than your shipped application code — typically by compromising a plugin, a dependency, the Gradle Wrapper, or the build script itself. Unlike a vulnerability in your product, the payload runs during ./gradlew build on a developer laptop or CI runner, often with elevated privileges: access to signing keys, artifact repository credentials, cloud IAM tokens injected as environment variables, and the source tree itself. Because the code executes at build time, it's invisible to runtime application security tools like WAFs or RASP, and it frequently never appears in the compiled artifact at all — making forensic recovery after the fact extremely difficult.

Gradle is a uniquely attractive target because of scale: the Gradle Plugin Portal hosts more than 24,000 published plugins, and the vast majority of Android and JVM backend projects pull in a dozen or more of them (Kotlin, Spring Boot, Shadow, Detekt, Jacoco, Protobuf, and internal org-specific plugins) without ever reading their source. Each of those plugins runs with full build-time trust.

How does Gradle plugin injection actually work?

Gradle plugin injection works by inserting or substituting code inside the buildscript {} or plugins {} block so that Gradle downloads and executes attacker-controlled logic during the configuration phase — before any tests or compilation even begin. There are three common vectors:

  1. Typosquatting the Plugin Portal. An attacker publishes com.gitub.actions or org.springframwork.boot and waits for a fat-fingered id declaration in someone's plugins {} block.
  2. Dependency confusion. If an internal plugin is resolved by name only (e.g., id("internal-lint-plugin") version "1.2.0") without pinning a private repository, Gradle's default repository resolution order can pull a same-named, higher-version artifact from a public repo instead — a class of attack Alex Birsan demonstrated at scale across dozens of companies in 2021, and one that applies just as cleanly to Gradle and Maven coordinates as it does to npm and PyPI packages.
  3. Compromised maintainer accounts or CI tokens. When a legitimate, widely-used plugin's publishing credentials are stolen, a malicious version can be pushed under a trusted groupId, and every project using a floating version range (+, latest.release) picks it up on the very next build with zero code review.

Because plugin resolution happens during Gradle's configuration phase, injected code runs on every single build invocation — including local ones — long before your CI pipeline's security scanning stage would ever see a compiled artifact.

What does a malicious Gradle plugin look like in the wild?

A malicious Gradle plugin usually looks almost identical to a normal one, because the actual payload is a handful of lines hidden inside an afterEvaluate {} or doLast {} block deep in an otherwise-functional plugin. Researchers analyzing typosquatted and hijacked build tooling have repeatedly found the same pattern: the plugin does its advertised job (say, formatting Kotlin code or generating a changelog) so nothing looks broken, while a secondary task silently exfiltrates environment variables — AWS_SECRET_ACCESS_KEY, GITHUB_TOKEN, GRADLE_PUBLISH_KEY, npm and Maven publishing tokens — to an attacker-controlled endpoint disguised as a telemetry or "usage analytics" call.

Other observed techniques include:

  • Registering a custom Gradle task with finalizedBy so the malicious step runs even if the build itself fails, maximizing the chance credentials are still harvested.
  • Modifying the Gradle Wrapper JAR (gradle-wrapper.jar) checked into a repository, so that simply cloning a project and running ./gradlew — something most engineers do without a second thought — executes attacker code before Gradle proper is even downloaded. This is precisely why Gradle added wrapper checksum verification (gradle/verification-metadata.xml) as a mitigation, but it's opt-in and rarely enforced.
  • Reaching into ~/.gradle/init.d/ init scripts, which apply globally to every project a developer builds on that machine, turning one compromised repo into a persistent, machine-wide backdoor.

Why is build.gradle security so hard to enforce?

Build.gradle security is hard to enforce because Gradle was designed for developer productivity and flexibility, not for treating the build script as untrusted, executable input — which is exactly what it is once a third-party plugin or dependency is involved. A few structural reasons this keeps getting overlooked:

  • Turing-complete build files. Groovy and Kotlin DSL support arbitrary logic, network calls, file I/O, and shell execution (exec {}) directly inside the build script. There is no sandbox by default.
  • Transitive plugin trust. Applying one plugin can pull in its own plugin dependencies, and few teams audit that graph the way they'd audit application dependencies.
  • No default lockfile enforcement. Gradle supports dependency locking and verification-metadata.xml for checksum/signature pinning, but both are optional, and most projects generated from default templates ship without them.
  • CI trust boundaries are wide. A build.gradle change is often reviewed with less scrutiny than application code in pull requests — reviewers look at logic diffs, not at a new classpath line in buildscript {}, which is exactly where an attacker would hide a malicious dependency bump.
  • Version ranges and dynamic resolution. Declarations like implementation("com.some.plugin:core:+") mean the exact code executing in your build changes without a corresponding commit in your own repository — a moving target for any point-in-time security review.

What real-world incidents have already happened?

Real-world incidents show this isn't theoretical: build-tool and CI-adjacent supply chain compromises have repeatedly demonstrated that build-time execution is a preferred attacker path, not an edge case. The 2021 dependency confusion research disclosed by Alex Birsan showed internal package names — including Java/Gradle artifact coordinates — could be preemptively squatted on public registries and pulled automatically into over 35 major organizations' builds, several of which used Gradle or Maven internally. Separately, the 2021 Codecov Bash Uploader compromise showed how a single script embedded in build and CI pipelines (conceptually identical to a Gradle init script or plugin task) can silently exfiltrate CI environment variables across thousands of downstream customers for months before detection. And the ongoing pattern of typosquatted packages on the Gradle Plugin Portal and Maven Central — flagged periodically by security researchers scanning for lookalike groupId/artifactId pairs — confirms that this is an actively probed attack surface, not a hypothetical one. The common thread across all of them: the compromise happened in build tooling, ran with CI-level trust, and was discovered weeks or months after initial exposure, not at commit time.

How Safeguard Helps

Safeguard treats your build pipeline the same way it treats production infrastructure: as an execution environment that needs continuous integrity verification, not a one-time checklist. For Gradle specifically, Safeguard:

  • Maps your full plugin and dependency graph, including transitive plugin dependencies pulled in via buildscript {} and plugins {} blocks, so typosquats, unexpected version jumps, and newly introduced classpath entries are flagged before they reach a build runner.
  • Detects dependency confusion exposure by identifying internally-named packages and plugins that resolve from public repositories instead of your private registry, and enforces repository-scoping so internal names can't be shadowed.
  • Verifies Gradle Wrapper integrity against known-good checksums on every clone and pull, catching a tampered gradle-wrapper.jar before ./gradlew ever executes it.
  • Continuously monitors published plugin behavior for known malicious patterns — unexpected outbound network calls during the configuration phase, environment variable access outside declared task inputs, and finalizedBy/afterEvaluate chains that execute regardless of build success.
  • Enforces build.gradle security policy in CI, requiring dependency locking, signature verification, and pinned (non-floating) plugin versions before a pipeline is allowed to publish or deploy.

The goal isn't to slow builds down — it's to make sure that when ./gradlew build runs, every line of code executing on that machine is something your team actually intended to run, not something an attacker injected three dependencies upstream.

Never miss an update

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