[pbs-devel] [PATCH proxmox 3/3] auth-api: allow log-in via parameters even if HttpOnly cookie is invalid

Dominik Csapak d.csapak at proxmox.com
Fri Jul 25 14:23:02 CEST 2025


LGTM

tested by invalidating my http-only cookie and logged in via
the login mask. worked successfully

Reviewed-by: Dominik Csapak <d.csapak at proxmox.com>
Tested-by: Dominik Csapak <d.csapak at proxmox.com>

On 7/25/25 13:24, Shannon Sterz wrote:
> previously the new HttpOnly endpoint would fail when a cookie was
> provided even if the body of the request contained valid credentials.
> this lead to issues when browser-based clients may have gotten invalid
> HttpOnly cookies e.g. if a Proxmox Backup Server was re-installed at
> the same IP address. the client could not remove the cookie due to the
> new protections. while the server did not allow the client to log in
> as it trusted the HttpOnly cookie over the parameters.
> 
> allow users to log in again in such a scenario, but don't allow a
> ticket refresh. if the client has a valid ticket but cannot provide it
> via HttpOnly cookie, something is off and forcing the client to
> re-authenticate is probably the safer option.
> 
> Reported-by: Laurențiu Leahu-Vlăducu <l.leahu-vladucu at proxmox.com>
> Suggested-By: Dominik Csapak <d.csapak at proxmox.com>
> Signed-off-by: Shannon Sterz <s.sterz at proxmox.com>
> ---
>   proxmox-auth-api/src/api/access.rs | 50 +++++++++++++++++++-----------
>   proxmox-auth-api/src/types.rs      |  2 +-
>   2 files changed, 33 insertions(+), 19 deletions(-)
> 
> diff --git a/proxmox-auth-api/src/api/access.rs b/proxmox-auth-api/src/api/access.rs
> index 671a370b..490fe5c8 100644
> --- a/proxmox-auth-api/src/api/access.rs
> +++ b/proxmox-auth-api/src/api/access.rs
> @@ -59,7 +59,7 @@ pub async fn create_ticket(
>           .downcast_ref::<RestEnvironment>()
>           .ok_or_else(|| format_err!("detected wrong RpcEnvironment type"))?;
>   
> -    handle_ticket_creation(create_params, env)
> +    handle_ticket_creation(create_params, true, env)
>           .await
>           // remove the superfluous ticket_info to not confuse clients
>           .map(|mut info| {
> @@ -121,6 +121,7 @@ fn create_ticket_http_only(
>           let auth_context = auth_context()?;
>           let host_cookie = auth_context.prefixed_auth_cookie_name();
>           let mut create_params: CreateTicket = serde_json::from_value(param)?;
> +        let password = create_params.password.take();
>   
>           // previously to refresh a ticket, the old ticket was provided as a password via this
>           // endpoint's parameters. however, once the ticket is set as an HttpOnly cookie, some
> @@ -139,16 +140,22 @@ fn create_ticket_http_only(
>               // after this only `__Host-{Cookie Name}` cookies are in the iterator
>               .filter_map(|c| extract_cookie(c, host_cookie))
>               // so this should just give us the first one if it exists
> -            .next()
> -            // if not use the parameter
> -            .or(create_params.password);
> +            .next();
>   
>           let env: &RestEnvironment = rpcenv
>               .as_any()
>               .downcast_ref::<RestEnvironment>()
>               .ok_or(format_err!("detected wrong RpcEnvironment type"))?;
>   
> -        let mut ticket_response = handle_ticket_creation(create_params, env).await?;
> +        let mut ticket_response = handle_ticket_creation(create_params.clone(), true, env).await;
> +
> +        if ticket_response.is_err() && password.is_some() {
> +            create_params.password = password;
> +            ticket_response = handle_ticket_creation(create_params, false, env).await;
> +        }
> +
> +        let mut ticket_response = ticket_response?;
> +
>           let mut response =
>               Response::builder().header(http::header::CONTENT_TYPE, "application/json");
>   
> @@ -185,6 +192,7 @@ fn create_ticket_http_only(
>   
>   async fn handle_ticket_creation(
>       create_params: CreateTicket,
> +    allow_ticket_refresh: bool,
>       env: &RestEnvironment,
>   ) -> Result<CreateTicketResponse, Error> {
>       let username = create_params.username;
> @@ -199,6 +207,7 @@ async fn handle_ticket_creation(
>           create_params.privs,
>           create_params.port,
>           create_params.tfa_challenge,
> +        allow_ticket_refresh,
>           env,
>       )
>       .await
> @@ -240,6 +249,7 @@ async fn handle_ticket_creation(
>       }
>   }
>   
> +#[allow(clippy::too_many_arguments)]
>   async fn authenticate_user(
>       userid: &Userid,
>       password: &str,
> @@ -247,6 +257,7 @@ async fn authenticate_user(
>       privs: Option<String>,
>       port: Option<u16>,
>       tfa_challenge: Option<String>,
> +    allow_ticket_refresh: bool,
>       rpcenv: &RestEnvironment,
>   ) -> Result<AuthResult, Error> {
>       let auth_context = auth_context()?;
> @@ -261,21 +272,24 @@ async fn authenticate_user(
>           return authenticate_2nd(userid, &tfa_challenge, password);
>       }
>   
> -    if password.starts_with(prefix) && password.as_bytes().get(prefix.len()).copied() == Some(b':')
> -    {
> -        if let Ok(ticket_userid) = Ticket::<Userid>::parse(password)
> -            .and_then(|ticket| ticket.verify(auth_context.keyring(), prefix, None))
> +    if allow_ticket_refresh {
> +        if password.starts_with(prefix)
> +            && password.as_bytes().get(prefix.len()).copied() == Some(b':')
>           {
> -            if *userid == ticket_userid {
> -                return Ok(AuthResult::CreateTicket);
> +            if let Ok(ticket_userid) = Ticket::<Userid>::parse(password)
> +                .and_then(|ticket| ticket.verify(auth_context.keyring(), prefix, None))
> +            {
> +                if *userid == ticket_userid {
> +                    return Ok(AuthResult::CreateTicket);
> +                }
> +                bail!("ticket login failed - wrong userid");
> +            }
> +        } else if let Some(((path, privs), port)) = path.zip(privs).zip(port) {
> +            match auth_context.check_path_ticket(userid, password, path, privs, port)? {
> +                None => (), // no path based tickets supported, just fall through.
> +                Some(true) => return Ok(AuthResult::Success),
> +                Some(false) => bail!("No such privilege"),
>               }
> -            bail!("ticket login failed - wrong userid");
> -        }
> -    } else if let Some(((path, privs), port)) = path.zip(privs).zip(port) {
> -        match auth_context.check_path_ticket(userid, password, path, privs, port)? {
> -            None => (), // no path based tickets supported, just fall through.
> -            Some(true) => return Ok(AuthResult::Success),
> -            Some(false) => bail!("No such privilege"),
>           }
>       }
>   
> diff --git a/proxmox-auth-api/src/types.rs b/proxmox-auth-api/src/types.rs
> index 0964e072..9bde661c 100644
> --- a/proxmox-auth-api/src/types.rs
> +++ b/proxmox-auth-api/src/types.rs
> @@ -678,7 +678,7 @@ impl TryFrom<String> for Authid {
>   
>   #[api]
>   /// The parameter object for creating new ticket.
> -#[derive(Debug, Deserialize, Serialize)]
> +#[derive(Debug, Clone, Deserialize, Serialize)]
>   pub struct CreateTicket {
>       /// User name
>       pub username: Userid,





More information about the pbs-devel mailing list