Jan 24, 2026·6 min read·15 visits
In Gitea versions up to 1.25.3, the 'Scheduled Auto-Merge' feature lacked proper authorization checks on its cancellation endpoints. This means any user with read access (even a lowly intern or a random public user) could cancel a maintainer's scheduled merge, causing silent delays in deployment. Fixed in v1.25.4 by enforcing strict permission checks.
A logic flaw in Gitea's access control allows any user with read access to a repository to cancel scheduled auto-merges, effectively enabling low-privileged users to disrupt CI/CD workflows and release pipelines.
Automation is the holy grail of DevOps. We set up CI/CD pipelines, branch protections, and auto-merges so we can go grab coffee while the robots do the heavy lifting. Gitea, a popular lightweight Git server, offers a 'Scheduled Auto-Merge' feature. It promises that once your tests pass and approvals land, the code merges itself. It's 'set it and forget it.'
But here's the kicker with CVE-2026-20888: 'forgetting it' was dangerous. Because while you were sipping that coffee, expecting your code to be in production, someone else—potentially a user with barely any privileges—could simply reach over and turn off the switch.
This isn't a complex memory corruption bug or a fancy SQL injection. It's a classic logic flaw where the application simply forgot to ask, 'Hey, are you actually allowed to touch this?' It’s the digital equivalent of locking your front door but installing a 'Cancel Security' button on the outside wall.
The vulnerability resides in how Gitea handled the cancellation of these scheduled merges. There are two ways to tell Gitea to stop an auto-merge: via the Web Interface and via the API.
In the Web Interface (routers/web/repo/pull.go), the handler for canceling the auto-merge explicitly lacked an authorization check. It retrieved the Pull Request data, but it didn't verify if the person clicking the button was the original scheduler ('the doer') or a repository maintainer. If you could see the PR, you could likely hit the endpoint.
The API Endpoint (routers/api/v1/repo/pull.go) was slightly more confused. It did have a check, but it was the wrong one. It checked if the user was a Repo Admin. This is a classic 'Goldilocks' failure in permissions:
The result? A permission mismatch where the barriers to entry were either non-existent or nonsensical.
Let's look at the smoking gun. The fix involved unifying the logic to use a proper permission check function: pull_service.IsUserAllowedToMerge.
In the API Router, notice how the check shifts from a blunt 'Is Admin' check to a nuanced 'Can Merge' check:
// routers/api/v1/repo/pull.go
// BEFORE: Only Admins allowed? Weird logic.
- allowed, err := access_model.IsUserRepoAdmin(ctx, ctx.Repo.Repository, ctx.Doer)
// AFTER: Check if the user actually has merge rights.
+ allowed, err := pull_service.IsUserAllowedToMerge(ctx, pull, ctx.Repo.Permission, ctx.Doer)However, the real horror show was in the Web Router, where the check was effectively absent or implicit. The patch forces a verification step:
// routers/web/repo/pull.go
// ADDED IN FIX:
if ctx.Doer.ID != scheduledPR.DoerID {
// If you aren't the one who scheduled it...
allowed, err := pull_service.IsUserAllowedToMerge(ctx, ctx.Repo.Repository, ctx.Repo.Permission, ctx.Doer)
if !allowed {
ctx.Error(403)
return
}
}This code block says: 'If you are the person who scheduled this, proceed. If not, prove you have merge rights.' Simple, effective, and previously missing.
Exploiting this doesn't require Metasploit. It requires curl and a grudge.
Imagine a scenario where a developer, Alice, schedules a critical bug fix to auto-merge over the weekend once the slow test suite finishes. Bob, a disgruntled user with read-only access to the public repo, decides to cause chaos.
The Attack Chain:
42) and the repo owner/name.# The Web Interface vector
curl -X POST "https://gitea.target.local/owner/repo/pulls/42/cancel_auto_merge" \
-H "Cookie: i_like_gitea=..." \
-H "X-Csrf-Token: ..."scheduled_auto_merge table entry is deleted. Alice's PR passes tests on Sunday morning, but Gitea does nothing. The fix is not deployed. Monday morning starts with an incident call.Because the impact is 'Integrity Low' and 'Confidentiality None', this flies under the radar of many scanners. But for a DevOps team, this is a denial-of-service against their workflow.
Why panic over a CVSS 4.3?
Because reliability is security. In modern DevSecOps, we treat infrastructure as code and deployments as automated pipelines. If an attacker can silently decouple the train from the engine, the train stops moving, and nobody notices until the passengers start screaming.
While this won't dump your database, it allows an insider threat or a public nuisance to sandbag your engineering velocity with zero technical sophistication.
The mitigation is straightforward: Update to Gitea v1.25.4.
If you cannot update immediately, you are in a tight spot. Since this logic resides in the compiled Go binary, you can't just 'patch a file' like in PHP or Python. You would need to:
POST requests to */cancel_auto_merge for non-admin IP addresses, but mapping permissions to IPs is a nightmare.automerge.RemoveScheduledAutoMerge action in your audit logs. If you see it triggered by someone who isn't a maintainer, you have a problem.But seriously, just update the binary.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Gitea Gitea | <= 1.25.3 | 1.25.4 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-862 |
| Attack Vector | Network |
| CVSS | 4.3 (Medium) |
| Impact | Integrity (Low) |
| Privileges | Low (Read Access) |
| Exploit Status | PoC Available |
The product does not perform an authorization check when an actor attempts to access a resource or perform an action.