skip to content

How can a build produce a backdoored artifact when every source file is clean?

level: juniorimportance: must knowfreq 64%

answer

  1. what you reviewed is not what you shipped
  2. review covers inputs, not the output
  3. anything that executes during the build
  4. plugins, rewriters, packaging, base image
  5. clean diff, dirty binary

basics

~20 s

Because the artifact is made by the build, not by the repository. Anything that runs during the build - a build script, a plugin, a build-time dependency, the compiler image, a packaging step - can add code that appears in no source file.

solid answer

~50 s

Review, branch protection and a green pull-request check all operate on the build's **inputs**. The thing you ship is the build's **output**, and between the two sits a whole execution environment: build scripts, build plugins, annotation processors and bytecode rewriters, packaging and shading steps, dependencies that exist only at build time, and the base image the build runs in. Any of those can add, replace or rewrite code on its way into the artifact. That is build-time injection, and it is deliberately chosen by attackers precisely because the repository stays clean: the diff is honest, the tests pass, and the release looks normal. It also means the investigation has to start from the artifact rather than from `git log` - you compare what shipped against what the declared source would produce, and you enumerate everything the build was allowed to execute.

go deeper

for a junior

Be ready to say plainly that the repository is an input and the artifact is an output, and to name three things that run during a build and could change the output.

for a middle

You are expected to walk the build step by step and point at every stage with write access to the artifact - plugins, post-compile rewriting, packaging, the runner image - and explain why each check stays green.

for a senior

Show that you would start an investigation from the published bytes rather than from the repository, and that you treat the builder and every secret it could read as compromised until proven otherwise.

for a principal

Own the framing that source-side controls cannot answer an artifact-side question, and be able to explain to a customer or auditor what your organisation can actually evidence about a release beyond "we reviewed the code".

## The repository is not the artifact Almost every control an engineering organisation is proud of operates on **inputs**: pull-request review, required approvals, protected branches, signed commits, a green check on the PR. All of them answer the question "is the source we intend to build acceptable?" None of them answers "does the thing we published correspond to that source?" Build-time injection is the attack that lives in that gap. The attacker does not modify a source file, because modifying a source file is the one thing your process is built to catch. Instead they arrange for something that runs *during* the build to change what comes out. ## What actually runs during a build A build is not a pure function of the repository. It is a program - usually a long one - executing on a machine with write access to the output. The places code can enter without touching application source include: - **The build configuration itself** - build scripts are code, and a build script can rewrite files, download something and execute it, or swap an output. - **Build plugins and their transitive dependencies.** A plugin is code you run at full privilege on your own source tree. Its own dependency graph is usually nobody's job to review. - **Post-compilation steps** - annotation processors, bytecode rewriters, obfuscators, shading and relocation, minifiers, resource packers. These exist to modify compiled output; that is their normal function, which is what makes a malicious one so easy to hide. - **Build-time-only dependencies** - code generators, linters that run in-process, test infrastructure that shares the build JVM or interpreter. - **The environment** - the base image or runner the build executes on, the language toolchain installed on it, an interpreter or agent injected via an environment variable, a pre-existing file in the workspace. - **The packaging and publishing step**, which handles the finished bytes last and is often the least-reviewed part of the pipeline. A concrete shape: a build plugin in a payments SDK's build rewrites compiled classes after compilation, so the published library contains a call to an attacker-controlled endpoint. That call exists in no source file and in no reviewed diff. Every consumer that embeds the SDK now handles card data next to attacker code, and the source repository they could inspect is genuinely clean. ## Why the usual signals stay green - **Tests pass**, because tests exercise code built by the same compromised build, and because the payload is typically dormant - gated on the environment, a date, or a target it is looking for. - **The diff is small and correct**, because the attacker did not author it. - **Static analysis on the source finds nothing**, because the source is not where the code is. - **A signature verifies**, because the signing step is at the *end* of the build. Signing after injection produces a validly signed backdoored artifact. A signature says who vouches for these bytes and that nobody altered them afterwards; it never says the bytes correspond to any particular source. - **A component inventory can also look clean.** An inventory answers what is *inside* the artifact; injected code that adds no new component simply does not show up as an entry. ## The direction that matters Keep the three claims straight, because mixing them up is the canonical wrong answer in this area. An inventory of contents tells you *what is inside*. A signature tells you *who vouches for it*. Neither tells you *how it came to be* - and build-time injection is entirely a question of how it came to be. That is why the countermeasures for this class are about the build rather than about the code, and why "we reviewed the repository" is never a complete answer to "is this release safe?". ## What this changes in practice When you suspect it, you work from the artifact backwards, not from the repository forwards: get the published bytes and their digest, build the declared source yourself somewhere you control, and compare - even a coarse comparison of what classes or symbols exist localises the injection. Then enumerate everything the release build was permitted to execute and check what changed between the last release you trust and this one. And treat the build machine as compromised: every credential it could read is burned, so rebuilding and republishing from the same machinery is not a fix.

  • Does a green CI run on the pull request tell you the released artifact is clean?
    No. It tells you a build of that source passed its checks. The released artifact is produced by a later run, usually with different privileges, different secrets and a publishing step. If something in that run rewrites the output, every check can pass on the PR and still ship an artifact that does not correspond to the reviewed source.
  • Where would you look first for the injected step?
    At everything with write access to the output: the build configuration, the plugin list and each plugin's own dependencies, any post-compilation rewriting or packaging step, and the image or runner the build executes on. Then diff those inputs against the last release you have reason to trust - the injected step is usually something that appeared or changed recently.
  • Why does a passing unit-test suite not rule this out?
    Because the tests run against code built by the same compromised build, so they measure the injected artifact's behaviour, not the source's. Payloads are also normally dormant - conditioned on a production environment, a specific consumer or a delay - so the behaviour the tests can observe is exactly the benign behaviour the source describes.

Auditing the recipe tells you nothing about the kitchen. Build-time injection is a cook adding an ingredient after the last person read the recipe, then serving the dish under the recipe's name.

saying these in an interview costs you the question

  • Claims review of the diff proves the release is safe
  • Says a green CI run means the artifact matches the source
  • Thinks a valid signature proves it was built from that source
  • Considers only application dependencies, never build-time ones
  • Assumes the git tag is what actually produced the binary

context