CVE-2026-20888

Unscheduled Disruption: Killing Gitea Auto-Merges via logic Flaws

Amit Schendel
Amit Schendel
Senior Security Researcher

Jan 24, 2026·6 min read·6 visits

Executive Summary (TL;DR)

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.

The Hook: The Fragile Automaton

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 Flaw: The 'Anyone Can Stop' Button

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:

  1. Too Hot: It blocked legitimate maintainers (who aren't full admins) from managing their own merge queues.
  2. Too Cold: The logic failed to consistently apply the correct 'write' permission model across all interaction paths.

The result? A permission mismatch where the barriers to entry were either non-existent or nonsensical.

The Code: Diffing the Disaster

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.

The Exploit: Sabotage via CURL

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:

  1. Recon: Bob views the repository and sees an open PR with the 'Auto-Merge Scheduled' label. He notes the PR index (e.g., 42) and the repo owner/name.
  2. Execution: Bob authenticates (even as a low-priv user) and sends a request to the vulnerable endpoint.
# 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: ..."
  1. Effect: The server processes the request. The 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.

The Impact: Silent Workflow Death

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.

  1. Release Delays: Critical security patches scheduled for auto-merge are canceled, leaving the window of exposure open longer than necessary.
  2. Resource Waste: CI/CD runners burn CPU cycles validating code that won't land automatically, requiring human intervention to restart the process.
  3. Trust Erosion: Teams lose faith in their tooling. 'Why didn't that merge? Gitea is broken again.'

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 Fix: Restoring Order

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:

  1. Restrict Access: Make public repositories private if possible, or audit your user list to ensure only trusted individuals have read access (though this defeats the purpose of open source).
  2. WAF Rules: You could try to block POST requests to */cancel_auto_merge for non-admin IP addresses, but mapping permissions to IPs is a nightmare.
  3. Monitoring: Set up alerts for the 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.

Technical Appendix

CVSS Score
4.3/ 10
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N
EPSS Probability
0.02%
Top 97% most exploited

Affected Systems

Gitea < 1.25.4

Affected Versions Detail

Product
Affected Versions
Fixed Version
Gitea
Gitea
<= 1.25.31.25.4
AttributeDetail
CWE IDCWE-862
Attack VectorNetwork
CVSS4.3 (Medium)
ImpactIntegrity (Low)
PrivilegesLow (Read Access)
Exploit StatusPoC Available
CWE-862
Missing Authorization

The product does not perform an authorization check when an actor attempts to access a resource or perform an action.

Vulnerability Timeline

Vulnerability Published
2026-01-22
Patches Merged (PR #36341, #36356)
2026-01-22
Fixed Version v1.25.4 Released
2026-01-22

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.