Jan 24, 2026·6 min read·32 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.
A state persistence vulnerability exists in Tornado's CurlAsyncHTTPClient component where pooled pycurl.Curl handles are reused across asynchronous requests without a complete state reset. Consequently, sensitive per-request configurations, such as client TLS certificates or proxy basic authentication credentials, persist on the shared handle. This behavior leads to subsequent requests leaking these credentials to unauthorized remote servers.
CVE-2026-48748 is a denial-of-service vulnerability in Netty's HTTP/3 codec (netty-codec-http3) occurring when QPACK dynamic tables are enabled but the blocked streams limit is not explicitly configured. A bug in limit checking and a memory leak in stream tracking allow unauthenticated remote attackers to exhaust the JVM heap memory and crash the server.
CVE-2026-50009 is a cryptographic design vulnerability in the Netty network application framework. Prior to version 4.2.15.Final, the framework's QUIC protocol implementation fails to cryptographically segregate the generated Connection IDs and the associated Stateless Reset Tokens. An on-path network attacker who sniffs traffic during a Connection ID rotation can extract secret token material from cleartext headers, enabling them to inject spoofed reset packets and terminate active connections.
A critical hostname verification bypass vulnerability exists in the Netty network application framework when configured as a TLS client. When a developer registers a custom plain X509TrustManager, Netty wraps it inside an X509TrustManagerWrapper to adapt it to the X509ExtendedTrustManager API. However, this wrapper discards the SSLEngine context, bypassing critical hostname checks. Because the wrapper is identified as an X509ExtendedTrustManager, standard cryptographic engines and Netty's OpenSSL wrappers do not re-wrap it, failing to execute any hostname validation. Consequently, clients silently accept certificates for any host, enabling unauthenticated Man-in-the-Middle (MitM) attacks.
An uncontrolled resource pre-allocation flaw in the Netty Redis codec module allows remote unauthenticated attackers to cause a denial of service (OutOfMemoryError) by sending a crafted Redis Serialization Protocol (RESP) array header.
CVE-2026-50020 is a medium-severity HTTP Request Smuggling/Response Smuggling vulnerability (CWE-444) within the Netty asynchronous network application framework. The flaw resides in Netty's HTTP codec implementation, specifically the HttpObjectDecoder class, which silently consumes arbitrary ISO control bytes preceding the first request line.