Files
vuln-osv/data/Go/GHSA-5c3f-6486-3g7g.json
T

1 line
11 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"schema_version":"1.9.0","id":"GHSA-5c3f-6486-3g7g","published":"2026-06-23T17:03:25Z","modified":"2026-07-21T13:30:31.752876950Z","aliases":["CVE-2026-52809","GO-2026-5140"],"summary":"Gogs's password-reset tokens use account-activation lifetime, ignoring RESET_PASSWORD_CODE_LIVES","details":"## Summary\n\nPassword-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.\n\n## Severity\n\n**Medium** (CVSS 3.1: 6.8)\n\n`CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N`\n\n- **Attack Vector:** Network — the reset endpoint is reachable over HTTP/S.\n- **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).\n- **Privileges Required:** None — no Gogs account is required.\n- **User Interaction:** Required — the victim must have triggered a password-reset request.\n- **Scope:** Unchanged — the impact is confined to the victim's Gogs account.\n- **Confidentiality Impact:** High — successful exploitation leads to account takeover, exposing all private repositories and data.\n- **Integrity Impact:** High — the attacker can change the victim's password and gain full write access.\n- **Availability Impact:** None.\n\n## Affected component\n\n- `internal/userx/userx.go` — `GenerateActivateCode()` (line 39)\n- `internal/email/email.go` — `SendResetPasswordMail()` (line 132)\n- `internal/route/user/auth.go` — `verifyUserActiveCode()` (lines 426–439) and `ResetPasswdPost()` (line 621)\n\n## CWE\n\n- **CWE-324**: Use of a Key Past Its Expiration Date\n- **CWE-613**: Insufficient Session Expiration\n\n## Description\n\n### The reset token lifetime is hardcoded to `ActivateCodeLives` at generation\n\n`GenerateActivateCode` (called for both account activation and password reset) bakes `conf.Auth.ActivateCodeLives` — not `ResetPasswordCodeLives` — into the token as a 6-digit field:\n\n```go\n// internal/userx/userx.go:36-46\nfunc GenerateActivateCode(userID int64, email, name, password, rands string) string {\n code := tool.CreateTimeLimitCode(\n fmt.Sprintf(\"%d%s%s%s%s\", userID, email, strings.ToLower(name), password, rands),\n conf.Auth.ActivateCodeLives, // ← always ActivateCodeLives, never ResetPasswordCodeLives\n nil,\n )\n code += hex.EncodeToString([]byte(strings.ToLower(name)))\n return code\n}\n```\n\n`CreateTimeLimitCode` embeds the `minutes` value at positions 12–17 of the token:\n\n```\nToken format: YYYYMMDDHHMM (12) | 000180 (6-digit lives) | SHA1 (40) | hex-username\n```\n\n`SendResetPasswordMail` calls `u.GenerateEmailActivateCode(u.Email())` — which resolves to `GenerateActivateCode` — with no option to pass a different lifetime:\n\n```go\n// internal/email/email.go:131-132\nfunc SendResetPasswordMail(c *macaron.Context, u User) error {\n return SendUserMail(c, u, tmplAuthResetPassword, u.GenerateEmailActivateCode(u.Email()), ...)\n}\n```\n\n### `ResetPasswordCodeLives` is used only for display, not enforcement\n\n`VerifyTimeLimitCode` discards the `minutes` argument and re-extracts the lifetime directly from the token itself:\n\n```go\n// internal/tool/tool.go:62-86\nfunc VerifyTimeLimitCode(data string, minutes int, code string) bool {\n start := code[:12]\n lives := code[12:18]\n if d, err := strconv.Atoi(lives); err == nil {\n minutes = d // ← argument overridden by value baked into the token\n }\n retCode := CreateTimeLimitCode(data, minutes, start)\n if retCode == code && minutes > 0 {\n before, _ := time.ParseInLocation(\"200601021504\", start, time.Local)\n if before.Add(time.Minute * time.Duration(minutes)).Unix() > now.Unix() {\n return true\n }\n }\n return false\n}\n```\n\nThe `verifyUserActiveCode` caller passes `conf.Auth.ActivateCodeLives` as `minutes`, but it makes no difference:\n\n```go\n// internal/route/user/auth.go:426-439\nfunc verifyUserActiveCode(code string) (user *database.User) {\n minutes := conf.Auth.ActivateCodeLives // passed to VerifyTimeLimitCode but immediately overridden\n if user = parseUserFromCode(code); user != nil {\n prefix := code[:tool.TimeLimitCodeLength]\n data := strconv.FormatInt(user.ID, 10) + user.Email + user.LowerName + user.Password + user.Rands\n if tool.VerifyTimeLimitCode(data, minutes, prefix) {\n return user\n }\n }\n return nil\n}\n```\n\n`ResetPasswdPost` validates the reset token through `verifyUserActiveCode`, so it inherits the same flaw:\n\n```go\n// internal/route/user/auth.go:621\nif u := verifyUserActiveCode(code); u != nil {\n```\n\n`ResetPasswordCodeLives` appears only in email template data and in the admin config display — it has zero effect on actual token validation:\n\n```go\n// internal/email/email.go:109 — template data only, not used to generate the token\n\"ResetPwdCodeLives\": conf.Auth.ResetPasswordCodeLives / 60,\n```\n\n### Full execution chain\n\n1. **Victim requests reset**: `POST /user/forget_password` → `SendResetPasswordMail` generates a token embedding `ActivateCodeLives = 180` at bytes 12–17.\n2. **Email delivered**: The reset email says \"link valid for 10 minutes\" (from `ResetPwdCodeLives` in the template) but the embedded lifetime is 180.\n3. **`RESET_PASSWORD_CODE_LIVES` window closes**: After 10 minutes the victim believes the link has expired.\n4. **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.\n5. **Account takeover**: Attacker sets a new password and authenticates as the victim.\n\n## Proof of Concept\n\n```ini\n# app.ini configuration that exposes the bug:\n[auth]\nACTIVATE_CODE_LIVES = 180\nRESET_PASSWORD_CODE_LIVES = 10\n```\n\n```bash\n# 1) Request password reset for victim account\ncurl -i -X POST -d 'email=victim@example.com' http://HOST/user/forget_password\n\n# 2) Obtain the reset link from the email.\n# Wait 11 minutes (past RESET_PASSWORD_CODE_LIVES, within ACTIVATE_CODE_LIVES).\n\n# 3) Submit the \"expired\" reset code — it still succeeds\ncurl -i -X POST \\\n -d 'code=<CODE_FROM_EMAIL>&password=AttackerNewPass' \\\n 'http://HOST/user/reset_password?code=<CODE_FROM_EMAIL>'\n\n# Expected: HTTP 302 redirect to /user/login — password successfully changed\n# despite the reset window having \"closed\" 10 minutes ago.\n```\n\n## Impact\n\n- 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.\n- 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.\n- The reset email actively misleads users by advertising a shorter expiry that is never enforced.\n- All password-reset operations are affected; there is no per-user or per-request way to issue a correctly-expiring token.\n\n## Recommended remediation\n\n### Option 1: Add a `ResetPasswordCodeLives`-aware generation function (preferred)\n\nIntroduce a dedicated code-generation path that passes `conf.Auth.ResetPasswordCodeLives` instead of `ActivateCodeLives`:\n\n```go\n// internal/userx/userx.go\nfunc GenerateResetPasswordCode(userID int64, email, name, password, rands string) string {\n code := tool.CreateTimeLimitCode(\n fmt.Sprintf(\"%d%s%s%s%s\", userID, email, strings.ToLower(name), password, rands),\n conf.Auth.ResetPasswordCodeLives, // ← correct lifetime\n nil,\n )\n code += hex.EncodeToString([]byte(strings.ToLower(name)))\n return code\n}\n```\n\nUpdate `email.User` to expose this through the interface:\n\n```go\n// internal/email/email.go interface\nGenerateResetPasswordCode(email string) string\n```\n\nUpdate `SendResetPasswordMail` to call it:\n\n```go\nfunc SendResetPasswordMail(c *macaron.Context, u User) error {\n return SendUserMail(c, u, tmplAuthResetPassword, u.GenerateResetPasswordCode(u.Email()), ...)\n}\n```\n\nBecause `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.\n\n### Option 2: Validate the extracted lifetime against the configured maximum\n\nAdd a post-extraction check in `VerifyTimeLimitCode` or in the reset-specific verification function to reject tokens whose embedded lifetime exceeds `ResetPasswordCodeLives`:\n\n```go\n// in verifyUserActiveCode, after extracting the prefix:\nembeddedLives := ... // parse positions 12-18 of the code\nif embeddedLives > conf.Auth.ResetPasswordCodeLives {\n return nil // reject tokens with a longer-than-allowed lifetime\n}\n```\n\nThis is a defence-in-depth measure but does not fix the root cause; Option 1 is preferred.\n\n## Credit\n\nThis vulnerability was discovered and reported by [bugbunny.ai](https://bugbunny.ai).","affected":[{"package":{"name":"gogs.io/gogs","ecosystem":"Go","purl":"pkg:golang/gogs.io/gogs"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.14.3"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/06/GHSA-5c3f-6486-3g7g/GHSA-5c3f-6486-3g7g.json"}}],"references":[{"type":"WEB","url":"https://github.com/gogs/gogs/security/advisories/GHSA-5c3f-6486-3g7g"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-52809"},{"type":"WEB","url":"https://github.com/gogs/gogs/pull/8328"},{"type":"WEB","url":"https://github.com/gogs/gogs/commit/187e9c557930eb4a8b9b1502ee45cccf3255ee7f"},{"type":"PACKAGE","url":"https://github.com/gogs/gogs"},{"type":"WEB","url":"https://github.com/gogs/gogs/releases/tag/v0.14.3"}],"database_specific":{"cwe_ids":["CWE-324","CWE-613"],"github_reviewed":true,"github_reviewed_at":"2026-06-23T17:03:25Z","nvd_published_at":"2026-06-24T21:16:56Z","severity":"MODERATE"},"severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N"}]}