VYPR
Critical severity9.4OSV Advisory· Published Aug 18, 2025· Updated Apr 15, 2026

CVE-2025-55299

CVE-2025-55299

Description

VaulTLS is a modern solution for managing mTLS (mutual TLS) certificates. Prior to 0.9.1, user accounts created through the User web UI have an empty but not NULL password set, attackers can use this to login with an empty password. This is combined with that fact, that previously disabling the password based login only effected the frontend, but still allowed login via the API. This vulnerability is fixed in 0.9.1.

Affected products

1

Patches

1
6ac0a43a768f

Fix password-based login

https://github.com/7ritn/vaultlsEmily EhlertAug 18, 2025via osv
7 files changed · +80 6
  • backend/Cargo.toml+1 1 modified
    @@ -1,6 +1,6 @@
     [package]
     name = "backend"
    -version = "0.9.0"
    +version = "0.9.1"
     edition = "2024"
     
     [[bin]]
    
  • backend/src/api.rs+22 3 modified
    @@ -55,8 +55,15 @@ pub(crate) async fn setup(
             return Err(ApiError::Other("Password is required".to_string()))
         }
     
    +    let trim_password = setup_req.password.as_deref().unwrap_or("").trim();
    +
    +    let password = match trim_password {
    +        "" => None,
    +        _ => Some(trim_password)
    +    };
    +
         let mut password_hash = None;
    -    if let Some(ref password) = setup_req.password {
    +    if let Some(password) = password {
             state.settings.set_password_enabled(true)?;
             password_hash = Some(Password::new_server_hash(password)?);
         }
    @@ -92,6 +99,10 @@ pub(crate) async fn login(
         jar: &CookieJar<'_>,
         login_req_opt: Json<LoginRequest>
     ) -> Result<(), ApiError> {
    +    if !state.settings.get_password_enabled() {
    +        warn!("Password login is disabled.");
    +        return Err(ApiError::Unauthorized(Some("Password login is disabled".to_string())))
    +    }
         let user: User = state.db.get_user_by_email(login_req_opt.email.clone()).await.map_err(|_| {
             warn!(user=login_req_opt.email, "Invalid email");
             ApiError::Unauthorized(Some("Invalid credentials".to_string()))
    @@ -151,6 +162,7 @@ pub(crate) async fn change_password(
     
         let password_hash = Password::new_server_hash(&change_pass_req.new_password)?;
         state.db.set_user_password(user_id, password_hash).await?;
    +    // todo unset
     
         info!(user=user.name, "Password Change: Success");
     
    @@ -440,8 +452,15 @@ pub(crate) async fn create_user(
         payload: Json<CreateUserRequest>,
         _authentication: AuthenticatedPrivileged
     ) -> Result<Json<i64>, ApiError> {
    -    let password_hash = match payload.password {
    -        Some(ref p) => Some(Password::new_server_hash(p)?),
    +    let trim_password = payload.password.as_deref().unwrap_or("").trim();
    +
    +    let password = match trim_password {
    +        "" => None,
    +        _ => Some(trim_password)
    +    };
    +
    +    let password_hash = match password {
    +        Some(p) => Some(Password::new_server_hash(p)?),
             None => None,
         };
     
    
  • backend/src/constants.rs+2 1 modified
    @@ -1,12 +1,13 @@
     use argon2::{Algorithm, Argon2, Params, Version};
    +use const_format::formatcp;
     use once_cell::sync::Lazy;
     
     pub(crate) const SETTINGS_FILE_PATH: &str = "settings.json";
     pub(crate) const DB_FILE_PATH: &str = "database.db3";
     pub(crate) const TEMP_DB_FILE_PATH: &str = "encrypted.db3";
     pub(crate) const CA_FILE_PATH: &str = "ca.cert";
     pub(crate) const API_PORT: u16 = 3737;
    -pub const VAULTLS_VERSION: &str = "v0.9.0";
    +pub const VAULTLS_VERSION: &str = formatcp!("v{}", env!("CARGO_PKG_VERSION"));
     
     #[cfg(not(test))]
     pub static ARGON2: Lazy<Argon2<'static>> = Lazy::new(|| {
    
  • backend/src/db.rs+27 0 modified
    @@ -144,6 +144,24 @@ impl VaulTLSDB {
             Ok(())
         }
     
    +    pub(crate) async fn fix_password(&self) -> Result<()> {
    +        let users = self.get_all_user().await?;
    +
    +        trace!("Checking for users with empty passwords");
    +
    +        for id in users.iter().map(|user| user.id) {
    +            let user = self.get_user(id).await?;
    +            if let Some(stored_password) = user.password_hash {
    +                if stored_password.verify("") {
    +                    // Password stored is empty
    +                    info!("Password for user {} is empty, disabling password", user.name);
    +                    self.unset_user_password(user.id).await?;
    +                }
    +            }
    +        }
    +        Ok(())
    +    }
    +
         /// Insert a new CA certificate into the database
         /// Adds id to the Certificate struct
         pub(crate) async fn insert_ca(
    @@ -375,6 +393,15 @@ impl VaulTLSDB {
             })
         }
     
    +    pub(crate) async fn unset_user_password(&self, id: i64) -> Result<()> {
    +        db_do!(self.pool, |conn: &Connection| {
    +            Ok(conn.execute(
    +                "UPDATE users SET password_hash = NULL WHERE id=?1",
    +                params![id]
    +            ).map(|_| ())?)
    +        })
    +    }
    +
         /// Register a user with an OIDC ID:
         /// If the user does not exist, a new user is created.
         /// If the user already exists and has matching OIDC ID, nothing is done.
    
  • backend/src/lib.rs+1 0 modified
    @@ -73,6 +73,7 @@ pub async fn create_rocket() -> Rocket<Build> {
         let db_initialized = db_path.exists();
         let encrypted = settings.get_db_encrypted();
         let db = VaulTLSDB::new(encrypted, false).expect("Failed opening SQLite database");
    +    db.fix_password().await.expect("Failed fixing passwords");
         if !encrypted && env::var("VAULTLS_DB_SECRET").is_ok() {
             settings.set_db_encrypted().unwrap()
         }
    
  • backend/tests/api/api_test_functionality.rs+1 1 modified
    @@ -37,7 +37,7 @@ async fn test_version() -> Result<()>{
     
         assert_eq!(response.status(), Status::Ok);
         assert_eq!(response.content_type(), Some(ContentType::Plain));
    -    assert_eq!(response.into_string().await, Some("v0.9.0".into()));
    +    assert_eq!(response.into_string().await, Some("v0.9.1".into()));
     
         Ok(())
     }
    
  • backend/tests/api/api_test_safety.rs+26 0 modified
    @@ -174,5 +174,31 @@ async fn access_deleted_users_certs() -> Result<()> {
         assert_eq!(response.into_string().await.unwrap(), "[]");
     
         Ok(())
    +}
    +
    +#[tokio::test]
    +async fn password_disabled_login() -> Result<()> {
    +    let client = VaulTLSClient::new_authenticated().await;
    +
    +    let mut settings = client.get_settings().await?;
    +
    +    settings["common"]["password_enabled"] = Value::Bool(false);
    +
    +    client.put_settings(settings).await?;
     
    +    client.logout().await?;
    +
    +    let login_data = LoginRequest{
    +        email: TEST_USER_EMAIL.to_string(),
    +        password: TEST_PASSWORD.to_string()
    +    };
    +
    +    let request = client
    +        .post("/auth/login")
    +        .header(ContentType::JSON)
    +        .body(serde_json::to_string(&login_data)?);
    +    let response = request.dispatch().await;
    +    assert_ne!(response.status(), Status::Ok);
    +
    +    Ok(())
     }
    \ No newline at end of file
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

2

News mentions

0

No linked articles in our index yet.