Skip to content

bug - [derive(Debug)] cred leak #741

@peatey

Description

@peatey

Describe the bug

StoredCredentials and StoredAuthorizationState in crates/rmcp/src/transport/auth.rs both derive #[derive(Debug)]. This causes secret fields — OAuth access tokens, refresh tokens, PKCE verifiers, and CSRF tokens — to be printed in plaintext whenever these types are formatted via a Debug formatter. There is no compiler warning or type-system protection against this.

To Reproduce

  1. Add the auth feature to your rmcp dependency.
  2. Run an OAuth flow to populate a StoredAuthorizationState or StoredCredentials.
  3. Format either type using {:?} in any log call:
tracing::error!("auth state: {:?}", stored_state);
  1. Observe that live PKCE verifiers, CSRF tokens, access tokens, and refresh tokens are emitted in plaintext to log output.

Expected behavior

Secret fields should be redacted in Debug output:

StoredAuthorizationState { pkce_verifier: [REDACTED], csrf_token: [REDACTED], created_at: 1234567890 }

Logs

Current {:?} output for StoredAuthorizationState:

StoredAuthorizationState { 
    pkce_verifier: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk", 
    csrf_token: "xK9mP2nQ8rL4vT6wY1uZ3aB5cD7eF0gH",
    created_at: 1741564800
}

Additional context

Affected types:

StoredCredentials (auth.rs line 62) — token_response contains access_token and refresh_token:

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StoredCredentials {
    pub client_id: String,
    pub token_response: Option<OAuthTokenResponse>,
    pub granted_scopes: Vec<String>,
    pub token_received_at: Option<u64>,
}

StoredAuthorizationState (auth.rs line 122):

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StoredAuthorizationState {
    pub pkce_verifier: String,
    pub csrf_token: String,
    pub created_at: u64,
}

The MCP spec's own auth guidance explicitly states: "Don't log credentials. Never log Authorization headers, tokens, codes, or secrets." The current type definitions make it easy to violate this by accident — including via anyhow/thiserror error chains that automatically format Debug representations of involved values.

Suggested fix: use the [secrecy](https://crates.io/crates/secrecy) crate. Wrap secret fields in SecretString, whose Debug impl prints [REDACTED] and requires .expose_secret() for access. Its serde feature keeps serialisation transparent so CredentialStore implementations are unaffected. Note that OAuthTokenResponse comes from the oauth2 crate and cannot be wrapped directly — Debug should be implemented manually on StoredCredentials to redact that field.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P0Critical: blocking, security, data loss, or crashT-securitySecurity-related changesbugSomething is not working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions