CVE-2026-0798

Gitea's Ghost in the Machine: Leaking Private Release Notes via Zombie Watchers

Amit Schendel
Amit Schendel
Senior Security Researcher

Jan 24, 2026·5 min read·2 visits

Executive Summary (TL;DR)

If you fire an employee and revoke their Git access, they might still be watching your repo. In Gitea versions prior to 1.25.4, the release mailer didn't double-check permissions before hitting 'Send'. This resulted in private release notes, titles, and tags being broadcast to users who should have been locked out. The fix ensures permissions are validated at the moment of dispatch and wipes the watcher list when a repo goes private.

A logic flaw in Gitea's notification system allowed unauthorized users—specifically 'watchers' who lost access or remained subscribed after a repository went private—to continue receiving detailed release emails containing private changelogs and tags.

The Hook: The Zombie Subscriber

Imagine this scenario: You have a developer, let's call him 'Bob'. Bob is a bit of a loose cannon, so you revoke his access to your sensitive private repositories. You lock the doors, change the digital locks, and feel secure. But next Tuesday, when you push a new release titled 'v2.0 - Fixed Critical Auth Bypass', Bob gets an email about it.

He doesn't have access to the code anymore, but he knows exactly what you shipped, when you shipped it, and thanks to the detailed changelog you meticulously wrote, he knows exactly where your security bodies are buried.

This is the essence of CVE-2026-0798. It’s not a buffer overflow or a fancy ROP chain. It’s a classic logic error where the application confused 'interest' (watching a repo) with 'authorization' (actually having the right to see it). Gitea, the popular self-hosted Git service, forgot that just because someone wants to know about a release, doesn't mean they should.

The Flaw: Lazy Lists and Missing Checks

The vulnerability stems from two distinct but cooperating failures in Gitea's state management. The first is a failure of sanitization during state transition. When a repository administrator toggles a repo from 'Public' to 'Private', the system flips a boolean flag in the database. What it didn't do was evict the current audience. The list of 'watchers'—users subscribed to notifications—remained intact. This is like turning a public park into a private club but forgetting to ask the people sitting on the benches to leave.

The second, and more critical failure, was a Time-of-Check to Time-of-Use (TOCTOU) style logic gap in the mailer service. When a new release is cut, Gitea grabs the list of watchers. In the vulnerable versions, it treated this list as a trusted source of truth for recipients. It assumed that if you were on the list, you belonged there.

It never paused to ask: 'Does this user currently have read access to this now-private repository?' It just looped through the IDs and fired off emails. This meant that revoking a user's repository access didn't actually stop the information flow—it just stopped them from clicking the link, but the email content (the leak) was already delivered.

The Code: The Smoking Gun

Let's look at the fix to understand the break. The vulnerability lived in services/mailer/mail_release.go. The original code simply iterated over recipients and sent the mail. The patch introduces a necessary permission check.

Here is the diff that stopped the leak. Notice the introduction of access_model.CheckRepoUnitUser. This function is the bouncer that should have been there all along.

// services/mailer/mail_release.go
 
// BEFORE: Iterate and spray
// for _, user := range recipients {
//     SendMail(user, ...)
// }
 
// AFTER: Validate before sending
recipients = slices.DeleteFunc(recipients, func(u *user_model.User) bool {
    // If the user is the publisher, they get a pass.
    // Otherwise, check if they actually have READ access to Releases.
    return u.ID == rel.PublisherID || 
           !access_model.CheckRepoUnitUser(ctx, rel.Repo, u, unit.TypeReleases)
})

Additionally, they patched the 'Public to Private' transition logic in services/repository/repository.go. Now, when you hide a repo, Gitea nukes the watcher list entirely:

// services/repository/repository.go
 
func MakeRepoPrivate(ctx context.Context, repo *repo_model.Repository) (err error) {
    // ...
    // The new "Reset Button"
    if err = repo_model.ClearRepoWatches(ctx, repo.ID); err != nil {
        return err
    }
    // ...
}

This is a nuclear approach to the problem—unsubscribing everyone—but it safely defaults to 'secure' rather than 'convenient'.

The Exploit: Passive Listening

Exploiting this requires zero active hacking tools. No Burp Suite, no Metasploit. It relies entirely on the configuration lifecycle of the target organization. This is a persistence technique.

Scenario 1: The Insider Threat

  1. Preparation: While employed, you click 'Watch' on every critical repository you have access to.
  2. Trigger: You get fired. The admin revokes your user permissions in Gitea.
  3. Payoff: You sit back and wait. Every time the dev team pushes a new release, your personal email (if configured) or your still-active email setup receives the release title, the version tag, and the full description. If the team puts credentials or sensitive architecture details in the release notes, you own them.

Scenario 2: The Pivot

  1. Preparation: You watch a Public repository for an open-source tool maintained by a company.
  2. Trigger: The company decides to take the tool internal/private to work on a proprietary 'Pro' version. They switch the visibility to Private.
  3. Payoff: The admin thinks the door is closed. But you, an arbitrary internet user, are still on the watcher list. You now get updates on the proprietary features being added to the private codebase.

The Impact: Why Low Severity is Dangerous

The CVSS score is a measly 3.5 (Low). This is because it requires specific preconditions (Low Privileges) and only leaks 'Confidentiality' of the release metadata. But don't let the score fool you into ignoring it.

In the real world, developers are chatty. Release notes often contain:

  • "Fixed bug in API key generation logic" (Now the attacker knows the old logic was flawed).
  • "Hotfix for SQL Injection in login" (Attackers now know previous versions are vulnerable).
  • "Added support for internal server 10.0.0.5" (Internal network disclosure).

Information disclosure is the precursor to compromise. Knowing what is changing in a private codebase gives an attacker the roadmap they need to launch a more targeted attack once they find another way in, or allows them to exploit the knowledge of patched vulnerabilities against unpatched instances.

Fix Analysis (1)

Technical Appendix

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

Affected Systems

Gitea < 1.25.4

Affected Versions Detail

Product
Affected Versions
Fixed Version
Gitea
Gitea
<= 1.25.31.25.4
AttributeDetail
CWECWE-284 (Improper Access Control)
CVSS v3.13.5 (Low)
Attack VectorNetwork
Attack ComplexityLow
Privileges RequiredLow (Requires previous access)
ImpactInformation Disclosure
CWE-284
Improper Access Control

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

Vulnerability Timeline

Fix committed to Gitea repository
2026-01-20
CVE Published
2026-01-22
Advisory Modified
2026-01-23

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.