Reject cross-repo artifact scopes #16
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/scope-isolation-15"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Fixes #15.
Confirmed the vulnerable path before the fix:
download-implpassed therepositoryinput intoresolveConfig, so workflow-controlled input/env could select another repository's storage prefix while all repos shared the same backend credential.Changes:
GITHUB_REPOSITORYscope.repositoryinput orRASTER_ARTIFACTS_SCOPEoverride before backend access.Verification:
npm testnpm run checkUpload follow-up: filed rasterstate/upload-artifact-action#10 for equivalent write-scope validation; this PR intentionally scopes only the download-side read boundary.
Adversarial review: CHANGES (do not merge as the #15 access-boundary fix)
I did not author this. Reviewed at
654c90b, diffed vsmain, against the #15 threat model (shared runner pool, one sharedRASTER_ARTIFACTS_S3_*credential, scope values set inside the CI job). CI is green (ci/test,test/unit,test/e2eall pass) and the suite is 35/35 locally, but green CI here only proves the two weaker vectors are closed, not the real one.The fix is client-side only, and the boundary is still bypassable. It moves the anchor from "
repositoryinput /RASTER_ARTIFACTS_SCOPE" to "GITHUB_REPOSITORY" and rejects the first two when they disagree with the third (config.js:65-82). ButGITHUB_REPOSITORYis itself job-controlled env, read straight fromprocess.env(config.js:48) with no server-side or signed binding. The attacker just tells the same lie one level down.Proof (probe 1 + 3): the bypass succeeds
Malicious job sets
GITHUB_REPOSITORYto the victim, leavesrepositoryandRASTER_ARTIFACTS_SCOPEunset. NowcurrentRepo,requestedRepository, andrequestedScopeare allrasterstate/paragon, all three agree, noConfigErroris thrown, and the shared credential reads the victim's prefix:Result: exit 0,
dest/secret.txt===VICTIM-SECRET. The action logsRepository: rasterstate/paragon run: 999and extracts. (In a first pass the run stopped one step later on the unrelated#11digest check because my seed had no digest; with the digest a real upload writes, it extracts cleanly.) So the scope check accepts the foreign scope and fetches it. This is the exact scenario #15 describes, and it is not stopped.Because every input the check compares is drawn from the attacker's own job environment, this is a client-side check the attacker controls, not an access boundary. Per the task's probe 3: that is not a fix.
What the fix does and does not do
repository's ability to select a foreign scope whileGITHUB_REPOSITORYstays honest, and blocks theRASTER_ARTIFACTS_SCOPEoverride. Fine as hardening against an accidental misconfig.GITHUB_REPOSITORYis equally forgeable and is now the sole anchor.config.js:79-80says scope "cannot be overridden by workflow-controlled env," andaction.yml/READMEsay cross-repo is "refused with shared backend credentials."GITHUB_REPOSITORYis workflow-controlled env, and the credential still grants the read.Probe 2 (legit path) and probe 4 (tests)
downloads a named artifact into the destination. Good.repository input cannot select another artifact scope,RASTER_ARTIFACTS_SCOPE cannot override the repository scope) both holdGITHUB_REPOSITORYhonest and only lie via the other knob. Neither covers theGITHUB_REPOSITORY-lie vector, i.e. the one that actually bypasses. The suite therefore gives false confidence: it asserts the boundary exactly where it holds and not where it breaks.To actually close #15
The real fix is what the issue already calls for and this PR skips: a boundary the job cannot assert from its own env.
repositoryclaim, rather than trustingprocess.env.GITHUB_REPOSITORY.Until then, keep #15 open and reduce the claims: this prevents accidental cross-scope selection, it does not isolate a malicious repo on a shared credential. Add a test that sets
GITHUB_REPOSITORYto a foreign value and asserts the read is refused, that is the case that must pass for #15 to be closed, and today it does not.Verdict: CHANGES. Not an access boundary in its current form; do not merge as the #15 fix.
Superseded. Two adversarial reviews proved cross-repo artifact isolation cannot be enforced client-side (the action runs inside the attacker-controlled job, so any env var or verification key it reads is forgeable - see the CHANGES on this PR and on #18). The real boundary must be store-side; tracked in #19 (and #17). Closing this client-side approach.
Pull request closed