skip to content

In indirect poisoned pipeline execution, which repo files are executable inputs that reviewers read as config?

level: middleimportance: should knowfreq 45%

answer

  1. does the build read it or run it
  2. one-word entrypoint hides a whole file
  3. plugins named in a lint config get loaded
  4. restore and install run hooks
  5. the diff looks like housekeeping

basics

~20 s

Anything the job invokes rather than merely reads: build scripts, task-runner files, lint and test configuration that loads plugins, code-generation steps, and properties files that inject work into dependency restore. Reviewers see settings; the runner sees a program.

solid answer

~50 s

The rule of thumb is that a file is executable input if the build parses it with an interpreter, loads it as a plugin, or runs it as a lifecycle hook. That covers far more than people expect. Build definitions in most ecosystems are programs, not data. A task-runner file is where a one-word CI entrypoint such as `bundle exec rake ci` actually resolves. Lint and test configuration commonly names plugins that the runner loads from the tree, so the quality gate itself becomes the carrier. In a .NET repository a shared build-properties file or a custom target can inject work that runs during package restore, so the executable change never appears in a `.cs` diff at all. The review asymmetry is the whole attack: a six-line change to a properties file gets waved through, and it runs with the job's credentials on the next push.

go deeper

for a junior

Know that a build script is a program, not a settings file, and that CI runs whatever the entrypoint command resolves to inside the repository. Being able to name two or three such files is enough at this level.

for a middle

Explain the execution model: which files the build tool auto-discovers, where plugins get loaded, and which phases run hooks. Be able to walk a diff and say for each file whether the build reads it or runs it.

for a senior

Demonstrate that you would inventory these paths for a real repository, put them behind independent review, and — more importantly — make execution in an unreviewed build worthless by removing credentials from it.

for a principal

Weigh the friction of path-scoped ownership review against delivery speed across many repositories, and decide where the structural control replaces the human one rather than stacking both everywhere.

### The question a reviewer never asks For every file in a diff, there is one security-relevant question: **does the build execute this, or only read it?** Most engineers assume that only files with a source extension are executable, and that everything else is settings. In a modern repository that assumption is wrong for a large fraction of the tree, and indirect poisoned pipeline execution lives entirely in the gap. ### The categories of executable input **1. Build definitions that are actually programs.** In several major ecosystems the build file is written in a general-purpose or near-general-purpose language: a JVM build script in a Kotlin or Groovy DSL, a Makefile, a Python packaging script, an MSBuild target file. These are configuration-shaped and Turing-complete. Anyone who can edit one can run arbitrary code at build time, and in a monorepo the build file often sits outside whatever paths the team thinks of as sensitive. **2. Task-runner files behind a one-word entrypoint.** Pipelines are usually written to invoke a single verb — run the build, run the CI task — and the repository decides what that verb means. If the CI entrypoint of a Rails application is `bundle exec rake ci`, then the task-runner file is the pipeline, functionally, and it has none of the pipeline definition's protection. Adding a task, or changing what the existing `ci` task depends on, is a code change disguised as project plumbing. **3. Quality-gate configuration that loads plugins.** Lint and test configuration files frequently declare plugins or extensions that the runner then loads from the repository or from a resolved package. The linter's job is to run over the whole tree with whatever plugins it was told to load, so a plugin declaration is a request to execute code. It is a particularly comfortable hiding place because the diff reads as an improvement to the team's own standards. **4. Dependency-manager lifecycle hooks.** Restore and install phases run hooks in most ecosystems. In a .NET line-of-business repository, a shared build-properties file picked up automatically by every project can register a target that runs during package restore. The executable input is then two layers away from anything a reviewer of the feature would look at, and it fires on a phase — restore — that nobody thinks of as running repository code. It also fires early, often before any test or scan step the team believes is protecting them. **5. Code generation and vendored generated files.** A generator invoked during the build is code. A checked-in generated file that a later step compiles is code with the additional advantage that nobody reads generated diffs. ### Why this variant beats good review Three things stack up. The change is small, so it looks like housekeeping. The file lives in a directory the feature reviewer has no context for, so the reviewer defers. And the semantics are indirect: reading `<Target Name="..." AfterTargets="Restore">` or a new entry in a plugin list does not visibly say *this runs a shell command*, so even a careful reader has to know the ecosystem's execution model to see it. The attacker position that fits this best is not an anonymous outsider — it is a contributor who legitimately has commit rights and whose changes are expected. On an internal HR portal repository, that contributor's payload runs on a runner that can reach employee personal data; on a line-of-business repository it runs with the restore-time credential and a checkout of source that is itself the asset. ### Controls that actually apply - **Inventory the executable inputs.** Write down, per repository, which files the build parses, loads or hooks. If the team cannot produce that list, they cannot review these changes on purpose. - **Path-scoped required review.** Make changes under those paths require approval from a named owner, independent of the feature reviewer. This is the control that maps directly onto the attack, and it is worth its friction cost precisely because the file set is small. - **Do not let unreviewed refs reach a credentialed job.** Even a perfectly hidden payload is worth much less in a job with a read-only token, no secrets, and an ephemeral runner. Preventing execution entirely is hard; making execution worthless is achievable. - **Split the pipeline.** Build arbitrary branches unprivileged; do privileged work in a job that only trusted refs and an approval can reach. - **Detection as a backstop.** Alert on the first appearance of a build-affecting file in a repository that never had one, on network egress from a build job, and on credential use from an unexpected ref. These are detective controls and they arrive after execution — useful, but never the primary defence. ### The test to carry into a review For each file in the diff: is this parsed by an interpreter, loaded as a plugin, or run as a hook during the build? If yes, review it as code — regardless of its extension.

  • How would you produce the list of executable inputs for a repository you have just inherited?
    Start from the pipeline definition and follow every command it invokes into the repository, then follow each of those files into what it loads. Ask the ecosystem's own questions: which files are auto-discovered by the build tool, which config declares plugins, and which lifecycle phases run hooks. The output is a short path list you can then put behind required review.
  • Why is a change during dependency restore a particularly good hiding place?
    Restore runs early, before most tests, scans and policy checks, and almost nobody thinks of it as executing repository code. In ecosystems where a shared properties or target file is auto-imported by every project, the executable change is also two layers away from the feature diff, so the reviewer of the feature never sees it.
  • Is path-scoped required review enough on its own?
    No. It raises the cost of the specific edit but it is one control against a determined insider with legitimate access, and it does nothing about the many other files a build can be persuaded to execute. Pair it with the structural fix: unprivileged builds for arbitrary refs, credentials only in jobs a trusted ref can reach, and ephemeral runners.

saying these in an interview costs you the question

  • Says only source files can execute during a build
  • Thinks a linter merely reads files and never loads code
  • Assumes dependency restore does not run repository code
  • Protects the pipeline file and calls the problem solved
  • Relies on reviewers noticing a small config diff

context