Obfuscate Bearer Token values in logs (#16164)

Sometimes a credential's `Debug` formatted value appears in tracing logs
- make sure the credential doesn't appear there.

## Test plan

Added a test case + ran
```
uv pip install --default-index $PYX_API_URL/$SOME_INDEX $SOME_PACKAGE -vv
```
With an authenticated uv client and confirmed the tokens are obfuscated.

---------

Co-authored-by: Zsolt Dollenstein <zsol.zsol@gmail.com>
This commit is contained in:
Zanie Blue 2025-10-13 08:28:20 -05:00 committed by GitHub
parent 01d43382be
commit 15829bb30a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 54 additions and 9 deletions

View file

@ -29,6 +29,6 @@ impl AsRef<[u8]> for AccessToken {
impl std::fmt::Display for AccessToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
write!(f, "****")
}
}

View file

@ -30,7 +30,7 @@ pub enum Credentials {
/// RFC 6750 Bearer Token Authentication
Bearer {
/// The token to use for authentication.
token: Vec<u8>,
token: Token,
},
}
@ -102,6 +102,36 @@ impl fmt::Debug for Password {
}
}
#[derive(Clone, PartialEq, Eq, Ord, PartialOrd, Hash, Default, Deserialize)]
#[serde(transparent)]
pub struct Token(Vec<u8>);
impl Token {
pub fn new(token: Vec<u8>) -> Self {
Self(token)
}
/// Return the [`Token`] as a byte slice.
pub fn as_slice(&self) -> &[u8] {
self.0.as_slice()
}
/// Convert the [`Token`] into its underlying [`Vec<u8>`].
pub fn into_bytes(self) -> Vec<u8> {
self.0
}
/// Return whether the [`Token`] is empty.
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl fmt::Debug for Token {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "****")
}
}
impl Credentials {
/// Create a set of HTTP Basic Authentication credentials.
#[allow(dead_code)]
@ -115,7 +145,9 @@ impl Credentials {
/// Create a set of Bearer Authentication credentials.
#[allow(dead_code)]
pub fn bearer(token: Vec<u8>) -> Self {
Self::Bearer { token }
Self::Bearer {
token: Token::new(token),
}
}
pub fn username(&self) -> Option<&str> {
@ -286,7 +318,7 @@ impl Credentials {
// Parse a `Bearer` authentication header.
if let Some(token) = header.as_bytes().strip_prefix(b"Bearer ") {
return Some(Self::Bearer {
token: token.to_vec(),
token: Token::new(token.to_vec()),
});
}
@ -591,4 +623,15 @@ mod tests {
"Basic { username: Username(Some(\"user\")), password: Some(****) }"
);
}
#[test]
fn test_bearer_token_obfuscation() {
let token = "super_secret_token";
let credentials = Credentials::bearer(token.into());
let debugged = format!("{credentials:?}");
assert!(
!debugged.contains(token),
"Token should be obfuscated in Debug impl: {debugged}"
);
}
}

View file

@ -10,6 +10,7 @@ use uv_static::EnvVars;
use uv_warnings::warn_user_once;
use crate::Credentials;
use crate::credentials::Token;
use crate::realm::{Realm, RealmRef};
/// The [`Realm`] for the Hugging Face platform.
@ -45,7 +46,7 @@ impl HuggingFaceProvider {
if RealmRef::from(url) == *HUGGING_FACE_REALM {
if let Some(token) = HUGGING_FACE_TOKEN.as_ref() {
return Some(Credentials::Bearer {
token: token.clone(),
token: Token::new(token.clone()),
});
}
}

View file

@ -15,6 +15,7 @@ use uv_small_str::SmallString;
use uv_state::{StateBucket, StateStore};
use uv_static::EnvVars;
use crate::credentials::Token;
use crate::{AccessToken, Credentials, Realm};
/// Retrieve the pyx API key from the environment variable, or return `None`.
@ -84,7 +85,7 @@ impl From<PyxTokens> for Credentials {
impl From<AccessToken> for Credentials {
fn from(access_token: AccessToken) -> Self {
Self::Bearer {
token: access_token.into_bytes(),
token: Token::new(access_token.into_bytes()),
}
}
}

View file

@ -13,7 +13,7 @@ use uv_redacted::DisplaySafeUrl;
use uv_state::{StateBucket, StateStore};
use uv_static::EnvVars;
use crate::credentials::{Password, Username};
use crate::credentials::{Password, Token, Username};
use crate::realm::Realm;
use crate::service::Service;
use crate::{Credentials, KeyringProvider};
@ -142,7 +142,7 @@ impl From<TomlCredential> for TomlCredentialWire {
username: Username::new(None),
scheme: AuthScheme::Bearer,
password: None,
token: Some(String::from_utf8(token).expect("Token is valid UTF-8")),
token: Some(String::from_utf8(token.into_bytes()).expect("Token is valid UTF-8")),
},
}
}
@ -190,7 +190,7 @@ impl TryFrom<TomlCredentialWire> for TomlCredential {
));
}
let credentials = Credentials::Bearer {
token: value.token.unwrap().into_bytes(),
token: Token::new(value.token.unwrap().into_bytes()),
};
Ok(Self {
service: value.service,