Skip to main content

Gogs EUVDEUVD-2026-39079

| CVE-2026-52809 MEDIUM
Use of a Key Past its Expiration Date (CWE-324)
2026-06-23 https://github.com/gogs/gogs GHSA-5c3f-6486-3g7g
6.8
CVSS 3.1 · Vendor: https://github.com/gogs/gogs
View

Attack ChainAIDerived

Hypothetical attack flow derived from CVE metadata – not a confirmed exploit chain

  1. Recon
    Victim requests Gogs password reset
  2. Delivery
    Server generates token embedding ActivateCodeLives (e.g., 180 min)
  3. Exploit
    Reset email advertises shorter false expiry (e.g., 10 min)
  4. Install
    Attacker reads intercepted reset email after 10-min window closes
  5. C2
    Submits token to POST /user/reset_password
  6. Execute
    VerifyTimeLimitCode extracts embedded 180-min lifetime, accepts token
  7. Impact
    Attacker sets new password and achieves account takeover

Vulnerability Assessment

Weakness
CWE
OWASP Top 10:2025
A04:2025 - Cryptographic Failures
ASVS 5.0
V11 - Cryptography Key rotation and expiry
Exploitation Conditions

Exploitation requires two simultaneous conditions to hold: (1) the Gogs instance must be configured in app.ini with RESET_PASSWORD_CODE_LIVES set to a value strictly less than ACTIVATE_CODE_LIVES - instances where both values are equal or where RESET_PASSWORD_CODE_LIVES was never explicitly reduced below the default are not exploitable beyond the normal token window; and (2) the attacker must have obtained the victim's password-reset token from the reset email, most likely via a shared, forwarded, or otherwise compromised mailbox. The victim must have actively triggered a password-reset request (UI:R per CVSS). No Gogs account is required by the attacker (PR:N per CVSS vector). The exploitable window is specifically the time between RESET_PASSWORD_CODE_LIVES expiry and ACTIVATE_CODE_LIVES expiry; a default Gogs installation with both values at 180 minutes provides no exploitable delta whatsoever.

Impact

The CVSS 3.1 score of 6.8 (Medium, AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N) is well-calibrated: the AC:H rating reflects two mandatory simultaneous conditions that significantly constrain the realistic attacker population. No EPSS data was provided, and the vulnerability is not listed in CISA KEV, so exploitation probability from those sources is unknown. However, a fully functional proof-of-concept is embedded directly in the public GitHub Security Advisory, lowering the skill bar for anyone already in possession of an intercepted token. The practical risk is paradoxically highest in security-conscious deployments that explicitly reduced RESET_PASSWORD_CODE_LIVES for compliance - those administrators have a false sense of control while the enforcement gap is maximized. Default or misconfigured instances where both lifetime values happen to be equal present no exploitable delta. The C:H/I:H impact correctly reflects full account takeover capability once the two preconditions are met. No conflicting signals between CVSS and description were identified.

Exploit ScenarioDerived

An attacker with read access to a victim's email account - via a shared inbox, email forwarding rule, or compromised mail credentials - waits for the victim to trigger a Gogs password-reset request, then allows the administrator-configured RESET_PASSWORD_CODE_LIVES window (e.g., 10 minutes) to elapse so the victim believes the link is expired. The attacker then submits the intercepted token to POST /user/reset_password?code=<TOKEN> up to 170 minutes later; because the token embeds the 180-minute ACTIVATE_CODE_LIVES value, VerifyTimeLimitCode accepts it and updates the victim's password. A working proof-of-concept for this scenario is published in GitHub Security Advisory GHSA-5c3f-6486-3g7g.

This Month Plan within 30 days. Why: MEDIUM severity (CVSS 4.0-6.9).
Patch available 0.2% exploitation probability (EPSS)

Get an e-mail when this CVE is exploited, patched or re-scored. Free account.

Alert me

AnalysisAIv1 – Jun 23, 2026

Password-reset tokens in Gogs self-hosted Git service (versions before 0.14.3) remain valid for the full account-activation lifetime - up to 3 hours by default - regardless of the administrator-configured RESET_PASSWORD_CODE_LIVES setting, because GenerateActivateCode() hardcodes conf.Auth.ActivateCodeLives into the token at generation time. An attacker who obtains an intercepted reset token can exploit it far beyond the intended expiry window to perform full account takeover, exposing all private repositories and source code. No KEV listing at time of analysis, but publicly available exploit code exists within the GitHub Security Advisory GHSA-5c3f-6486-3g7g.

RemediationAI

Upgrade Gogs to version 0.14.3 or later; the patch is confirmed via GitHub PR #8328 (https://github.com/gogs/gogs/pull/8328) and commit 187e9c557930eb4a8b9b1502ee45cccf3255ee7f, which introduces a dedicated GenerateResetPasswordCode() function that correctly uses conf.Auth.ResetPasswordCodeLives as the token lifetime. Because VerifyTimeLimitCode reads the lifetime from the token itself, no change to the verification side is required - tokens generated by the fixed path will automatically expire at the correct configured time. If an immediate upgrade is not possible, set RESET_PASSWORD_CODE_LIVES equal to or greater than ACTIVATE_CODE_LIVES in app.ini; this eliminates the exploitable time-window delta but prevents enforcement of a shorter reset window, negating the compliance intent. A more restrictive compensating control is to disable self-service password reset entirely and require administrator intervention, which closes the attack surface completely but adds operational overhead for every reset request. Vendor advisory: https://github.com/gogs/gogs/security/advisories/GHSA-5c3f-6486-3g7g.

Affected VersionsENISA

gogs < 0.14.3

Affected Packages

go gogs.io/gogs fix: 0.14.3
euvd gogs fix: 0.14.3

Source: OSV (Open Source Vulnerabilities)

Affects your stack

Affects 2 packages in go, euvd

Checking this CVE against your own stacks and servers is part of the Basic plan and up.

Technical ContextAI · show details

Gogs is a self-hosted Git service written in Go (CPE: pkg:go/gogs.io/gogs). The root cause (CWE-324: Use of a Key Past Its Expiration Date; CWE-613: Insufficient Session Expiration) is a shared code path: GenerateActivateCode() in internal/userx/userx.go is called for both account activation and password reset, but it unconditionally embeds conf.Auth.ActivateCodeLives - not ResetPasswordCodeLives - as a 6-digit field at positions 12-17 of the token. The verification function VerifyTimeLimitCode() in internal/tool/tool.go ignores the minutes argument passed by its caller and instead re-extracts the lifetime from the token itself (minutes = d where d is parsed from positions 12-18), making any externally passed value irrelevant. ResetPasswordCodeLives only appears in email template rendering data and admin UI display - it has zero effect on actual token validation. The fix in PR #8328 introduces a dedicated GenerateResetPasswordCode() function backed by a shared generateTimeLimitedUserCode() helper that accepts the lifetime as a parameter, correctly passing conf.Auth.ResetPasswordCodeLives for the reset path.

Know when the next one hits your stack

  • Watch products and get an email when something new is published
  • Check this CVE against your own stack and servers
  • Exploit and PoC references, threat intelligence from 22 sources

No credit card · 7-day full trial

Severity by source

Vendor (https://github.com/gogs/gogs) PRIMARY 6.8 MEDIUM
AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N
vuln.today AI 6.8 MEDIUM

AC:H reflects the dual prerequisite of a misconfigured lifetime delta and prior token interception; C:H/I:H reflects full account takeover; A:N as no service disruption results.

3.1 AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N
4.0 AV:N/AC:H/AT:P/PR:N/UI:P/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N
SUSE MEDIUM

Primary rating from Vendor (https://github.com/gogs/gogs).

CVSS VectorVendor: https://github.com/gogs/gogs

Attack Vector
Network
Attack Complexity
High
Privileges Required
None
User Interaction
Required
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
None

DescriptionCVE.org

Summary

Password-reset tokens are generated using conf.Auth.ActivateCodeLives (the account-activation lifetime), not conf.Auth.ResetPasswordCodeLives. The token lifetime is baked into the token itself at generation time and is re-extracted from the token at verification time, making RESET_PASSWORD_CODE_LIVES irrelevant to actual enforcement. When an administrator configures a shorter reset window (e.g., 10 minutes) for compliance or security reasons, reset tokens remain exploitable for the full activation lifetime instead, while the reset email falsely advertises the shorter expiry.

Severity

Medium (CVSS 3.1: 6.8)

CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N

  • Attack Vector: Network - the reset endpoint is reachable over HTTP/S.
  • Attack Complexity: High - successful exploitation requires (1) the instance to be configured with RESET_PASSWORD_CODE_LIVES < ACTIVATE_CODE_LIVES, AND (2) the attacker to have intercepted the victim's reset token (e.g., from a compromised or shared email inbox).
  • Privileges Required: None - no Gogs account is required.
  • User Interaction: Required - the victim must have triggered a password-reset request.
  • Scope: Unchanged - the impact is confined to the victim's Gogs account.
  • Confidentiality Impact: High - successful exploitation leads to account takeover, exposing all private repositories and data.
  • Integrity Impact: High - the attacker can change the victim's password and gain full write access.
  • Availability Impact: None.

Affected component

  • internal/userx/userx.go - GenerateActivateCode() (line 39)
  • internal/email/email.go - SendResetPasswordMail() (line 132)
  • internal/route/user/auth.go - verifyUserActiveCode() (lines 426-439) and ResetPasswdPost() (line 621)

CWE

  • CWE-324: Use of a Key Past Its Expiration Date
  • CWE-613: Insufficient Session Expiration

Description

The reset token lifetime is hardcoded to ActivateCodeLives at generation

GenerateActivateCode (called for both account activation and password reset) bakes conf.Auth.ActivateCodeLives - not ResetPasswordCodeLives - into the token as a 6-digit field:

go
// internal/userx/userx.go:36-46
func GenerateActivateCode(userID int64, email, name, password, rands string) string {
    code := tool.CreateTimeLimitCode(
        fmt.Sprintf("%d%s%s%s%s", userID, email, strings.ToLower(name), password, rands),
        conf.Auth.ActivateCodeLives,   // ← always ActivateCodeLives, never ResetPasswordCodeLives
        nil,
    )
    code += hex.EncodeToString([]byte(strings.ToLower(name)))
    return code
}

CreateTimeLimitCode embeds the minutes value at positions 12-17 of the token:

Token format: YYYYMMDDHHMM (12) | 000180 (6-digit lives) | SHA1 (40) | hex-username

SendResetPasswordMail calls u.GenerateEmailActivateCode(u.Email()) - which resolves to GenerateActivateCode - with no option to pass a different lifetime:

go
// internal/email/email.go:131-132
func SendResetPasswordMail(c *macaron.Context, u User) error {
    return SendUserMail(c, u, tmplAuthResetPassword, u.GenerateEmailActivateCode(u.Email()), ...)
}

ResetPasswordCodeLives is used only for display, not enforcement

VerifyTimeLimitCode discards the minutes argument and re-extracts the lifetime directly from the token itself:

go
// internal/tool/tool.go:62-86
func VerifyTimeLimitCode(data string, minutes int, code string) bool {
    start := code[:12]
    lives := code[12:18]
    if d, err := strconv.Atoi(lives); err == nil {
        minutes = d    // ← argument overridden by value baked into the token
    }
    retCode := CreateTimeLimitCode(data, minutes, start)
    if retCode == code && minutes > 0 {
        before, _ := time.ParseInLocation("200601021504", start, time.Local)
        if before.Add(time.Minute * time.Duration(minutes)).Unix() > now.Unix() {
            return true
        }
    }
    return false
}

The verifyUserActiveCode caller passes conf.Auth.ActivateCodeLives as minutes, but it makes no difference:

go
// internal/route/user/auth.go:426-439
func verifyUserActiveCode(code string) (user *database.User) {
    minutes := conf.Auth.ActivateCodeLives   // passed to VerifyTimeLimitCode but immediately overridden
    if user = parseUserFromCode(code); user != nil {
        prefix := code[:tool.TimeLimitCodeLength]
        data := strconv.FormatInt(user.ID, 10) + user.Email + user.LowerName + user.Password + user.Rands
        if tool.VerifyTimeLimitCode(data, minutes, prefix) {
            return user
        }
    }
    return nil
}

ResetPasswdPost validates the reset token through verifyUserActiveCode, so it inherits the same flaw:

go
// internal/route/user/auth.go:621
if u := verifyUserActiveCode(code); u != nil {

ResetPasswordCodeLives appears only in email template data and in the admin config display - it has zero effect on actual token validation:

go
// internal/email/email.go:109 - template data only, not used to generate the token
"ResetPwdCodeLives": conf.Auth.ResetPasswordCodeLives / 60,

Full execution chain

  1. Victim requests reset: POST /user/forget_password → SendResetPasswordMail generates a token embedding ActivateCodeLives = 180 at bytes 12-17.
  2. Email delivered: The reset email says "link valid for 10 minutes" (from ResetPwdCodeLives in the template) but the embedded lifetime is 180.
  3. RESET_PASSWORD_CODE_LIVES window closes: After 10 minutes the victim believes the link has expired.
  4. Attacker submits the token: POST /user/reset_password?code=<TOKEN> → ResetPasswdPost → verifyUserActiveCode → VerifyTimeLimitCode extracts 000180 from the token → confirms the token has not yet reached the 180-minute mark → returns the user object → password is updated.
  5. Account takeover: Attacker sets a new password and authenticates as the victim.

Proof of Concept

ini
# app.ini configuration that exposes the bug:
[auth]
ACTIVATE_CODE_LIVES = 180
RESET_PASSWORD_CODE_LIVES = 10
bash
# 1) Request password reset for victim account
curl -i -X POST -d 'email=victim@example.com' http://HOST/user/forget_password
# 2) Obtain the reset link from the email.
#    Wait 11 minutes (past RESET_PASSWORD_CODE_LIVES, within ACTIVATE_CODE_LIVES).
# 3) Submit the "expired" reset code - it still succeeds
curl -i -X POST \
  -d 'code=<CODE_FROM_EMAIL>&password=AttackerNewPass' \
  'http://HOST/user/reset_password?code=<CODE_FROM_EMAIL>'
# Expected: HTTP 302 redirect to /user/login - password successfully changed
# despite the reset window having "closed" 10 minutes ago.

Impact

  • An administrator who sets RESET_PASSWORD_CODE_LIVES shorter than ACTIVATE_CODE_LIVES to limit the window of exposure for intercepted reset emails gets no security benefit from that configuration.
  • Reset tokens remain valid for the full activation lifetime (default 3 hours), giving an attacker who has intercepted a reset email a much larger window to use it.
  • The reset email actively misleads users by advertising a shorter expiry that is never enforced.
  • All password-reset operations are affected; there is no per-user or per-request way to issue a correctly-expiring token.

Recommended remediation

Option 1: Add a ResetPasswordCodeLives-aware generation function (preferred)

Introduce a dedicated code-generation path that passes conf.Auth.ResetPasswordCodeLives instead of ActivateCodeLives:

go
// internal/userx/userx.go
func GenerateResetPasswordCode(userID int64, email, name, password, rands string) string {
    code := tool.CreateTimeLimitCode(
        fmt.Sprintf("%d%s%s%s%s", userID, email, strings.ToLower(name), password, rands),
        conf.Auth.ResetPasswordCodeLives,   // ← correct lifetime
        nil,
    )
    code += hex.EncodeToString([]byte(strings.ToLower(name)))
    return code
}

Update email.User to expose this through the interface:

go
// internal/email/email.go interface
GenerateResetPasswordCode(email string) string

Update SendResetPasswordMail to call it:

go
func SendResetPasswordMail(c *macaron.Context, u User) error {
    return SendUserMail(c, u, tmplAuthResetPassword, u.GenerateResetPasswordCode(u.Email()), ...)
}

Because VerifyTimeLimitCode reads the lifetime from the token itself, no change to the verification side is required - tokens generated with ResetPasswordCodeLives will automatically expire at the correct time.

Option 2: Validate the extracted lifetime against the configured maximum

Add a post-extraction check in VerifyTimeLimitCode or in the reset-specific verification function to reject tokens whose embedded lifetime exceeds ResetPasswordCodeLives:

go
// in verifyUserActiveCode, after extracting the prefix:
embeddedLives := ... // parse positions 12-18 of the code
if embeddedLives > conf.Auth.ResetPasswordCodeLives {
    return nil  // reject tokens with a longer-than-allowed lifetime
}

This is a defence-in-depth measure but does not fix the root cause; Option 1 is preferred.

Credit

This vulnerability was discovered and reported by bugbunny.ai.

Threat Intelligence Overview

EPSS Score
0.2%
Top 90% exploitable
Prevalence
Low
niche self-hosted Git alternative, smaller footprint than Gitea or GitLab
Threat Intel
1
1 source
Social Media
1
Sightings
Data Quality
5 sources
Confidence: Medium

How this decision was reached

The engine reads each signal below and applies a fixed list of rules from top to bottom. The first rule that matches sets the recommended action – like a statement, every line is shown.

SignalValue
Severity MEDIUM
CISA KEV No
Public POC No
EPSS 0.2%
Patch available Yes
Active threat No
Deciding rule
MEDIUM severity (CVSS 4.0-6.9)
What every action level means
Emergency Immediate action required. Criteria: CRITICAL + in CISA KEV (actively exploited) without patch, OR CRITICAL + EPSS > 30% without patch, OR CRITICAL + active threat signals without patch.
Act Now Prioritize within 24-48h. Criteria: in KEV with patch available, OR CRITICAL + public POC, OR HIGH + EPSS > 10%, OR CRITICAL + active threat, OR any CRITICAL severity.
This Week Schedule remediation this week. Criteria: HIGH severity (CVSS 7.0-8.9). Patch within normal change window.
This Month Plan within 30 days. Criteria: MEDIUM severity (CVSS 4.0-6.9), lower exploitation probability. Include in regular patch cycle.
Monitor Track and reassess. Criteria: LOW severity (CVSS 0.1-3.9), minimal exploitation risk. No immediate action needed.

SSVC AssessmentENISA

SSVC Decision· derived
Scheduled
Exploitation· derived
None
System Exposure· derived
Open
Automatable· derived
No
Human Impact· derived
Medium

Priority Score

34
Low Medium High Critical
KEV: 0
EPSS: 0
CVSS: +34
PoC: 0
Weaponized: 0
Threat intel: 0
Gov advisory: 0

Attack Technique

Information disclosure occurs when an application unintentionally exposes sensitive data that aids attackers in reconnaissance or directly compromises security.

Vendor StatusVendor

SUSE

Severity: Moderate
Product Status
SUSE Linux Enterprise Server 16.1 Affected
SUSE Linux Enterprise Server for SAP applications 16.1 Affected
SUSE Linux Enterprise Module for Package Hub 15 SP5 Affected
SUSE Linux Enterprise Module for Package Hub 15 SP6 Affected
openSUSE Leap 15.5 Affected

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy