Breaking

Vulnerability Disclosure: Authentication Bypass in Vaultwarden versions < 1.32.5

During a penetration test for a customer, we briefly assessed Vaultwarden, an open-source online password safe. In June 2024, the German Federal Office for Information Security (BSI) published results1 of a static and dynamic test of the Vaultwarden server component. Therefore, only a partial source code audit was performed during our assessment. However, a quick look was needed to find some glaring issues with the authentication.

Vaultwarden

Vaultwarden is an alternative online password safe server to Bitwarden and exposes the same API so that Bitwarden clients can connect to the Vaultwarden server. Since Bitwarden has a Browser client and Mobile clients, they can all connect to Vaultwarden, too.

The Issue

One of the first locations to look at is the login endpoint since it is unauthenticated. The endpoint is /identity/connect/token and can be found in src/api/identity.rs. The endpoint handles multiple different kinds of login methods. Still, we were interested in the password login, which calls the _password_login function. This function is responsible for regular user password authentication, and the password check code was unexpectedly complicated.

// Check password
let password = data.password.as_ref().unwrap();
if let Some(auth_request_uuid) = data.auth_request.clone() {
    if let Some(auth_request) = AuthRequest::find_by_uuid(auth_request_uuid.as_str(), conn).await {
        if !auth_request.check_access_code(password) {
            err!(
                "Username or access code is incorrect. Try again",
                format!("IP: {}. Username: {}.", ip.ip, username),
                ErrorEvent {
                    event: EventType::UserFailedLogIn,
                }
            )
        }
    } else {
        err!(
            "Auth request not found. Try again.",
            format!("IP: {}. Username: {}.", ip.ip, username),
            ErrorEvent {
                event: EventType::UserFailedLogIn,
            }
        )
    }
} else if !user.check_valid_password(password) {
    err!(
        "Username or password is incorrect. Try again",
        format!("IP: {}. Username: {}.", ip.ip, username),
        ErrorEvent {
            event: EventType::UserFailedLogIn,
        }
    )
}

One might notice the checks for an auth_request, in which case the password is checked with the auth request and not the user. However, the auth request is only looked up by the auth request UUID, and no link is made between the auth request and the user. So, the idea that came to mind was to create an auth_request for one user and use it to log in as another user.

The endpoint for creating an auth request is /api/auth-requests and handled in src/api/core/accounts.rs. It was actually unauthenticated meaning that an unauthenticated attacker can create an auth request to log in as another user.

Fixes

The first fix with commit 20d9e885 added the following code snippet to the /api/auth-requests handler.

// Validate device uuid and type
match Device::find_by_uuid_and_user(&data.device_identifier, &user.uuid, &mut conn).await {
    Some(device) if device.atype == client_headers.device_type => {}
    _ => err!("AuthRequest doesn't exist", "Device verification failed"),
}

This code checks that the requestor is in possession of a device_identifier of the user for which this auth request is intended. Since the device_identifier is a UUID, attackers are unlikely to get access to other users’ device identifiers. However, authenticated attackers can access their own device_identifier and can create auth requests for their own user. Since this fix is still missing any checks during the login that the auth request is associated in any way with the user who wants to log in, attackers can use their own auth request to still log into Vaultwarden as another user but with the restriction that they at least need an account first.

Then with commit 37c14c3c6 the problem was finally fixed.

let password = data.password.as_ref().unwrap();

// If we get an auth request, we don't check the user's password, but the access code of the auth request
if let Some(ref auth_request_uuid) = data.auth_request {
    let Some(auth_request) = AuthRequest::find_by_uuid(auth_request_uuid.as_str(), conn).await else {
        err!(
            "Auth request not found. Try again.",
            format!("IP: {}. Username: {}.", ip.ip, username),
            ErrorEvent {
                event: EventType::UserFailedLogIn,
            }
        )
    };

    // Delete the request after we used it
    auth_request.delete(conn).await?;

    if auth_request.user_uuid != user.uuid
        || !auth_request.approved.unwrap_or(false)
        || ip.ip.to_string() != auth_request.request_ip
        || !auth_request.check_access_code(password)
    {
        err!(
            "Username or access code is incorrect. Try again",
            format!("IP: {}. Username: {}.", ip.ip, username),
            ErrorEvent {
                event: EventType::UserFailedLogIn,
            }
        )
    }
} else if !user.check_valid_password(password) {
    err!(
        "Username or password is incorrect. Try again",
        format!("IP: {}. Username: {}.", ip.ip, username),
        ErrorEvent {
            event: EventType::UserFailedLogIn,
        }
    )
}

After the fix, the auth request is checked on login to belong to the user trying to log in. Since the previous changes require that an auth request can only be created for known devices and device UUIDs are not exposed, they prevent other users from creating auth requests for other users. Furthermore, the auth request must now be approved by the same authenticated user.

However, it should be noted that this authentication bypass could not bypass the second factor.

Vulnerability Impact

The vulnerability allows attackers to read, write, and delete user data. Since key material is stored encrypted on the server with the original password used for encryption, and we did not use the password to log in, it is not possible to decrypt the user’s key material. However, it might allow offline brute-force attacks, but the use of organizations was more interesting, as our customer was interested in using them.

A password is also required to store the organization’s key material in encrypted form. For this, organizations have one master key, which encrypts all the key material. This key is shared with other users when invited into an organization. However, the server might only give them access to some key material since it can be grouped into collections and allow restricting access. With the authentication bypass, it is now possible for an attacker in an organization to log into the applications as a higher-privileged user of the organization and read all the key material. Since the attacker is in the organization and has the master key, all the key material can be decrypted.

Further Issues

Two other problems were identified. First, an email HTML injection allows changing a username, which is afterward included in an emergency access invite to other users. When the manipulated username is included in the mail template, it is not escaped, which allows adding a body separator and a custom HTML body. Finally, a self XSS was possible with the breach check where the user could submit usernames reflected by the server and allow for HTML injection. However, we found that this can only be used on oneself and is also hindered by the content security policy.

Conclusion

This vulnerability shows how important a second factor can be. While some second factors might be weak (1 in a million chance), having a second factor to mitigate such problems is still good.

The quickly found security contact for Vaultwarden via a SECURITY.md and their fast reaction and efforts in fixing the vulnerabilities was remarkable to see even though the first fix was not sufficient. At time of publishing this blog post, CVEs were requested but still not yet assigned. We will update this blog post accordingly in the future.

We appreciate our customer allowing us to disclose the vulnerability to Vaultwarden and publish the details in this blog post. Their support helps improve the software and share important information with the community.

Cheers!

Nils

Timeline

  • End of October 2024: ERNW assesses Vaultwarden for the customer.
  • November 08, 2024: ERNW discloses the vulnerabilities to the Vaultwarden team.
  • November 10, 2024: Fix and release of Vaultwarden v1.32.4.
  • November 11, 2024: ERNW retests the software and identifies that the fix is not sufficient.
  • November 11, 2024: Public merge with fix and request for feedback by the Vaultwarden team.
  • November 12, 2024: ERNW acknowledges that the fix is complete.
  • November 18, 2024: Release of Vaultwarden v1.32.5.

  1. Bundesamt für Sicherheit in der Informationstechnik (BSI). Projekt 486 – Codeanalyse von Open Source Software (Projekt CAOS). Online: https://www.bsi.bund.de/DE/Service-Navi/Publikationen/Studien/Projekt_P486/projekt_P486_node.html. PDF: https://www.bsi.bund.de/SharedDocs/Downloads/DE/BSI/Publikationen/Studien/P486-Codeanalyse/Vaultwarden-Passwortmanager.pdf?__blob=publicationFile&v=5↩︎

Leave a Reply

Your email address will not be published. Required fields are marked *