SEC2-082: Firefox workflows keep the checkout credential in the working copy while running code the pull request controls #10

Closed
opened 2026-10-17 10:40:18 +00:00 by tiagoagueda · 0 comments
Owner

Found on 2026-10-16 in a read-only security review of postulo-firefox at c9ee306, checked by a second, independent pass.

Severity: Low · Category: Configuration · Reference: CWE-522 (Insufficiently Protected Credentials); CWE-829 (Inclusion of Functionality from Untrusted Control Sphere); OWASP A08:2021 (Software and Data Integrity Failures)

Location:

  • .forgejo/workflows/ci.yml:14-15 (actions/checkout@v4 with no persist-credentials: false, then npm ci)
  • .forgejo/workflows/release.yml:24-25 (same)
  • scripts/build.mjs:15-21, 34 (clones pin.repository from source.json and executes its scripts/build.mjs)
  • postulo-chromium .forgejo/workflows/ci.yml:14-16 (the model: persist-credentials: false)

Issue

Both postulo-firefox workflows run on pull_request and start with

- uses: actions/checkout@v4
- run: npm ci

actions/checkout writes the job's token into .git/config unless persist-credentials: false
is set; postulo-chromium's workflow sets it and this repository's does not. Everything after it
runs code the pull request chooses: the lifecycle scripts of whatever package-lock.json the PR
carries under npm ci, and scripts/build.mjs, which reads source.json from the PR and
git clones and executes the scripts/build.mjs of that repository at that ref
(scripts/build.mjs:20-21, 34). A PR that changes source.json's repository therefore runs a
repository of its choosing in CI. That CI runs PR code is inherent; the credential left beside it
is not. Whether Forgejo hands a fork's pull_request a token with write scope is a server setting
that could not be checked from the repository, so the reach below is "likely"; the missing flag
is confirmed.

Impact

Someone who opens a pull request against postulo-firefox may be able to read the job's token from
the checkout and act on the repository with it (push to a branch, alter the release workflow); the
release job on a tag push is then only as trustworthy as that token's scope. Depends on the
Forgejo instance's token policy for forks.

Fix

In both workflows:

- uses: actions/checkout@v4
  with:
    persist-credentials: false

and have scripts/build.mjs refuse a pin.repository other than
https://source.tiagoagueda.com/postulo/postulo-chromium.git, so a PR cannot point the build
elsewhere by editing one JSON line. (Action references stay on tags, as the core's
.forgejo/zizmor.yml records for this Forgejo; a permissions: block is not added, since Forgejo
16 ignores it.)

Done when both workflows carry persist-credentials: false, a grep of .git/config after
checkout in CI shows no extraheader, and a test of build.mjs refuses a source.json naming
another repository.

#1 (closed), "Reproducible builds … and small hardening": its item 3 asked to update
upload-artifact@v3, which stays at v3 by a documented choice (ci.yml:26-30); it did not
mention persist-credentials.

Found on 2026-10-16 in a read-only security review of `postulo-firefox` at `c9ee306`, checked by a second, independent pass. **Severity:** Low · **Category:** Configuration · **Reference:** CWE-522 (Insufficiently Protected Credentials); CWE-829 (Inclusion of Functionality from Untrusted Control Sphere); OWASP A08:2021 (Software and Data Integrity Failures) **Location:** - `.forgejo/workflows/ci.yml:14-15` (`actions/checkout@v4` with no `persist-credentials: false`, then `npm ci`) - `.forgejo/workflows/release.yml:24-25` (same) - `scripts/build.mjs:15-21, 34` (clones `pin.repository` from `source.json` and executes its `scripts/build.mjs`) - postulo-chromium `.forgejo/workflows/ci.yml:14-16` (the model: `persist-credentials: false`) ## Issue Both postulo-firefox workflows run on `pull_request` and start with ```yaml - uses: actions/checkout@v4 - run: npm ci ``` `actions/checkout` writes the job's token into `.git/config` unless `persist-credentials: false` is set; postulo-chromium's workflow sets it and this repository's does not. Everything after it runs code the pull request chooses: the lifecycle scripts of whatever `package-lock.json` the PR carries under `npm ci`, and `scripts/build.mjs`, which reads `source.json` from the PR and `git clone`s and executes the `scripts/build.mjs` of *that* repository at *that* ref (`scripts/build.mjs:20-21, 34`). A PR that changes `source.json`'s `repository` therefore runs a repository of its choosing in CI. That CI runs PR code is inherent; the credential left beside it is not. Whether Forgejo hands a fork's `pull_request` a token with write scope is a server setting that could not be checked from the repository, so the reach below is "likely"; the missing flag is confirmed. ## Impact Someone who opens a pull request against postulo-firefox may be able to read the job's token from the checkout and act on the repository with it (push to a branch, alter the release workflow); the release job on a tag push is then only as trustworthy as that token's scope. Depends on the Forgejo instance's token policy for forks. ## Fix In both workflows: ```yaml - uses: actions/checkout@v4 with: persist-credentials: false ``` and have `scripts/build.mjs` refuse a `pin.repository` other than `https://source.tiagoagueda.com/postulo/postulo-chromium.git`, so a PR cannot point the build elsewhere by editing one JSON line. (Action references stay on tags, as the core's `.forgejo/zizmor.yml` records for this Forgejo; a `permissions:` block is not added, since Forgejo 16 ignores it.) Done when both workflows carry `persist-credentials: false`, a grep of `.git/config` after checkout in CI shows no `extraheader`, and a test of `build.mjs` refuses a `source.json` naming another repository. ## Related #1 (closed), "Reproducible builds … and small hardening": its item 3 asked to update `upload-artifact@v3`, which stays at v3 by a documented choice (`ci.yml:26-30`); it did not mention `persist-credentials`.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo-firefox#10
No description provided.