CVE-2026-20883

The Zombie Stopwatch: Haunting Gitea with Revoked Access

Amit Schendel
Amit Schendel
Senior Security Researcher

Jan 24, 2026·6 min read·3 visits

Executive Summary (TL;DR)

Gitea forgot to check if you still work there before showing you your timesheets. If a user started a stopwatch on a private issue and then lost access to the repository, the API would still happily serve up the issue title and repo name in the stopwatch list. It's a classic Broken Access Control (CWE-284) fixed in version 1.25.4.

A logic flaw in Gitea's stopwatch feature created a persistence vulnerability where users maintained visibility into private issue metadata after access revocation. By failing to re-validate permissions during API object serialization, the system allowed 'zombie' stopwatch records to leak sensitive titles and repository names.

The Hook: The Ghost in the Machine

In the world of access control, we usually worry about the front door. Can the attacker pick the lock? Can they steal a key? But sometimes, the vulnerability isn't about getting in; it's about what you leave behind when you're kicked out. Enter Gitea's Stopwatch feature—a seemingly innocent productivity tool that allows developers to track time on issues.

Imagine this: You are a contractor hired to work on a top-secret project. You start a timer on an issue titled "Critical Security Flaw in Authentication". A week later, you are fired. Your access to the repository is revoked. You can't see the code, you can't see the issues, and you definitely shouldn't know what the team is working on.

But because Gitea's developers treated the stopwatch as a personal metric rather than a repository-tied asset, that timer keeps ticking in your personal dashboard. And unlike a real physical stopwatch, this digital ghost carries the metadata of the thing it was timing. It's not just counting seconds; it's whispering secrets from a room you're no longer allowed to enter.

The Flaw: Trusting the Past

The root cause here is a classic failure in the Object-Relational Mapping (ORM) to API conversion layer. When you build an API, you often pull raw rows from a database and pass them through a "serializer" or "converter" to turn them into pretty JSON for the frontend. In Gitea, the function responsible for this was ToStopWatches in services/convert/issue.go.

The logic was dangerously simple: "Fetch all stopwatch records associated with User X." The code assumed that if a record existed in the database, the user must have permission to see it. It treated the database state as the source of truth for authorization, rather than checking the current access control list (ACL) at runtime.

This is a Time-of-Check to Time-of-Use (TOCTOU) variant effectively turned into a permanent state. Validating permission only when the stopwatch is started is insufficient. Permissions change. Teams change. Repositories go from public to private. By failing to re-validate the relationship between the user and the repository during the read operation (the API call), the code created a permanent window into the past state of authorization.

The Code: The Smoking Gun

Let's look at the difference between "it works" and "it's secure." The vulnerability existed because the conversion function was blind to the context of the user's current permissions.

The Vulnerable Logic (Pseudo-code):

// Old way: Blindly convert DB rows to JSON
func ToStopWatches(stopwatches []Stopwatch) []APIStopwatch {
    var apiWatches []APIStopwatch
    for _, sw := range stopwatches {
        // Just grab the issue and repo details directly
        repo := GetRepo(sw.RepoID)
        issue := GetIssue(sw.IssueID)
        
        // Dump it into the response
        apiWatches = append(apiWatches, &APIStopwatch{
            Repo:  repo.Name,
            Issue: issue.Title,
        })
    }
    return apiWatches
}

The Fix (Commit 95ea2df):

The patch introduces two critical changes. First, it passes the requesting user (doer) into the function. Second, it explicitly checks CanReadIssuesOrPulls. If you can't read the issue anymore, the stopwatch is effectively invisible to the API response.

// The Fix: Trust but Verify
func ToStopWatches(ctx, stopwatches, doer) {
    for _, sw := range stopwatches {
        repo := GetRepo(sw.RepoID)
        
        // CRITICAL: Check permissions at runtime
        perm, _ := access_model.GetUserRepoPermission(ctx, repo, doer)
        
        // If the door is locked, don't tell them what's inside
        if !perm.CanReadIssuesOrPulls(sw.Issue.IsPull) {
            continue 
        }
 
        // Safe to add to response
        apiWatches = append(apiWatches, ...)
    }
}

This is the difference between asking "Did you have a key yesterday?" and "Do you have a key right now?"

The Exploit: Ex-Employee Revenge

Exploiting this requires no fancy tools—just standard API interactions. This is a "logic bug," which is my favorite kind because WAFs are useless against it.

Step 1: Infiltration Get added to a private repository. This could be legitimate employment, a contractor gig, or a compromised account that is temporarily added to a team.

Step 2: Tagging the Assets Browse the repository for sensitive issues. Maybe there's an issue named "Hardcoded AWS Keys in CI/CD" or "Layoff Strategy Q3". Start a stopwatch on every juicy issue you find. You don't need to log time; you just need to create the record in the DB.

Step 3: The Purge The admin removes your account from the organization. You verify via the UI: "404 Not Found" on the repository. The front door is locked.

Step 4: The Look-Back Query the API directly: GET /api/v1/user/stopwatches

The server responds with a JSON array containing the repo_name and issue_title for every stopwatch you started. You confirm the layoffs are happening. You see they haven't fixed the AWS keys yet. You have insider trading info, and you aren't even an insider anymore.

The Impact: Why Titles Matter

You might be thinking, "So what? I see a title. I don't see the code." Do not underestimate the value of metadata. In many organizations, issue titles contain high-level strategic information.

If I'm a competitor, knowing that you have an issue titled "Emergency Patch for Zero-Day in Payment Gateway" tells me everything I need to know about your current stability. If I'm an attacker, seeing "Refactor SQL Injection in Login" tells me exactly where to point sqlmap.

Furthermore, this persistence violates the fundamental security principle of Complete Mediation. Every access to every object must be checked for authority. Gitea failed this check, allowing a dangling reference to become an unauthorized peephole.

The Fix: Double Tap

The remediation provided by the Gitea team in version 1.25.4 is robust because it attacks the problem from two angles—the view layer and the data layer.

First, as detailed in the code section, the API presentation layer now filters out results you can't see. This stops the immediate bleeding. But leaving unauthorized records in the database is just messy hygiene.

So, the second part of the fix involves Active Cleanup. They modified services/repository/collaboration.go and services/org/team.go. Now, when a user is removed from a repository or a team, the backend triggers a cleanup routine RemoveStopwatchesByRepoID. It nukes the stopwatch records entirely.

If you are running Gitea <= 1.25.3, you are vulnerable. Upgrade immediately. If you can't upgrade, you need to manually audit your stopwatch database table for users who shouldn't be there, but frankly, upgrading is easier than writing SQL queries to cross-reference permission tables.

Fix Analysis (1)

Technical Appendix

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

Affected Systems

Gitea self-hosted instancesGitea Docker containers

Affected Versions Detail

Product
Affected Versions
Fixed Version
Gitea
Gitea
<= 1.25.31.25.4
AttributeDetail
CVE IDCVE-2026-20883
CWECWE-284 (Improper Access Control)
CVSS Score6.5 (Medium)
Attack VectorNetwork (API)
Fix Version1.25.4
ImpactInformation Disclosure
CWE-284
Improper Access Control

The software does not properly restrict access to a resource from an unauthorized actor.

Vulnerability Timeline

Fix committed to main branch
2026-01-13
Gitea 1.25.4 Released
2026-01-22
CVE-2026-20883 Published
2026-01-22

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.