CVE-2026-91982: Vikunja TOTP Secret Is Readable After Vulnerability

MODERATE
Advisory Identifier: GHSA-88f6-4rjv-x774
CWE-312 CWE-522
Published
Advisory Summary

CVE-2026-91982 is a MODERATE totp secret is readable after vulnerability (CWE-312: Cleartext Storage of Sensitive Information) affecting Vikunja libraries in the Go ecosystem. The issue involves totp secret is readable after enrollment, no step-up auth. Remediation is available by upgrading affected packages to version 2.6.0.

CVE-2026-91982 at a Glance

CVE Identifier CVE-2026-91982
GHSA Identifier GHSA-88f6-4rjv-x774
Severity MODERATE
CVSS v3.1 Base Score N/A
CWE Classification CWE-312: Cleartext Storage of Sensitive Information
Affected Product Vikunja
Package Ecosystem Go
Published Date
Last Updated

How to Fix CVE-2026-91982

To remediate this vulnerability, upgrade all affected Vikunja dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

code.vikunja.io/api fixed: 2.6.0
Upgrade code.vikunja.io/api to 2.6.0
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for code.vikunja.io/api

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-91982, inspect your project dependencies for Vikunja:

inspect dependencies for code.vikunja.io/api

Analyze the terminal output to locate any version of Vikunja in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 2.6.0), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-312: Cleartext Storage of Sensitive Information, where the product stores sensitive information in cleartext within a resource that might be accessible to another control sphere. In Vikunja, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### Summary Once a user has TOTP enabled, the API still hands back the raw shared secret to anyone holding that account's access token. Reading it doesn't ask for the password, even though disabling TOTP does. So a stolen token, an XSS, or a browser left open is enough to copy the second factor into your own authenticator and keep generating valid codes indefinitely. ### Details `GET /api/v1/user/settings/totp` returns the full TOTP object, including the secret field and the otpauth:// provisioning URL. `GET /api/v1/user/settings/totp/qrcode` renders the same secret as a QR image. Neither requires re-authentication, the access token alone is enough. This is inconsistent with the rest of the flow: `POST /api/v1/user/settings/totp/disable` calls CheckUserPassword before it will turn TOTP off. So the destructive action is gated behind the password, but reading out the secret that backs the second factor isn't. There's also no reason for the secret to be readable at all once enrollment is finished, the client only needs it during setup. The fix is to stop returning secret/url/the QR code once enabled is true (only expose them during the enrollment window). Requiring the password on the read, like disable already does, would also be reasonable. ### PoC The attacker needs the victim's access token, from an XSS, a leaked/stolen token, or an unlocked session. TOTP must already be enabled on the account. 1. With the victim's bearer token, call: ``` GET /api/v1/user/settings/totp Authorization: Bearer <victim_token> ``` The response contains the shared secret and the otpauth:// URL: ``` {"secret": "<base32 secret>", "enabled": true, "url": "otpauth://totp/..."} ``` 2. Paste that secret (or scan `/api/v1/user/settings/totp/qrcode`) into any authenticator app. 3. It now produces the same 6-digit codes as the victim's device. Nothing is logged and the victim gets no notification. ### Impact This defeats the purpose of the second factor. 2FA is supposed to survive exactly this situation, a compromised password or a hijacked session, but a single read of the account's own settings hands over the shared secret with no step-up check and no trace. Combined with a known or later-recovered password, it's a durable account takeover that TOTP was meant to prevent.

CVE-2026-91982 Affected Vikunja Versions

1 component(s)
Fixed Versions:
2.6.0
Product / Package Ecosystem Affected Versions Fixed Version
code.vikunja.io/api
Go
< 2.6.0
2.6.0

References

Frequently Asked Questions

What is CVE-2026-91982?
CVE-2026-91982 is a security vulnerability in Vikunja involving TOTP secret is readable after enrollment, no step-up auth under CWE-312: Cleartext Storage of Sensitive Information.
Which Vikunja versions are affected?
Affected versions include releases prior to 2.6.0 across affected packages: code.vikunja.io/api.
What version fixes CVE-2026-91982?
Upgrading to 2.6.0 (or the latest patched release for your respective branch) resolves the vulnerability.
Is code.vikunja.io/api affected?
Yes, code.vikunja.io/api is affected in releases prior to 2.6.0. Upgrade your dependency to 2.6.0 or later.
What CWE is associated with CVE-2026-91982?
CVE-2026-91982 is classified under CWE-312: Cleartext Storage of Sensitive Information (The product stores sensitive information in cleartext within a resource that might be accessible to another control sphere.).
How can I check Go dependencies for CVE-2026-91982?
Run the dependency tree command: 'inspect dependencies for code.vikunja.io/api'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-88f6-4rjv-x774",
  "modified" : "2026-10-09T20:51:17Z",
  "published" : "2026-10-09T20:51:17Z",
  "aliases" : [ "CVE-2026-91982" ],
  "summary" : "Vikunja: TOTP secret is readable after enrollment, no step-up auth",
  "details" : "### Summary\nOnce a user has TOTP enabled, the API still hands back the raw shared secret to anyone holding that account's access token. Reading it doesn't ask for the password, even though disabling TOTP does. So a stolen token, an XSS, or a browser left open is enough to copy the second factor into your own authenticator and keep generating valid codes indefinitely.\n\n### Details\n`GET /api/v1/user/settings/totp` returns the full TOTP object, including the secret field and the otpauth:// provisioning URL. `GET /api/v1/user/settings/totp/qrcode` renders the same secret as a QR image. Neither requires re-authentication, the access token alone is enough.\n\nThis is inconsistent with the rest of the flow: `POST /api/v1/user/settings/totp/disable` calls CheckUserPassword before it will turn TOTP off. So the destructive action is gated behind the password, but reading out the secret that backs the second factor isn't. There's also no reason for the secret to be readable at all once enrollment is finished, the client only needs it during setup.\n\nThe fix is to stop returning secret/url/the QR code once enabled is true (only expose them during the enrollment window). Requiring the password on the read, like disable already does, would also be reasonable.\n\n### PoC\nThe attacker needs the victim's access token, from an XSS, a leaked/stolen token, or an unlocked session. TOTP must already be enabled on the account.\n\n1. With the victim's bearer token, call:\n```\nGET /api/v1/user/settings/totp\nAuthorization: Bearer <victim_token>\n```\nThe response contains the shared secret and the otpauth:// URL:\n```\n{\"secret\": \"<base32 secret>\", \"enabled\": true, \"url\": \"otpauth://totp/...\"}\n```\n2. Paste that secret (or scan `/api/v1/user/settings/totp/qrcode`) into any authenticator app.\n3. It now produces the same 6-digit codes as the victim's device. Nothing is logged and the victim gets no notification.\n\n### Impact\nThis defeats the purpose of the second factor. 2FA is supposed to survive exactly this situation, a compromised password or a hijacked session, but a single read of the account's own settings hands over the shared secret with no step-up check and no trace. Combined with a known or later-recovered password, it's a durable account takeover that TOTP was meant to prevent.",
  "severity" : [ {
    "type" : "CVSS_V4",
    "score" : "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Go",
      "name" : "code.vikunja.io/api"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "2.6.0"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 2.5.0"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/go-vikunja/vikunja/security/advisories/GHSA-88f6-4rjv-x774"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-91982"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/go-vikunja/vikunja/pull/3688"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/go-vikunja/vikunja"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/go-vikunja/vikunja/releases/tag/v2.6.0"
  }, {
    "type" : "WEB",
    "url" : "https://www.vulncheck.com/advisories/vikunja-before-2.6.0-totp-secret-disclosure-via-api"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-312", "CWE-522" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-10-09T20:51:17Z",
    "nvd_published_at" : null
  }
}