Gogs 2FA Bypass: The Universal Skeleton Key in Your Git Server
Feb 6, 2026·5 min read·3 visits
Executive Summary (TL;DR)
Gogs failed to verify ownership of 2FA recovery codes. An attacker with your password can log in to your account using *their* recovery codes. Patch to 0.13.4 immediately.
A critical logic error in the Gogs self-hosted Git service allows attackers to bypass Two-Factor Authentication (2FA) by using recovery codes belonging to a different account. If an attacker possesses a victim's primary credentials (username/password), they can successfully authenticate as the victim by supplying a recovery code generated from an account they control. This effectively neutralizes the 2FA protection layer, turning a credential leak into a full account takeover.
The Hook: Security Theater
We often treat Two-Factor Authentication (2FA) like a magic shield. You lose your password in a breach? No problem, the hacker doesn't have your phone or your YubiKey. You're safe. But in software engineering, security features are only as good as the logic implementing them. In the case of Gogs, a popular self-hosted Go Git service, that logic was flawed in the most comedic way possible.
Imagine a bank vault that requires a key to open. You show up, claiming to be the owner of Vault 101. The guard asks for your key. You hand them the key to your own house. The guard looks at the key, checks a list, and says, "Yep, that's a valid key that exists in the world," and opens Vault 101 for you. That is exactly what is happening here.
CVE-2025-64175 isn't a complex memory corruption bug or a mathematical break in cryptography. It's a fundamental logic error where the application forgot to ask whose key was being used.
The Flaw: A Case of Missing Identity
The root cause resides in the authentication flow for recovery codes. When a user loses access to their TOTP app (like Google Authenticator), they can use a one-time recovery code. The backend needs to verify two things: 1) Is this code valid and unused? 2) Does this code belong to the user trying to log in?
Gogs nailed the first part and completely ghosted the second. In the internal/db/two_factor.go file (or models in older structures), the function responsible for validating the code accepted a userID, but—and this is the kicker—it explicitly ignored it.
The database query was constructed to search the entire two_factor_recovery_code table for a matching string. If it found any row in the table that matched the code provided by the attacker, it returned success. It didn't matter if that code belonged to the admin, a random user, or the attacker themselves.
The Code: The Smoking Gun
Let's look at the Go code that caused this mess. In Go, the underscore _ is used as a blank identifier to ignore values. Usually, this is fine for unused return values. Here, it was used on the input argument userID. That should have been the first red flag during code review.
Vulnerable Code:
// UseRecoveryCode validates recovery code of given user and marks it is used if valid.
func UseRecoveryCode(_ int64, code string) error {
recoveryCode := new(TwoFactorRecoveryCode)
// CRITICAL BUG: No filter for user_id!
// It only checks if the code string exists and hasn't been used yet.
has, err := x.Where("code = ?", code).And("is_used = ?", false).Get(recoveryCode)
if err != nil {
return fmt.Errorf("get unused code: %v", err)
} else if !has {
return ErrTwoFactorRecoveryCodeNotFound{Code: code}
}
// ... marks code as used and returns success
}See the _ int64? That's the function saying, "I'm taking a User ID, but I promise not to look at it." The ORM query x.Where("code = ?", code) scans the global table. Since recovery codes are often random strings, collisions are rare, but that's irrelevant here. The attacker doesn't need to guess your code; they just need to provide a valid code that the database knows about.
The Exploit: Bring Your Own Key (BYOK)
To exploit this, an attacker needs two things: the victim's password (easy enough via phishing, credential stuffing, or reuse) and an account on the same Gogs instance (also easy if registration is open, or if the attacker is a rogue insider/contractor).
Here is the kill chain:
- Preparation: The attacker logs into their own account and enables 2FA. They generate a list of recovery codes. Let's say one of them is
deadbeef-1337. - Infiltration: The attacker goes to the login page and enters the victim's username and password.
- The Bypass: The system prompts for 2FA. The attacker clicks "Use Recovery Code".
- Execution: The attacker types in
deadbeef-1337(their own code). - Access Granted: The backend queries the DB: "Is
deadbeef-1337a valid, unused code?" The DB replies "Yes". The system logs the attacker into the victim's session.
The attacker has now bypassed the 2FA protection entirely. The system marks the attacker's recovery code as used, which is a small price to pay for full admin access.
The Impact: From Bypass to Supply Chain Attack
This is a "High" severity issue for a reason. Gogs is used to host source code. If an attacker compromises a developer's account, they don't just steal data; they can modify it.
With this bypass, an attacker can:
- Inject Backdoors: Push malicious code into production repositories.
- Delete History: Wipe repositories or rewrite git history to hide tracks.
- Steal Secrets: Access private repos containing API keys or deployment credentials.
Since this bypass requires the primary password, it effectively downgrades the security of 2FA-enabled accounts to that of 1FA accounts. If your password leaked, your 2FA saves you. With CVE-2025-64175, it doesn't.
The Fix: One Line of SQL
The remediation is straightforward: actually use the userID that was passed to the function. The developers patched this in version 0.13.4 (and 0.14.0+dev) by adding a constraint to the query.
Patched Code:
func (db *twoFactors) UseRecoveryCode(ctx context.Context, userID int64, code string) error {
return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var recoveryCode TwoFactorRecoveryCode
// FIX: Added "user_id = ?"
err := tx.Where("user_id = ? AND code = ? AND is_used = ?", userID, code, false).First(&recoveryCode).Error
// ...
})
}If you are running a self-hosted Gogs instance, you must upgrade immediately. If you cannot upgrade, disable open registration to prevent attackers from easily creating accounts to generate recovery codes, though this won't stop insider threats.
Official Patches
Fix Analysis (1)
Technical Appendix
CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:NAffected Systems
Affected Versions Detail
| Product | Affected Versions | Fixed Version |
|---|---|---|
Gogs Gogs | < 0.13.4 | 0.13.4 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-287 (Improper Authentication) |
| CVSS v4.0 | 7.7 (High) |
| Attack Vector | Network |
| Attack Complexity | Low |
| Privileges Required | Low (Any valid account on instance) |
| Impact | Full Account Takeover |
MITRE ATT&CK Mapping
Known Exploits & Detection
Vulnerability Timeline
Subscribe to updates
Get the latest CVE analysis reports delivered to your inbox.