Jan 24, 2026·7 min read·46 visits
Gitea failed to verify that an attachment belongs to the repository where it is being linked. An attacker with access to a private repository can take the UUID of a sensitive attachment and 'adopt' it into a public release or issue in a different repository. This exposes private files (binaries, configs, datasets) to the public internet. Fixed in version 1.25.4.
A critical logic flaw in Gitea's attachment handling allows authenticated users to link files from private repositories to public releases, effectively bypassing access controls and exposing sensitive data to the internet.
Gitea is the darling of the self-hosted Git world. It's written in Go, it's lightweight, and it generally does a good job of keeping the doors locked. But like many modern web applications, it suffers from a common architectural headache: the disconnect between an object's existence and its context. In the world of database-driven applications, we often assign unique identifiers (UUIDs) to everything—users, repos, issues, and, crucially for this story, attachments.
Attachments in Gitea are those binary blobs you drag and drop into an issue comment or a release page. They aren't Git objects; they live in a separate storage backend (local disk, S3, etc.) and are referenced by a database entry. When you upload a file, Gitea gives it a UUID and stamps it with a RepoID to remember where it came from. So far, so good.
But here is the million-dollar question: When you tell Gitea, "Hey, attach file UUID-1234 to Release v1.0 in Repository B," does Gitea check if UUID-1234 actually belongs to Repository B? Or does it just check if UUID-1234 exists? If you guessed the latter, congratulations. You've just found a Critical vulnerability. This is a classic logic flaw where the application trusts the ID provided by the user without validating the relationship between that ID and the current context.
Technically, this falls under the umbrella of Insecure Direct Object Reference (IDOR), or specifically CWE-639 (Authorization Bypass Through User-Controlled Key). The flaw wasn't in the authentication layer—you still needed to be logged in. The flaw was in the authorization context of the attachment handler.
The vulnerability stems from how Gitea handles the "Edit Release" or "Edit Issue" actions. When you edit a release, the frontend sends a list of attachment UUIDs that should be associated with that release. The backend iterates through this list to update the database.
The logic went something like this:
UUID-Z in the payload.UUID-Z. Does it exist? Yes.UUID-Z to Release X.The missing step? Checking if UUID-Z belongs to Repo A.
This oversight allows for what I call "Attachment Smuggling." If I have read access to a private repository (Repo P), I can see the UUIDs of its attachments. If I also have write access to a public repository (Repo Public)—which is trivial since I can just create one—I can take the UUID from the private repo and tell Gitea to attach it to my public release. The database updates, the link is created, and suddenly that private tax document or proprietary binary is being served publicly from my innocuous-looking repo.
Let's look at the smoking gun. The vulnerability lived in routers/web/repo/attachment.go. The code was responsible for handling attachment updates but was too trusting of the input.
The Vulnerable Logic (Conceptual):
// OLD CODE (Vulnerable)
func UpdateReleaseAttachments(ctx *context.Context) {
// Get the attachment ID from the request
attachID := ctx.FormString("attachment_uuid")
// Load the attachment from DB
attach, err := models.GetAttachmentByUUID(attachID)
if err != nil {
// Handle error
}
// DANGER: We proceed to link the attachment without checking ownership!
attach.ReleaseID = ctx.Release.ID
models.UpdateAttachment(attach)
}The developers fixed this in Pull Request #36320 by adding a strict ownership check. They realized that just because an attachment exists doesn't mean the current context has the right to move it.
The Fix (Patched):
// NEW CODE (Fixed in 1.25.4)
func UpdateReleaseAttachments(ctx *context.Context) {
// ... load attachment ...
// THE FIX: Ensure the attachment belongs to the current repository
if attach.RepoID != ctx.Repo.Repository.ID {
ctx.HTTPError(http.StatusBadRequest, "attachment does not belong to this repository")
return
}
// ... proceed ...
}This simple condition attach.RepoID != ctx.Repo.Repository.ID completely kills the attack class. It enforces a boundary: objects created in Repo A stay in Repo A. It effectively segregates the object references by their parent container.
To exploit this, an attacker needs two things: read access to a victim's private repo (perhaps as a contractor or low-level employee) and write access to any public repo.
Step 1: Reconnaissance
The attacker navigates to the target private repository, for example, Company/TopSecretProject. They find a release or an issue containing a sensitive attachment, say credentials.json or proprietary_firmware.bin. By inspecting the network traffic (Developer Tools > Network) or the HTML source, they locate the attachment's UUID. It looks something like a1b2c3d4-e5f6-7890-1234-567890abcdef.
Step 2: The Setup
The attacker creates a new public repository, Attacker/Free-Wallpapers. They create a new Release titled "v1.0".
Step 3: The Swap The attacker intercepts the HTTP request used to create or edit the release in their public repo. The payload will contain a field for attachments. They inject the stolen UUID from Step 1:
POST /Attacker/Free-Wallpapers/releases/new
{
"tag_name": "v1.0",
"title": "Cool Wallpaper",
"files": [
"a1b2c3d4-e5f6-7890-1234-567890abcdef" // <--- The UUID from the PRIVATE repo
]
}Step 4: The Payload
The server processes the request. It sees the UUID, grabs the file pointer from the storage backend, and links it to the Attacker/Free-Wallpapers release. Now, anyone on the internet can visit the attacker's public release page and download the proprietary_firmware.bin directly, bypassing the authentication checks that should have protected the original private repository.
This vulnerability is rated Critical (9.1) for a reason. It completely undermines the "Private" setting on a repository. In a corporate environment, Gitea is often used to host internal tools, configuration files with embedded secrets, or source code that isn't meant for the public eye.
Consider a scenario where a CI/CD pipeline uploads build artifacts to a private Gitea release. These artifacts might contain debug symbols, hardcoded API keys, or intellectual property. An insider threat—or someone who has compromised a low-level account—can use this vulnerability to exfiltrate gigabytes of data without triggering typical "mass download" alarms, because the traffic looks like legitimate usage of the public interface.
Furthermore, because the file is now hosted on a public URL, the attacker doesn't just download it; they can share the link with others. The file is effectively "laundered" through the public repository.
The fix is straightforward: Update to Gitea 1.25.4. The patch introduces the necessary validation logic to prevent cross-repository attachment linking.
If you cannot upgrade immediately, you are in a tight spot. There are no easy configuration toggles to disable this specific behavior. You would need to implement a Web Application Firewall (WAF) rule that inspects the body of POST requests to release/issue endpoints, but validating UUID ownership at the WAF level is practically impossible without querying the database.
Post-Exploitation Forensics:
If you suspect this has happened, you need to query your database. You can run a SQL query joining the attachment table with the repository table (via releases or issues) to find discrepancies.
-- Pseudo-SQL to find 'smuggled' attachments
SELECT * FROM attachment a
JOIN release r ON a.release_id = r.id
WHERE a.repo_id != r.repo_id;If that query returns rows, someone has linked an attachment from one repo to a release in another. Investigate immediately.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Gitea Gitea | <= 1.25.3 | 1.25.4 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-639 |
| Attack Vector | Network |
| CVSS Score | 9.1 (Critical) |
| EPSS Score | 0.00017 |
| Impact | High Confidentiality Loss |
| Exploit Status | No Public PoC |
Authorization Bypass Through User-Controlled Key
A UNIX symbolic link following vulnerability exists in the provider cache installation mechanism of OpenTofu. This flaw allows an attacker with control over the repository files to write files outside of the intended workspace boundary during initialization.
An incorrect authorization vulnerability (CWE-863) in Snipe-IT versions prior to 8.6.0 allows authenticated, low-privileged users with granular 'users.edit' permissions to modify restricted user flags ('activated' and 'ldap_import') and merge high-privileged administrator accounts into standard user accounts. This allows an attacker to lock administrators out of the system or completely hijack administrator accounts.
An open redirect vulnerability exists in Flask-Security versions up to and including 5.8.0. This flaw allows remote, unauthenticated attackers to perform open redirects by exploiting a parser differential between Python's standard library urlsplit() function and modern web browsers when subdomain redirection is allowed.
An incomplete security patch for CVE-2026-24421 in phpMyFAQ allows authenticated low-privileged users to bypass role-based access controls. While the initial patch addressed missing authorization in the BackupController, it left four critical write-enabled endpoints vulnerable. This allows remote attackers with a valid low-privilege API token to perform unauthorized data modifications, creating categories, creating FAQs, updating FAQs, and injecting questions directly into the database.
An in-depth security audit of the skillctl command-line package manager revealed five critical and high-severity security vulnerabilities. The identified flaws span parameter-level command argument injection via the source_sha parameter, uncontrolled resource consumption (Denial of Service) through unnamed UNIX FIFOs and character devices, directory path traversal in the destination argument, commit-message trailer forgery via newline injection in skill names, and local credential exfiltration leveraging UNIX hardlinks. These vulnerabilities represent significant vectors for workstation compromise when executing agentic tasks in repositories containing untrusted files or pull requests. Remediation was introduced in version v0.1.3.
CVE-2026-48153 is a Server-Side Request Forgery (SSRF) vulnerability in the Budibase OAuth2 SDK prior to version 3.39.0. It allows authenticated low-privileged users to bypass outbound network security blacklists and send arbitrary requests to internal subnets or cloud metadata services.