PST-098: Pull-request workflow runs share the runner cache with main through restore-keys prefixes #827

Closed
opened 2026-10-10 10:57:03 +00:00 by tiagoagueda · 1 comment
Owner

Found in the security audit of 2026-10-10 (SECURITY-AUDIT-2026-10-10.md), run against main at c30851b from 8 to 10 October 2026, as PST-098. Severity: Informational (not settled: see below).

The text below is the audit's entry, unchanged apart from links to the other issues it names. The finding was produced by automated review and checked by a second automated pass; re-check it before acting on it.


  • Severity: Informational (not settled: see below)
  • Category: CI cache poisoning
  • Location: .forgejo/workflows/ci.yml (31,83-91,207-213,290-296)
  • Found by: configuration and dependencies

Who can do it, and what they need. An external contributor who opens a pull request, if the Forgejo instance runs PR workflows from forks and the forgejo-runner cache is not isolated by ref. Precondition: both of those, which depend on the instance's configuration and runner version and were not verifiable from the repository.

How it works.

  1. ci.yml triggers on pull_request and restores ~/.cache/uv with restore-keys: uv-${{ runner.os }}-3.14-.
  2. A PR job writes a poisoned unpacked wheel into the uv cache under a key main's jobs will later restore.
  3. Later main runs install from that cache; uv does not re-hash cached unpacked wheels on install.
  4. The poisoned code runs in the test, checks, postgres and security jobs (read-only tokens; release.yml and dependencies.yml do not use the cache).

Impact. Falsified CI results on main at most; no release credential is reachable because release.yml does not restore any cache. Recorded as informational because the precondition is outside the repository.

Evidence.

ci.yml:31 `pull_request:`; ci.yml:210-212 `path: ~/.cache/uv  key: uv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('uv.lock') }}  restore-keys: uv-${{ runner.os }}-${{ matrix.python-version }}-`.

Verification (verdict: uncertain; severity after verification: Informational). Missing to settle it: (a) forgejo-runner 6.4's cache scoping (per repository only, or per ref), and (b) the instance's setting for running fork pull-request workflows without approval. Drop postgres and security from the list of affected jobs; affected are ci.yml browser, test and checks only. Line refs: browser cache step is ci.yml:82-90, test cache step 203-212 (uses at 207), checks cache step 289-295. Remediation if the runner does not scope by ref: do not use restore-keys prefixes on pull_request runs, or skip cache restore when github.event_name == 'pull_request'; require approval for fork PR workflows.

Suggested remediation. Confirm the instance's PR-from-fork policy and the runner's cache scoping; if caches are shared, drop restore-keys prefixes (exact key only) or skip the cache step on pull_request events.

Found in the security audit of 2026-10-10 (`SECURITY-AUDIT-2026-10-10.md`), run against `main` at `c30851b` from 8 to 10 October 2026, as **PST-098**. Severity: **Informational (not settled: see below)**. The text below is the audit's entry, unchanged apart from links to the other issues it names. The finding was produced by automated review and checked by a second automated pass; re-check it before acting on it. --- - **Severity:** Informational (not settled: see below) - **Category:** CI cache poisoning - **Location:** `.forgejo/workflows/ci.yml` (31,83-91,207-213,290-296) - **Found by:** configuration and dependencies **Who can do it, and what they need.** An external contributor who opens a pull request, if the Forgejo instance runs PR workflows from forks and the forgejo-runner cache is not isolated by ref. Precondition: both of those, which depend on the instance's configuration and runner version and were not verifiable from the repository. **How it works.** 1. ci.yml triggers on pull_request and restores ~/.cache/uv with `restore-keys: uv-${{ runner.os }}-3.14-`. 2. A PR job writes a poisoned unpacked wheel into the uv cache under a key main's jobs will later restore. 3. Later main runs install from that cache; uv does not re-hash cached unpacked wheels on install. 4. The poisoned code runs in the test, checks, postgres and security jobs (read-only tokens; release.yml and dependencies.yml do not use the cache). **Impact.** Falsified CI results on main at most; no release credential is reachable because release.yml does not restore any cache. Recorded as informational because the precondition is outside the repository. **Evidence.** ```text ci.yml:31 `pull_request:`; ci.yml:210-212 `path: ~/.cache/uv key: uv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('uv.lock') }} restore-keys: uv-${{ runner.os }}-${{ matrix.python-version }}-`. ``` **Verification** (verdict: uncertain; severity after verification: Informational). Missing to settle it: (a) forgejo-runner 6.4's cache scoping (per repository only, or per ref), and (b) the instance's setting for running fork pull-request workflows without approval. Drop postgres and security from the list of affected jobs; affected are ci.yml browser, test and checks only. Line refs: browser cache step is ci.yml:82-90, test cache step 203-212 (uses at 207), checks cache step 289-295. Remediation if the runner does not scope by ref: do not use restore-keys prefixes on pull_request runs, or skip cache restore when github.event_name == 'pull_request'; require approval for fork PR workflows. **Suggested remediation.** Confirm the instance's PR-from-fork policy and the runner's cache scoping; if caches are shared, drop restore-keys prefixes (exact key only) or skip the cache step on pull_request events.
tiagoagueda added this to the 1.0.0 milestone 2026-10-10 10:58:24 +00:00
Author
Owner

Fixed in 9d7c4f9c2 on main.

Fixed in 9d7c4f9c2 on main.
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#827
No description provided.