Seven actions/untrusted-checkout findings arrived with code scanning (#6). Six of them are low-stakes. The one in cd-apply.yml is not, and it is worth separating them.
The chain
cd-apply.yml validates its input carefully:
[[ "$CONTRACT_SHA" =~ ^[0-9a-f]{40}$ ]]
That constrains the shape of the SHA. It does not constrain where the SHA came from. The next step checks out that ref and the step after executes code from it:
- uses: actions/checkout@…
with:
repository: NDDev-OpenNetwork/cd-workflows
ref: ${{ inputs.contract_sha }}
path: _cd
- uses: ./_cd/.github/actions/lifecycle # executes the checked-out code
Every commit pushed to a pull request — including from a fork — is reachable in the base repository's object store. refs/pull/N/head is fetchable, and a bare SHA resolves regardless of which branch, if any, contains it. So "a valid 40-hex SHA in this repository" includes commits nobody reviewed and nobody merged.
Why it matters here specifically
|
cd-evidence, cd-plan, cd-verify, cd-resume, cd-rollback |
cd-apply |
| runner |
ubuntu-latest (hosted, disposable) |
[self-hosted, cd-apply-out-of-band] |
| permissions |
contents: read |
contents: read, id-token: write |
| environment |
none |
cd-apply |
So for cd-apply the consequence of pointing contract_sha at unreviewed code is execution on the estate's own hardware, with the ability to mint OIDC tokens. That is the exact risk ci-workflows/scripts/_runners.py was written to prevent for public repositories, arriving through a different door.
persist-credentials: false is set on both checkouts and is doing real work — no git token is left on disk. It does not help against the executed code itself.
The environment provides no gate today
environment: cd-apply reads like an approval boundary. It is not one yet:
GET /repos/NDDev-OpenNetwork/cd-workflows/environments -> (empty)
GET /repos/NDDev-OpenNetwork/cd-workflows/environments/cd-apply -> 404
The repository has no environments at all. GitHub creates one implicitly the first time a job references it, with no protection rules and no required reviewers. So the line that looks like the control is currently a label.
What would actually close it
Validating provenance rather than shape. In rough order of strength:
- Require the SHA to be an ancestor of
main. One git merge-base --is-ancestor against a shallow-fetched main, before the second checkout. Cheap, and it excludes every unmerged commit by construction.
- Require a signed tag and resolve the SHA from it, so the contract version is something someone deliberately published.
- Pin the contract in the reusable rather than accepting it as an input, and let callers upgrade by bumping their
uses: pin — which is reviewable in their diff.
And independently of the choice: create the cd-apply environment with required reviewers, so the declaration means what it reads as.
Scope
Filed rather than fixed. Which of the three is right is a decision about what contract_sha is for — if callers are meant to pin an arbitrary contract revision, (1) narrows that deliberately; if they are meant to track releases, (2) or (3) is closer to the intent. That is not a mechanical edit.
The two actions/code-injection findings from the same scan were mechanical and are fixed in #7.
Seven
actions/untrusted-checkoutfindings arrived with code scanning (#6). Six of them are low-stakes. The one incd-apply.ymlis not, and it is worth separating them.The chain
cd-apply.ymlvalidates its input carefully:That constrains the shape of the SHA. It does not constrain where the SHA came from. The next step checks out that ref and the step after executes code from it:
Every commit pushed to a pull request — including from a fork — is reachable in the base repository's object store.
refs/pull/N/headis fetchable, and a bare SHA resolves regardless of which branch, if any, contains it. So "a valid 40-hex SHA in this repository" includes commits nobody reviewed and nobody merged.Why it matters here specifically
cd-evidence,cd-plan,cd-verify,cd-resume,cd-rollbackcd-applyubuntu-latest(hosted, disposable)[self-hosted, cd-apply-out-of-band]contents: readcontents: read,id-token: writecd-applySo for
cd-applythe consequence of pointingcontract_shaat unreviewed code is execution on the estate's own hardware, with the ability to mint OIDC tokens. That is the exact riskci-workflows/scripts/_runners.pywas written to prevent for public repositories, arriving through a different door.persist-credentials: falseis set on both checkouts and is doing real work — no git token is left on disk. It does not help against the executed code itself.The environment provides no gate today
environment: cd-applyreads like an approval boundary. It is not one yet:The repository has no environments at all. GitHub creates one implicitly the first time a job references it, with no protection rules and no required reviewers. So the line that looks like the control is currently a label.
What would actually close it
Validating provenance rather than shape. In rough order of strength:
main. Onegit merge-base --is-ancestoragainst a shallow-fetchedmain, before the second checkout. Cheap, and it excludes every unmerged commit by construction.uses:pin — which is reviewable in their diff.And independently of the choice: create the
cd-applyenvironment with required reviewers, so the declaration means what it reads as.Scope
Filed rather than fixed. Which of the three is right is a decision about what
contract_shais for — if callers are meant to pin an arbitrary contract revision, (1) narrows that deliberately; if they are meant to track releases, (2) or (3) is closer to the intent. That is not a mechanical edit.The two
actions/code-injectionfindings from the same scan were mechanical and are fixed in #7.