CVE-2026-20750

Project Mayhem: Gitea Cross-Org IDOR (CVE-2026-20750)

Alon Barad
Alon Barad
Software Engineer

Jan 24, 2026·6 min read·3 visits

Executive Summary (TL;DR)

If you have write access to *any* organization's projects on a Gitea instance (version <= 1.25.3), you can close, reopen, or delete *any* project on the entire server. This is a text-book Insecure Direct Object Reference (IDOR) where the application validates your permission to act, but fails to validate that the object you are acting upon belongs to your jurisdiction.

A critical IDOR vulnerability in Gitea allows attackers with project write access in one organization to modify or delete projects in completely unrelated organizations. It's a classic case of checking permissions for the wrong object.

The Hook: Tenant Isolation is Hard

Multi-tenancy is the bedrock of modern software. When you host a Gitea instance for your company, you assume that 'Marketing' cannot delete 'Engineering's' kanban boards just because they feel like it. You assume that Organization A is a fortress, impenetrable to the users of Organization B.

Well, in CVE-2026-20750, those walls turned out to be made of paper. Gitea, the beloved lightweight self-hosted Git service, suffered from a classic logic error in its Project management module. It’s the kind of bug that makes you slap your forehead: the code checked if you had keys to your house, but then let you unlock your neighbor's door with them.

This isn't a complex memory corruption bug involving heap feng shui. This is a logic flaw that allows any user with a modicum of privilege (write access to any project) to wreak absolute havoc on the instance's data integrity. If you run a public or large internal Gitea instance, this is the digital equivalent of handing everyone a master key.

The Flaw: Trusting the URL Blindly

The vulnerability lies in how Gitea handled HTTP requests for Organization Projects. Specifically, the route handlers for modifying projects (like closing them or deleting them) failed to perform a critical cross-reference check.

When a user sends a request to modify a project, the application must answer two questions:

  1. Authorization: Does this user have permission to modify projects in the current context (Organization A)?
  2. Ownership: Does the project they want to modify (Project #1337) actually belong to Organization A?

Gitea successfully asked question #1. It checked if the user was an admin or had write access to Org A. However, it completely forgot to ask question #2. It simply took the Project ID provided in the URL (/org/OrgA/projects/1337) and executed the command on that ID.

Because Project IDs are global database integers, if you knew the ID of a project in Org B (which is often sequential and easy to guess), you could simply pass that ID while sitting comfortably inside Org A. The code saw you had rights in Org A and happily nuked the project from Org B.

The Code: The Smoking Gun

Let's look at the vulnerable code in routers/web/org/projects.go. The flaw is painfully obvious in hindsight. Here is the logic for changing a project's status (Open/Closed) before the patch:

// PRE-PATCH: The "Trust Me Bro" Approach
func ChangeProjectStatus(ctx *context.Context) {
    // 1. Get the Project ID from the URL path
    id := ctx.PathParamInt64("id")
    
    // 2. Call the model to change status
    // Note the '0': This signifies it's an Org project, not a repo project.
    // CRITICAL FAIL: It does not check if 'id' belongs to ctx.Org.Organization
    if err := project_model.ChangeProjectStatusByRepoIDAndID(ctx, 0, id, toClose); err != nil {
        // Error handling...
    }
    // Success...
}

The fix, introduced in commit 7b5de594cd92e30b9c3d40ffda119acad794cc64, forces a verification step. It retrieves the project object first, specifically asking the database: "Give me project X, but only if it belongs to Owner Y."

// POST-PATCH: The "Verify Then Trust" Approach
func ChangeProjectStatus(ctx *context.Context) {
    id := ctx.PathParamInt64("id")
 
    // NEW: Verify ownership before action.
    // ctx.ContextUser is the Organization context being accessed.
    project, err := project_model.GetProjectByIDAndOwner(ctx, id, ctx.ContextUser.ID)
    if err != nil {
        // If the project exists but belongs to another Org, this returns an error.
        ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
        return
    }
 
    // Now we proceed with the safe ID
    if err := project_model.ChangeProjectStatusByRepoIDAndID(ctx, 0, project.ID, toClose); err != nil {
        // ...
    }
}

By enforcing GetProjectByIDAndOwner, the exploit is dead. If an attacker tries to reference a foreign project ID, the lookup fails, and the code bails out before making changes.

The Exploit: Cross-Org Project Assassination

To exploit this, you don't need fancy tools. curl or Burp Suite is enough. Let's assume we are an attacker, Mallory, who has a legitimate account and has created an organization called MalloryCorp.

The Target: A project named "Top Secret Roadmap" in the TargetCorp organization. Let's say its Project ID is 500.

The Setup:

  1. Mallory logs into Gitea.
  2. Mallory navigates to her own organization, MalloryCorp.
  3. She opens the Network tab in her browser's dev tools or fires up Burp Suite.

The Attack: Mallory sends a POST request to the project closure endpoint. Normally, this URL would look like /org/MalloryCorp/projects/10/close (closing one of her own projects). Instead, she swaps the ID:

POST /org/MalloryCorp/projects/500/close HTTP/1.1
Host: gitea.example.com
Cookie: session=...
Content-Type: application/json
 
{"action": "close"}

The Execution Flow:

Suddenly, the "Top Secret Roadmap" in TargetCorp vanishes from their active board. Mallory can repeat this for IDs 1 through 10,000, effectively wiping every organization project on the server.

The Impact: Why Panic?

While this vulnerability does not offer Remote Code Execution (RCE), the business impact is severe (CVSS 9.1).

1. Data Loss (Availability): The most immediate threat is the deletion of project boards. In Gitea, projects hold issues, kanban columns, and workflow states. An attacker can iterate through sequential IDs and delete every project on the instance. While Git repositories remain untouched, the management layer is destroyed.

2. Data Manipulation (Integrity): Attackers can rename columns or reorder issues in other organizations, causing confusion and disrupting workflows. Imagine an attacker moving all "In Review" security tickets to "Done" without anyone noticing.

3. Reconnaissance (Confidentiality): Though the primary vector is modification, the behavior of the endpoint (success vs. 404) can be used as an oracle to map out valid Project IDs across the instance, gauging the activity levels of other organizations.

The Fix: Mitigation & Remediation

The remediation is straightforward: Patch immediately.

Official Fix: Upgrade to Gitea v1.25.4 or later. The patch refactors the router logic to ensure strict ownership checks on every database lookup involving Organization Projects.

Temporary Mitigation: If you cannot upgrade immediately, your options are limited because this is a logic flaw in the application code. WAF rules might help, but they are tricky to implement correctly without blocking legitimate traffic.

> [!TIP] > WAF Strategy: You could attempt to correlate the Referer header with the URL path, but this is brittle. Since the attack looks like a legitimate request (just with mismatched IDs), it is very hard to detect at the network layer. The only real fix is code-side.

Lessons Learned: For developers, this is a reminder that context is king. Never assume that just because a user is authorized to be here, they are authorized to touch that. Always validate the tuple (ResourceID, OwnerID) rather than just the ResourceID.

Fix Analysis (1)

Technical Appendix

CVSS Score
9.1/ 10
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
EPSS Probability
0.03%
Top 91% most exploited

Affected Systems

Gitea Server

Affected Versions Detail

Product
Affected Versions
Fixed Version
Gitea
Gitea
<= 1.25.31.25.4
AttributeDetail
CWE IDCWE-284 (Improper Access Control)
Attack VectorNetwork (AV:N)
CVSS v3.19.1 (Critical)
Root CauseMissing ownership validation on Project ID lookup
PrerequisitesAuthenticated User, Write Access to ANY Organization Project
Exploit StatusHigh (Trivial Logic Flaw)
CWE-284
Improper Access Control

The product does not restrict or incorrectly restricts access to a resource from an unauthorized actor.

Vulnerability Timeline

Fix commit merged
2026-01-14
Gitea v1.25.4 Released
2026-01-22
Advisory GHSA-h4fh-pc4w-8w27 Published
2026-01-23

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.