Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## Unreleased

- **Security:** instrumented Rust clients (`--instrument`) no longer record request bodies and headers as tracing span fields. The generated `#[tracing::instrument]` attribute now uses `skip_all`, so credentials in request payloads (passwords, tokens) can no longer reach logs in cleartext via their `Debug` output. Spans are still named after the endpoint path. Regenerate clients to pick this up.

## 0.17.6

- New `#[reflectapi(hidden)]` field attribute: the field stays in the schema (marked `"hidden": true`) and remains functional at runtime (e.g. header extraction by the axum adapter), but is excluded from generated clients, documentation, and OpenAPI specs. Not allowed on unnamed (tuple) fields, since removing a positional element would shift indices and break wire compatibility.
Expand Down
16 changes: 8 additions & 8 deletions reflectapi-demo/clients/rust/generated/src/generated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ pub mod interface {
Self { client }
}
/// Check the health of the service
#[tracing::instrument(name = "/health.check", skip(self, headers))]
#[tracing::instrument(name = "/health.check", skip_all)]
pub async fn check(
&self,
input: reflectapi::Empty,
Expand All @@ -78,7 +78,7 @@ pub mod interface {
Self { client }
}
/// List available pets
#[tracing::instrument(name = "/pets.list", skip(self, headers))]
#[tracing::instrument(name = "/pets.list", skip_all)]
pub async fn list(
&self,
input: super::types::myapi::proto::PetsListRequest,
Expand All @@ -90,7 +90,7 @@ pub mod interface {
reflectapi::rt::__request_impl(&self.client, "/pets.list", input, headers).await
}
/// Create a new pet
#[tracing::instrument(name = "/pets.create", skip(self, headers))]
#[tracing::instrument(name = "/pets.create", skip_all)]
pub async fn create(
&self,
input: super::types::myapi::proto::PetsCreateRequest,
Expand All @@ -102,7 +102,7 @@ pub mod interface {
reflectapi::rt::__request_impl(&self.client, "/pets.create", input, headers).await
}
/// Update an existing pet
#[tracing::instrument(name = "/pets.update", skip(self, headers))]
#[tracing::instrument(name = "/pets.update", skip_all)]
pub async fn update(
&self,
input: super::types::myapi::proto::PetsUpdateRequest,
Expand All @@ -114,7 +114,7 @@ pub mod interface {
reflectapi::rt::__request_impl(&self.client, "/pets.update", input, headers).await
}
/// Remove an existing pet
#[tracing::instrument(name = "/pets.remove", skip(self, headers))]
#[tracing::instrument(name = "/pets.remove", skip_all)]
pub async fn remove(
&self,
input: super::types::myapi::proto::PetsRemoveRequest,
Expand All @@ -127,7 +127,7 @@ pub mod interface {
}
#[deprecated(note = "Use pets.remove instead")]
/// Remove an existing pet
#[tracing::instrument(name = "/pets.delete", skip(self, headers))]
#[tracing::instrument(name = "/pets.delete", skip_all)]
pub async fn delete(
&self,
input: super::types::myapi::proto::PetsRemoveRequest,
Expand All @@ -139,7 +139,7 @@ pub mod interface {
reflectapi::rt::__request_impl(&self.client, "/pets.delete", input, headers).await
}
/// Fetch first pet, if any exists
#[tracing::instrument(name = "/pets.get-first", skip(self, headers))]
#[tracing::instrument(name = "/pets.get-first", skip_all)]
pub async fn get_first(
&self,
input: reflectapi::Empty,
Expand All @@ -151,7 +151,7 @@ pub mod interface {
reflectapi::rt::__request_impl(&self.client, "/pets.get-first", input, headers).await
}
/// Stream of change data capture events for pets
#[tracing::instrument(name = "/pets.cdc-events", skip(self, headers))]
#[tracing::instrument(name = "/pets.cdc-events", skip_all)]
pub async fn cdc_events(
&self,
input: reflectapi::Empty,
Expand Down
32 changes: 32 additions & 0 deletions reflectapi-demo/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,38 @@ fn write_rust_client() {
.unwrap();
}

#[test]
fn instrumented_rust_client_does_not_record_request_bodies() {
// Request bodies can carry credentials (passwords, tokens); recording
// them as span fields would print them to logs in cleartext via their
// `Debug` impl. Every generated `#[tracing::instrument]` attribute must
// therefore skip all function arguments.
let (schema, _) = crate::builder().build().unwrap();
let src = reflectapi::codegen::rust::generate(
schema,
reflectapi::codegen::rust::Config::default()
.format(true)
.instrument(true),
)
.unwrap();

let instrument_attributes: Vec<&str> = src
.lines()
.filter(|line| line.contains("tracing::instrument"))
.collect();
assert!(
!instrument_attributes.is_empty(),
"expected instrumented codegen to emit #[tracing::instrument] attributes"
);
for attribute in instrument_attributes {
assert!(
attribute.contains("skip_all"),
"instrument attribute records function arguments (request body \
may contain credentials): {attribute}"
);
}
}

#[test]
fn write_typescript_client() {
let (schema, _) = crate::builder().build().unwrap();
Expand Down
5 changes: 4 additions & 1 deletion reflectapi/src/codegen/rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1143,8 +1143,11 @@ fn __interface_types_from_function_group(
.next_back()
.unwrap_or_default()
.replace('-', "_");
// skip_all: request bodies must not be recorded into span fields —
// `input` may carry credentials (passwords, tokens) whose `Debug`
// output would land in logs in cleartext.
let attributes = if config.instrument {
format!(r#"#[tracing::instrument(name = "{path}", skip(self, headers))]"#)
format!(r#"#[tracing::instrument(name = "{path}", skip_all)]"#)
} else {
String::new()
};
Expand Down
Loading