The Ghost in the API: Gitea's Zombie Notification Leak (CVE-2026-20800)
Jan 24, 2026·6 min read·3 visits
Executive Summary (TL;DR)
If you kick a user out of a private Gitea repository, they shouldn't see what happens next. Due to a failure to check permissions during notification retrieval, removed users could still see the titles of new Issues and Pull Requests via the API. Fixed in version 1.25.4.
A classic logic flaw in Gitea's notification system allows users to view metadata of private repositories they no longer have access to. By failing to re-validate permissions at the time of API retrieval, Gitea effectively allowed 'zombie' access to sensitive issue and PR titles.
The Hook: The Ex-Employee Who Knew Too Much
Imagine this scenario: You have a contractor working on a sensitive project. Let's call him Bob. Bob is sloppy. You decide to revoke Bob's access to the Project-Manhattan repository. You go into Gitea settings, click 'Remove Collaborator', and breathe a sigh of relief. The locks are changed. Bob is gone.
Or is he?
In the world of CVE-2026-20800, Bob might be locked out of the front door, but Gitea left the bathroom window wide open. While Bob can no longer browse the code or push commits, the Notification API (/api/v1/notifications) acts like a gossiping neighbor. It happily continues to serve up details about what's happening inside the repository he was just kicked out of.
This isn't a complex memory corruption bug or a fancy buffer overflow. It's a fundamental misunderstanding of the difference between past access and current access. It’s a logic flaw that turns the useful feature of "staying updated" into a persistent surveillance tool for disgruntled ex-collaborators.
The Flaw: Trusting the Past
The vulnerability lies deep within services/convert/notification.go. This is the part of the Gitea codebase responsible for taking a raw notification record from the database and dressing it up into a pretty JSON object for the API response.
When a user queries their notifications, the system retrieves a list of events (like "New Issue Created" or "PR Merged"). The fatal mistake was an assumption of static trust. The code assumed that if a notification record existed in the database for a specific user, that user must have permission to see the associated repository data.
Instead of performing a fresh authorization check against the current access control list (ACL), the code simply hardcoded a AccessModeRead permission into the response object. It was effectively saying, "If you have a notification, you have read access." This is a classic Time-of-Check to Time-of-Use (TOCTOU) logical fallacy, but strictly in the business logic layer. The permission was valid when the notification was created, but the system failed to verify if it was still valid when the notification was read.
The Code: The Smoking Gun
Let's look at the vulnerable code in ToNotificationThread. This function constructs the API response. Notice how it handles the repository object:
// VULNERABLE CODE (Pre-1.25.4)
if n.Repository != nil {
// The fatal flaw: Hardcoding 'AccessModeRead'
result.Repository = ToRepo(ctx, n.Repository, access_model.Permission{AccessMode: perm.AccessModeRead})
// ... code continues to populate Subject (Issue/PR Title) ...
}The function ToRepo is called with a manually constructed Permission struct. The developers explicitly told the converter: "Treat this user as if they have Read access." There is no database query here checking if the user is actually a collaborator or if the repo is still public.
Now, look at the fix introduced in version 1.25.4 via PR #36339. The patch forces a reality check:
// PATCHED CODE (1.25.4)
if n.Repository != nil {
// The fix: ASK the database for current permissions
perm, err := access_model.GetUserRepoPermission(ctx, n.Repository, n.User)
if err != nil {
log.Error("GetUserRepoPermission failed: %v", err)
return result
}
// Only show repo details if they actually have access RIGHT NOW
if perm.HasAnyUnitAccessOrPublicAccess() {
result.Repository = ToRepo(ctx, n.Repository, perm)
}
}The difference is night and day. The patched code stops assuming and starts verifying via access_model.GetUserRepoPermission.
The Exploit: Reading the Secret Roadmap
Exploiting this requires no special tools—just a web browser or curl. Here is the attack chain for a user whose access has been revoked:
- Preparation: The attacker (User A) previously had access to a private repository (
Super-Secret-App). - Revocation: The admin removes User A from the repository.
- The Trigger: A valid user (User B) creates an Issue in that repository titled: "CRITICAL: Hardcoded AWS Keys in Payment Module".
- The Leak: User A, despite having no access to the repo, queries the API:
curl -H "Authorization: token <user_a_token>" \
https://gitea.example.com/api/v1/notifications?status-types=unread- The Result: The JSON response will contain the
subjectobject:
{
"id": 1337,
"repository": {
"full_name": "org/Super-Secret-App",
"private": true
},
"subject": {
"title": "CRITICAL: Hardcoded AWS Keys in Payment Module",
"type": "Issue"
}
}User A now knows about the critical security flaw in the payment module, simply by reading the title leaked in the notification stream.
The Impact: Metadata is Data
You might argue, "So what? They can't see the code." While true, in the world of software development, titles are metadata, and metadata is data.
Knowing the titles of Issues and Pull Requests can reveal:
- Unpatched Vulnerabilities: Titles like "Fix SQL Injection in Login" or "Rotate comprised API keys" are gold for attackers.
- Strategic Direction: Titles like "Implement support for Competitor X migration" or "Buyout preparation docs" reveal business strategy.
- Personnel Issues: Titles regarding sensitive HR data or internal conflicts if used in internal tracking repos.
This vulnerability breaks confidentiality. It violates the principle of least privilege by allowing data to persist beyond the authorization boundary.
The Fix: Check IDs at the Door
The remediation is straightforward: Upgrade to Gitea 1.25.4. The patch ensures that every time a notification is serialized for the API, a fresh permission check is performed against the current state of the database.
If you cannot upgrade immediately, there are no clean configuration workarounds other than manually purging the notification database table for users who have been offboarded, which is a risky database operation. The only real fix is the code change.
For security teams, this serves as a reminder: Access Control Lists are not static. Just because a user was allowed to receive a notification yesterday doesn't mean they are allowed to read it today. Always validate permissions at the time of access (Time-of-Use), not just at the time of creation.
Official Patches
Fix Analysis (1)
Technical Appendix
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:NAffected Systems
Affected Versions Detail
| Product | Affected Versions | Fixed Version |
|---|---|---|
Gitea Gitea | <= 1.25.3 | 1.25.4 |
| Attribute | Detail |
|---|---|
| CWE | CWE-862 (Missing Authorization) |
| Attack Vector | Network (API) |
| CVSS v3.1 | 6.5 (Medium) |
| Impact | Information Disclosure |
| Privileges | Low (Authenticated User) |
| User Interaction | None |
MITRE ATT&CK Mapping
Vulnerability Timeline
Subscribe to updates
Get the latest CVE analysis reports delivered to your inbox.