Ghost in the Repo: Deleting Gitea Attachments from the Grave
Jan 24, 2026·6 min read·3 visits
Executive Summary (TL;DR)
An Improper Access Control vulnerability in Gitea <= 1.25.3 allows a user to delete attachments they previously uploaded to a repository, even after their access to that repository has been revoked. By piggybacking on a request to a different repository they *do* control, attackers can purge critical evidence or release artifacts.
A logic flaw in Gitea's attachment handling allowed users to delete files from repositories they no longer had access to. If you uploaded it, you could kill it—even after being fired.
The Hook: The Ex-Employee's Revenge
We love self-hosted Git. It gives us control, privacy, and the warm fuzzy feeling of owning our data. Gitea is a heavyweight champion in this arena—fast, Go-based, and feature-rich. But features often bring complexity, and complexity brings logic bugs. Specifically, the kind of logic bugs that feel like a plot hole in a bad heist movie.
Imagine this scenario: You hire a contractor. They upload a bunch of architectural diagrams and documentation to your private repository. Things go south, you fire them, and you meticulously revoke their access to the repo. You think you're safe. You think your data is static and secure.
But CVE-2026-20736 says otherwise. This vulnerability is the digital equivalent of changing the locks on your office door but forgetting that the ex-employee still has a remote detonator for the filing cabinet. It turns out, in the world of Gitea versions 1.25.3 and below, ownership of a file trumped access to the room it was stored in.
The Flaw: Identity vs. Context
The root cause here is a classic case of Insecure Direct Object Reference (IDOR) mixed with a failure of contextual validation. In web application security, context is king. It's not enough to ask "Who is this user?"; you must also ask "Where is this user trying to act?"
Gitea handles attachments (images in issues, binaries in releases, PDFs in PRs) by assigning them a globally unique UUID. When a request comes in to delete an attachment, the application looks up that file by its UUID. So far, so standard.
The vulnerability lies in routers/web/repo/attachment.go. The application correctly verified that the user requesting the deletion was the same user who uploaded the file (ctx.Doer.ID == attach.UploaderID). However, it completely failed to cross-reference the attachment's location with the repository context of the request.
Essentially, the code said, "Is this your file? Yes? Okay, delete it." It never asked, "Does this file actually belong to the repository you are currently allowed to touch?" This decoupling of the object (the attachment) from its container (the repository) is what creates the exploit path.
The Code: The Smoking Gun
Let's look at the Go code that made this possible. The logic resides in the DeleteAttachment function. Before the patch, the security check was frustratingly simple.
Vulnerable Code (Pre-Patch):
// The check only validates if the user is signed in
// and if they are the original uploader.
if !ctx.IsSigned || (ctx.Doer.ID != attach.UploaderID) {
ctx.HTTPError(http.StatusForbidden)
return
}See the problem? It checks who you are, but not where the file is. If I'm User A and I uploaded File X, I pass this check. It doesn't matter if File X is in Repo Secret which I can no longer access.
The Fix (Patch 1.25.4):
The fix, introduced in PR #36320, adds the critical context check. It forces the application to verify that the attachment's RepoID matches the ID of the repository currently loaded in the request context (ctx.Repo).
// The new check ensures the attachment belongs to the current repo context
if attach.RepoID != ctx.Repo.Repository.ID {
ctx.HTTPError(http.StatusBadRequest, "attachment does not belong to this repository")
return
}They also modernized the permission model to allow repo admins to delete attachments even if they didn't upload them, but the security win is the block above: if the attachment isn't in the repo you're talking to, you get a 400 Bad Request.
The Exploit: Cleaning House from the Outside
Exploiting this requires a specific sequence of events, but it's trivial to execute if you are a disgruntled former collaborator. The only prerequisite is that the attacker must have an account on the Gitea instance and access to any repository (even a public one or their own user repo).
Here is the attack chain:
- The Setup: Alice is working on
Project-Alpha. She uploadsarchitecture_secrets.pdfto an Issue. The server assigns this file a UUID (e.g.,1234-5678...). - The Revocation: Alice gets fired. The admin removes Alice from
Project-Alpha. Alice tries to browse toProject-Alphaand gets a 404/403. Access denied. Working as intended. - The Pivot: Alice logs in and goes to
Project-Beta(or createsAlice/My-Sandbox). She has write access here. - The Strike: Alice sends a
POSTrequest to delete an attachment.
POST /Alice/My-Sandbox/issues/attachments/remove HTTP/1.1
Host: gitea.example.com
Cookie: session=alice_session_id
file=1234-5678-uuid-of-architecture-secrets-from-project-alpha- The Execution: The Gitea backend receives the request under the context of
My-Sandbox. It loads the attachment using the UUID provided. It sees Alice is the uploader. It deletes the file. - The Result:
architecture_secrets.pdfvanishes fromProject-Alpha, breaking the issue history and potentially destroying data.
The Impact: Integrity over Confidentiality
It is important to classify this correctly. This is primarily an Integrity violation. The attacker cannot read data they aren't supposed to see (so Confidentiality is preserved), nor can they take down the server (Availability is mostly fine).
However, in a DevOps environment, Integrity is everything. Attachments often contain:
- Screenshots of bugs (evidence).
- Binary builds attached to Releases.
- Spec documents or legal PDFs.
An attacker can selectively prune history. Imagine an open-source maintainer who goes rogue and decides to delete every binary release they ever uploaded, breaking downloads for thousands of users. Or an employee who deletes the proof of their misconduct attached to an internal HR ticket. It is a subtle, creeping kind of damage.
The Mitigation: Patch or Perish
There are no clever configuration workarounds here. You cannot toggle a setting to fix bad logic in a compiled binary. The path forward is an upgrade.
Immediate Action: Upgrade to Gitea 1.25.4 immediately. This release contains the patch (Commit fbea2c6) that enforces the repository ID check.
If upgrading is impossible today:
- Audit Logs: Scour your access logs for requests to
/attachments/remove. Correlate the timestamps with user sessions. It is difficult to detect the mismatch without deep packet inspection, but high volumes of deletions from users who were recently offboarded is a red flag. - WAF Rules: Theoretically, if your WAF can inspect the POST body and has knowledge of which UUIDs belong where (unlikely), you could block it. Realistically? Just patch the binary.
For developers reading this: Take this as a lesson in Authorization Context. Never assume that because a user owns an object, they have the right to modify it in the current context. Always validate the chain: User -> Context -> Object.
Official Patches
Fix Analysis (1)
Technical Appendix
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:NAffected Systems
Affected Versions Detail
| Product | Affected Versions | Fixed Version |
|---|---|---|
Gitea Gitea | <= 1.25.3 | 1.25.4 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-284 |
| Attack Vector | Network |
| CVSS v3.1 | 7.5 (High) |
| Affected Versions | <= 1.25.3 |
| Patch Version | 1.25.4 |
| Impact | Data Integrity Loss |
MITRE ATT&CK Mapping
The product does not properly restrict access to a resource from an unauthorized actor.
Known Exploits & Detection
Vulnerability Timeline
Subscribe to updates
Get the latest CVE analysis reports delivered to your inbox.