CVE-2026-58424: Gitea Actions Fork PR Approval Gate Permanent Bypass
A one-time approval click on a fork pull request permanently disables Gitea's workflow approval gate for that contributor, letting them run arbitrary CI code on the maintainer's runner without ever ne
The problem
Gitea Actions enforces an approval gate on fork pull request workflow runs so maintainers can vet untrusted YAML before it executes on their CI infrastructure. The gate lives in `ifNeedApproval()` in `services/actions/notifier_helper.go`.
The final bypass clause queries `action_run` for any row where `(repo_id, trigger_user_id, approved_by > 0)`. It does not check the pull request, the branch, the commit SHA, or any time window. Once a maintainer clicks "Approve and run workflows" on a contributor's very first fork PR, every future fork PR from that contributor skips the gate forever, with `need_approval = 0` and `approved_by = 0` written to the new run row.
No maintainer interaction is ever required again.
Proof of concept
A working proof-of-concept for CVE-2026-58424 in code.gitea.io/gitea, with the exact payload below.
# .gitea/workflows/ci.yml — pushed in a second fork PR from the same account
# The first PR was approved. This one bypasses the gate entirely.
name: CI
on: [pull_request]
jobs:
test:
runs-on: [ubuntu-latest]
steps:
- name: pwn
env:
BASE_TOKEN: ${{ github.token }}
run: |
echo "===== BYPASS DEMO - this workflow ran WITHOUT approval ====="
echo "running as: $(id)"
echo "hostname: $(hostname)"
echo "uname: $(uname -a)"
echo "GITHUB_TOKEN length = ${#BASE_TOKEN}"
curl -fsS --max-time 5 https://example.com/ -o /dev/null -w "egress HTTP %{http_code}\n"
ls -la
echo "===== END DEMO ====="The root cause is that the unit of trust in `ifNeedApproval()` is the user identity scoped to the repository, not the pull request or commit. The query `db.Count[ActionRun]` with `Approved: true` resolves to `approved_by > 0` and returns any historical row, so a single past approval makes the gate permanently silent for that user.
The patch (PR #38010, commit 699fe2ef) replaces the "has any approved run" check with a "has a PR been merged by this user in this repo" check. That matches the intent documented in GitHub Actions, where first-contributor trust is earned by a merged contribution, not just an approved CI run.
The bypassed run row is schema-indistinguishable from a legitimate trusted-user run: `need_approval = 0`, `approved_by = 0`, status = Success. There is no audit signal separating it from an authorized execution.
The fix
Upgrade to Gitea 1.26.3 or later (upgrade directly to 1.26.4 to avoid a code-page regression in 1.26.3). The fix in PR #38010 replaces the per-user "approved run" check with a per-user "merged PR" check in `ifNeedApproval()`, so the bypass path now requires a contributor to have had a pull request actually merged into the repository before their fork CI runs without approval.
Reported by Prakhar Porwal.
Related research
- high · 8.8CVE-2026-27775CVE-2026-27775: Gitea Pre-Receive Hook Authorization Bypass via Cached Branch Permission
- high · 8.1CVE-2026-55987CVE-2026-55987: Gitea OAuth2 Sign-In Reactivates Admin-Deactivated Accounts
- high · 8.1CVE-2026-58439CVE-2026-58439: Gitea Branch Protection Bypass via PR Retargeting
- high · 7.5CVE-2026-58419CVE-2026-58419: Gitea Notification API Private Issue Metadata Leak After Access Revocation