Your fork-PR pipeline holds the chart publish credential and checks out the fork's Helm chart to lint it. What has that handed an attacker?
answer
- trusted steps, untrusted objects
- the fetch is the vulnerability
- lint is a program the fork supplies
- assume execution wherever the credential lives
- split: unprivileged build, trusted consumer
basics
~20 sUse of the publish credential. Once the fork's files are on the runner, almost any tool run over them becomes attacker-controlled execution inside a job that already holds the credential, so the identity that publishes your charts becomes theirs.
solid answer
~50 sIt has handed over the publishing identity. The team's reasoning is that the pipeline definition comes from the base branch, so a contributor cannot change the steps — true, and irrelevant. The steps operate on files the contributor wrote. Linting and templating a chart parses attacker-authored manifests, resolves dependencies from repositories the attacker's chart file names, and typically invokes repository-local scripts or task-runner configuration that the same checkout supplied. Any one of those is code execution in a process whose environment holds the OCI publish credential, so the attacker can read it and push a malicious chart version under your name — which downstream consumers pull and install. The fix is structural, not a stricter lint: a credentialed job must never fetch the fork's ref. Do the build in a credential-free job and let a separate trusted run consume its output as inert data.
go deeper
Know that a job holding publish credentials must not build code from a stranger's fork, and that the pipeline file coming from the main branch does not change that.
Explain the mechanism: trusted steps applied to attacker-authored files, dependency resolution following attacker pointers, and every step sharing one credentialed environment.
Demonstrate the redesign — credential-free build job, trusted consumer of inert output — and how you find every instance of the pattern across an estate instead of fixing the one you were shown.
Own the tradeoff between contributor experience and credential blast radius, and decide which conveniences the organisation stops offering outside contributors rather than moving credentials toward untrusted code.
## The setup A public chart repository accepts community contributions. Because the maintainers wanted contributors to see lint and template output on their pull requests, the pipeline was moved to an event that runs in the base repository's context — where the definition is read from the default branch and the run holds the credentials, including the identity used to publish charts to an OCI registry. To have anything to lint, the pipeline explicitly fetches the pull request's head from the fork. That single fetch is the vulnerability. Everything else is detail. ## Why "the steps are ours" is not a defence The reassuring property is real: the contributor cannot edit the pipeline steps, because the definition is read from the base branch. But a step is a verb without an object. The objects — the chart, its templates, its values, its dependency declarations, the repository's own helper scripts and task-runner configuration — all arrive from the fork. A trusted verb applied to an untrusted object is an untrusted operation. Concretely, once the fork's tree is on disk: - **The repository's own tooling is attacker-supplied.** Most projects lint through a wrapper — a script, a task file, a hook configuration — that lives in the repository and is therefore part of the checkout the contributor controls. - **Dependency resolution follows the attacker's pointers.** A chart declares where its subcharts come from. Fetching and unpacking content from an attacker-named location is not "reading a file"; it is importing code the attacker chose. - **The rest of the job inherits the environment.** Even a step that itself does nothing dangerous runs in a process tree that already contains the credential; a modified helper anywhere in the job can read and exfiltrate it. So the honest assessment is not "it might execute code" but "assume it does, and design accordingly". ## What is actually lost, and to whom The asset here is not customer data. It is **the publishing identity** — the thing that makes a consumer believe a chart came from this project. An attacker who obtains it can publish a version that installs whatever they like into every cluster that pulls the chart, and it will carry your name and, if you sign, your signature. That is a supply-chain compromise of every downstream operator, launched from a pull request that was never merged and never even reviewed. Note also what the compromise does *not* need: no merge, no maintainer mistake in review, no repository write access. The attacker's only requirement was that your pipeline processed their file with credentials present. ## The safe boundary for a credentialed run A job that runs in the base repository's context is allowed to exist, and there are legitimate uses. The rule is that it may touch **event metadata as data** and nothing else: - read the pull-request number, author or labels; - add a label, post a status, leave a comment; - never fetch the contributor's ref, and never execute anything from it. And even the metadata is contributor-written text, so it must be handled as data — never pasted into a command line — which is a separate discipline in its own right. ## Getting the lint output back to the contributor anyway The requirement was real; the design was wrong. Split by trust: 1. **Untrusted job.** Triggered by the ordinary pull-request event, so the platform withholds secrets and gives a read-only token. It fetches the fork's chart, lints it, templates it, and writes the output to a file. If the contributor's code takes this job over, they get nothing they did not already have: a credential-free machine. 2. **Trusted run.** Holds the publish or comment credential, and never fetches the fork. It picks up the file produced above, and treats every byte of it as untrusted text: it does not execute it, does not interpolate it into a command, truncates it, and identifies the target pull request from the platform's own record of the run rather than from the file's contents. The invariant worth stating out loud in an interview: **data may flow from the untrusted side to the trusted side; credentials never flow the other way.** ## Detecting the pattern in an estate This is a mechanically greppable defect, which makes it a good thing to own at scale. For every pipeline, answer two questions: does this run hold credentials, and does it obtain content from an untrusted ref? Any pipeline answering yes twice is the bug — regardless of what the steps claim to do with the content. Inventory those first, then decide per pipeline whether to drop the credential or drop the fetch. "We only lint" is not an answer, because lint is a program, and the program was chosen by the attacker.
- The team argues the job only lints, so nothing executes. Does that change your analysis?No. The linter is usually invoked through a repository-local script or task file that came with the checkout, it resolves dependencies from locations the chart names, and every later step in the job shares the same credentialed environment. "Only lints" is a claim about intent that the attacker gets to redefine, so treat the credential as reachable.
- What may a base-context run safely do on a fork pull request?Work on event metadata it never executes: read the pull-request number and author, apply a label, post a status or comment. It must not fetch the contributor's ref. Even the metadata is attacker-written text, so it belongs in a variable and never in a command string.
- How do you prove the fix across a hundred pipelines rather than one?Inventory the two properties mechanically: which runs receive credentials, and which obtain content from an untrusted ref. The intersection is the defect list. Then make it a rule that a credentialed run may not fetch a pull-request head, and enforce it as a check on pipeline definitions rather than as guidance.
saying these in an interview costs you the question
- Thinks a base-branch pipeline definition makes the job safe
- Believes linting or templating cannot execute attacker code
- Says nothing is exposed because the PR is not merged
- Relies on log masking to protect the credential
- Adds a human review step instead of removing the credential