Skip to content

API token leaks into tracing logs: #[tracing::instrument] on every endpoint records Client (with token) via Debug #1041

Description

@sneg55

API token (and other secrets) leak into tracing logs via #[tracing::instrument] on every endpoint

Context: API Makeathon participant. Found in a code review of the official clients; independently flagged by two separate review passes.

Client derives Debug while holding token: String, and the generator puts a bare #[tracing::instrument] on every generated endpoint method. tracing::instrument records all arguments as span fields using their Debug representation, including &self. So self (which contains the bearer token) becomes a span field on every API call.

Evidence

kittycad/src/lib.rs:165:

#[derive(Clone, Debug)]
#[cfg(feature = "requests")]
pub struct Client {
    token: String,
    base_url: String,
    ...
}

Every endpoint, e.g. kittycad/src/api_calls.rs:16:

#[tracing::instrument]
pub async fn get<'a>(&'a self, id: uuid::Uuid) -> Result<...> { ... }

The authors clearly know this matters: Client::new uses #[tracing::instrument(skip(token))] (lib.rs:200). The generated endpoint methods do not carry that skip. The client also installs reqwest_tracing::TracingMiddleware by default (lib.rs:219), so spans are active in normal operation. A static scan finds ~240 instrumented endpoint methods. Some methods additionally take secrets as direct arguments (API-token creation takes a token value; SAML request bodies can carry a private signing key), which are logged the same way.

Concrete failure

Any user who initializes a standard subscriber (tracing_subscriber::fmt(), INFO level, the default) has their Zoo API token written verbatim to stdout / log files / any span exporter on every request:

INFO get{self=ApiCalls { client: Client { token: "kc-REAL-TOKEN", base_url: "https://api.zoo.dev" } } id=...}: sending request

format!("{client:?}") alone also prints the token.

Suggested fix

Emit #[tracing::instrument(skip(self))] (or skip_all with an explicit allowlist) from the generator, and add a manual Debug for Client that redacts token. Consider a secrecy::Secret<String> wrapper for the token field so it cannot be Debug-printed at all.

Verify

format!("{:?}", Client::new("sentinel-token")) contains sentinel-token. Or install tracing_subscriber::fmt().with_test_writer(), call any endpoint against a wiremock server, and assert the captured output does not contain the token (it will).

Environment

Reviewed against the current main of KittyCAD/kittycad.rs.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions