Feb 26, 2026·6 min read·13 visits
Vikunja versions prior to 2.0.0 allow persistent account takeover. Due to a lack of input validation, passwords could be reset to a single character. Worse, changing a password did not invalidate existing JSON Web Tokens (JWTs). An attacker with a stolen token remains logged in indefinitely, regardless of the victim's remediation attempts. Fix: Upgrade to v2.0.0 immediately.
CVE-2026-27575 represents a catastrophic failure in the authentication lifecycle of Vikunja, a popular self-hosted task management platform. The vulnerability is a two-headed beast: first, it allowed users (and attackers) to set passwords with a single character, bypassing security policies during updates. Second, and far more critical, it failed to invalidate active sessions upon password changes. This means an attacker who steals a session token retains permanent access to the victim's data, even after the victim explicitly resets their credentials to 'lock them out.' It is a classic case of stateless JWTs being deployed without a revocation strategy.
We love self-hosted software. There is something primal about owning your data, running your own infrastructure, and telling the big cloud providers to shove it. Vikunja has carved out a nice niche here as the slick, Go-based alternative to Trello or Todoist. It holds your life: your business plans, your personal goals, your grocery lists, and perhaps your sensitive API keys stored in task descriptions.
But here is the thing about 'rolling your own' auth: it is hard. Really hard. And when you decide to use stateless authentication because it scales better (spoiler: you are not Google, you do not need that scale), you walk into a minefield. CVE-2026-27575 is what happens when you step on two mines at once.
This isn't just a 'bypass'. This is a fundamental misunderstanding of how session lifecycles should interact with credential management. It turns your private task manager into a public bulletin board for anyone who grabbed your token three weeks ago.
The vulnerability is actually two distinct logic flaws that, when combined, create a perfect storm for persistence.
1. The Screen Door Password Policy (CWE-521) When you register a new account in Vikunja, the bouncer checks your ID. You need 8 characters, special symbols, the blood of a virgin, etc. But prior to version 2.0.0, if you went to the Change Password or Reset Password settings, the bouncer was on a smoke break. The API endpoints for updating credentials simply forgot to apply the validation rules. You could change your password to "a". Literally just the letter 'a'. This makes brute-forcing a specific account trivial if you know they recently rotated credentials.
2. The Zombie Session (CWE-613) This is the meat of the exploit. Vikunja used long-lived, stateless JSON Web Tokens (JWTs). The problem with stateless JWTs is that once they are signed, they are valid until they expire. The server doesn't remember issuing them; it just trusts the signature.
So, what happens when a user gets hacked, panics, and changes their password? In a sane world, all existing sessions are nuked. In Vikunja's world (pre-v2.0.0), the database updated the password hash, but the attacker's existing JWT—signed with the server's secret—remained perfectly valid. The lock on the door was changed, but the attacker was already inside the house, sleeping on the couch.
Let's look at the code. In Go, we love struct tags for validation. It's declarative and clean. But it only works if you actually put the tags there.
Here is the UserPassword struct used in the update flow before the patch:
// BEFORE: The "Trust Me Bro" Struct
type UserPassword struct {
OldPassword string `json:"old_password"`
NewPassword string `json:"new_password"`
}Notice anything missing? There are no length checks. No complexity requirements. The handler would take NewPassword, bcrypt it, and shove it into the database.
Now, look at the patch in commit 89c17d3b. The developers realized they left the barn door open:
// AFTER: The "We Actually Check" Struct
type UserPassword struct {
OldPassword string `json:"old_password"`
NewPassword string `json:"new_password" valid:"minLength:8"` // <--- The Fix
}But the bigger code change happened in commit 25268530. The entire session handling mechanism had to be rewritten. They moved from pure stateless JWTs to a stateful model where the token is checked against a list of valid sessions in the database.
This allowed them to implement RevokeAllSessions(user), which effectively kills the zombie tokens.
Let's walk through a realistic attack scenario. You are the attacker. You have phished a Vikunja admin's credentials.
Step 1: Infiltration You log in using the stolen credentials. The server issues you a JWT valid for 30 days. You stash this token in your local storage or a raw HTTP client. You are now authenticated.
Step 2: The Setup Just for kicks, you use the broken password policy to set the user's password to "1". This ensures that if you ever do get kicked out, getting back in is a trivial brute-force exercise. But you don't stop there.
Step 3: The 'Remediation' The admin notices the password change notification. Panic ensues. They log in (using your password "1"), and immediately reset their password to a 50-character random string generated by their password manager. They sigh in relief, thinking they have secured the breach.
Step 4: The Prestige
You, the attacker, sip your coffee. You send a request to /api/v1/projects using the JWT you obtained in Step 1.
Response: 200 OK.
The server checks the signature. Valid. The server checks the expiration. Valid. The server does not check if the password has changed since the token was issued. You still have full read/write access to the entire instance. You can exfiltrate data, delete backups, or modify tasks to inject XSS payloads for other users.
The impact here is High Confidentiality and High Integrity with a side of psychological horror.
For a business using Vikunja, this means that once an account is compromised, it stays compromised. There is no 'kill switch'. Even deleting the user might not immediately stop requests depending on how the caching layer handles JWT validation (though usually, user existence is checked).
This vulnerability highlights the danger of implementing authentication without considering the full lifecycle of a session. It is not enough to just issue a token; you must have a mechanism to destroy it. By failing to couple password rotation with session invalidation, Vikunja effectively broke the contract of trust between the application and the user. When a user changes their password, they expect the session to be reset. Violating that expectation leads to persistent compromises that are incredibly difficult for a standard admin to debug.
The fix provided in version 2.0.0 is substantial. It is not just a patch; it is a refactor.
UserPassword and PasswordReset structs now enforce minLength:8 and other complexity requirements.UserChangePassword handler now includes a call to wipe all session records associated with that user ID.How to remediate:
If you are running the Docker container, you need to pull the specific tag:
docker pull vikunja/vikunja:2.0.0
# OR
docker pull vikunja/vikunja:latest> [!WARNING]
> This update includes database migrations. Backup your database before applying the container update. The move to stateful sessions involves schema changes.
If you cannot update immediately, your only mitigation is to manually edit the database to change the user's salt or secret (if accessible), or fully delete and recreate the compromised user account—though simply updating is far safer.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Vikunja Vikunja | < 2.0.0 | 2.0.0 |
| Attribute | Detail |
|---|---|
| CWE IDs | CWE-521 (Weak Password), CWE-613 (Insufficient Session Expiration) |
| CVSS Score | 9.1 (Critical) |
| Attack Vector | Network (API) |
| Privileges Required | None (for initial access via weak policy logic) |
| Exploit Status | PoC Available / Trivial |
| Patch Date | 2026-02-25 |