Jan 24, 2026·6 min read·53 visits
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.
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 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.
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?"
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.
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 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.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Gitea Gitea | <= 1.25.3 | 1.25.4 |
| Attribute | Detail |
|---|---|
| CVE ID | CVE-2026-20883 |
| CWE | CWE-284 (Improper Access Control) |
| CVSS Score | 6.5 (Medium) |
| Attack Vector | Network (API) |
| Fix Version | 1.25.4 |
| Impact | Information Disclosure |
The software does not properly restrict access to a resource from an unauthorized actor.
An improper input validation vulnerability (CWE-20) exists in the Entity Syntax Tree (EST) JSON deserialization component of the Cedar Policy Language (`cedar-policy` and `@cedar-policy/cedar-wasm`). When parsing JSON-formatted policy templates that define `is ... in` scope constraints on `principal` or `resource`, the parser used wildcard pattern matching that ignored the provided slot identifier (`?principal` or `?resource`). This omission allows malformed JSON templates to bypass initial deserialization validation, resulting in either a runtime panic during template linking or inverted slot bindings that alter authorization outcomes.
The @eeacms/volto-chatbot package for Plone Volto contains an unauthenticated confused deputy vulnerability (CWE-862) in versions 2.0.3 and earlier. The package registers Express middleware on wildcard proxy routes (`**/_v1_da/**` and `**/_v1_ha/**`) without verifying client authentication or restricting target endpoints. When processing incoming HTTP requests, the middleware automatically attaches server-side administrative credentials before forwarding the requests to upstream Danswer (Onyx) or Halloumi (LLMGW) API servers. This allows unauthenticated network attackers to execute arbitrary administrative API calls against the backend infrastructure.
CVE-2026-9705 is an insufficient session expiration and authorization bypass vulnerability in Keycloak and Red Hat build of Keycloak. The vulnerability occurs in the OAuth 2.0 Dynamic Client Registration Management Protocol implementation, where the authentication handler fails to verify whether a target client is enabled before accepting requests authenticated with a previously issued Registration Access Token (RAT). Consequently, an actor possessing a valid RAT for an administratively disabled client can query, update, and re-enable the client configuration, restoring revoked access to realm resources.
CVE-2026-9800 is an authorization bypass vulnerability affecting the Keycloak Policy Enforcer (org.keycloak:keycloak-policy-enforcer), which is integrated into Red Hat build of Keycloak, Red Hat build of Quarkus, Red Hat Single Sign-On 7, and Red Hat JBoss Enterprise Application Platform Expansion Pack. The flaw stems from improper comparison logic in the PolicyEnforcer.isDefaultAccessDeniedUri method, which uses loose substring matching on the full raw request URI rather than exact, normalized path comparisons against the configured access-denied landing page. Consequently, an authenticated attacker possessing any valid low-privileged token can bypass role-based, scope-based, and User-Managed Access (UMA) policy evaluations by embedding the access-denied path segment into the URL path or query parameters.
CVE-2026-9799 is an authorization bypass vulnerability in the User-Managed Access (UMA) subsystem of Keycloak (org.keycloak.authorization). When a resource server is configured with the non-default PERMISSIVE policy enforcement mode, an authenticated user holding a valid UMA permission ticket for a single typed resource can obtain an authorization Requesting Party Token (RPT) granting access to all resources of the same type across the resource server. By submitting an authorization request with the type selector prefix 'resource-type-any:<type>', the attacker bypasses instance-level owner approval checks for unshared peer resources that lack explicit static administrative policies.
CVE-2026-9083 is a path traversal and information disclosure vulnerability (CWE-22) affecting Keycloak and the Red Hat build of Keycloak. The vulnerability resides in the Java Keystore key provider component (JavaKeystoreKeyProviderFactory), which fails to restrict the user-supplied keystore file path to a designated directory. An authenticated realm administrator holding the 'manage-realm' role can supply arbitrary absolute or relative filesystem paths when configuring a key provider. Because the application returns distinct error messages depending on whether the target path does not exist, lacks read permissions, or fails keystore parsing, an attacker can use this component as an oracle to probe the underlying host filesystem.