#![allow(clippy::expect_used)]
use std::fmt::Write as _;
use std::sync::{Arc, Mutex, Once};
use axum::body::Body;
use axum::http::{Method, Request, StatusCode};
use tensor_wasm_api::{build_router_with_config, AppState, AuthConfig, TenantConfig};
use tower::ServiceExt;
use tracing::field::{Field, Visit};
use tracing::span;
use tracing::Subscriber;
use tracing_subscriber::layer::{Context, SubscriberExt};
use tracing_subscriber::registry::LookupSpan;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::Layer;
#[derive(Debug, Clone)]
struct CapturedField {
span_name: &'static str,
field_name: &'static str,
value: String,
}
struct CapturingVisitor<'a> {
span_name: &'static str,
out: &'a Mutex<Vec<CapturedField>>,
}
impl<'a> Visit for CapturingVisitor<'a> {
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
let mut s = String::new();
let _ = write!(&mut s, "{value:?}");
if let Ok(mut guard) = self.out.lock() {
guard.push(CapturedField {
span_name: self.span_name,
field_name: field.name(),
value: s,
});
}
}
fn record_str(&mut self, field: &Field, value: &str) {
if let Ok(mut guard) = self.out.lock() {
guard.push(CapturedField {
span_name: self.span_name,
field_name: field.name(),
value: value.to_owned(),
});
}
}
}
struct CapturingLayer {
captured: Arc<Mutex<Vec<CapturedField>>>,
}
impl<S> Layer<S> for CapturingLayer
where
S: Subscriber + for<'a> LookupSpan<'a>,
{
fn on_new_span(&self, attrs: &span::Attributes<'_>, _id: &span::Id, _ctx: Context<'_, S>) {
let raw_name = attrs.metadata().name();
let span_name: &'static str = match raw_name {
"http.request" => "http.request",
_ => "__other__",
};
let mut visitor = CapturingVisitor {
span_name,
out: &self.captured,
};
attrs.record(&mut visitor);
}
}
fn install_capturing_subscriber(captured: Arc<Mutex<Vec<CapturedField>>>) {
static INIT: Once = Once::new();
INIT.call_once(|| {
let layer = CapturingLayer { captured };
let subscriber = tracing_subscriber::registry().with(layer);
let _ = subscriber.try_init();
});
}
fn dev_router() -> axum::Router {
build_router_with_config(
Arc::new(AppState::default()),
AuthConfig::default(),
TenantConfig::default(),
)
}
fn snapshot(captured: &Mutex<Vec<CapturedField>>) -> Vec<CapturedField> {
captured.lock().expect("captured lock not poisoned").clone()
}
#[tokio::test]
async fn span_attributes_do_not_contain_query_string_secrets() {
const ATTACKER_SECRET: &str = "ATTACKER_SECRET";
const TOKEN_VALUE: &str = "ABC123";
let captured: Arc<Mutex<Vec<CapturedField>>> = Arc::new(Mutex::new(Vec::new()));
install_capturing_subscriber(Arc::clone(&captured));
let pre_len = captured.lock().expect("lock").len();
let router = dev_router();
let req = Request::builder()
.method(Method::GET)
.uri(format!(
"/healthz?secret={ATTACKER_SECRET}&token={TOKEN_VALUE}"
))
.body(Body::empty())
.unwrap();
let resp = router.oneshot(req).await.expect("router runs");
assert_eq!(resp.status(), StatusCode::OK, "healthz must succeed");
let snap = snapshot(&captured);
let load_obs: Vec<&CapturedField> = snap.iter().skip(pre_len).collect();
if load_obs.is_empty() {
eprintln!(
"trace_span_does_not_leak_query: no spans captured \
(another test owns the global subscriber); skipping assertion"
);
return;
}
for obs in &load_obs {
assert!(
!obs.value.contains(ATTACKER_SECRET),
"span `{}` field `{}` leaked the query secret `{ATTACKER_SECRET}`: {:?}",
obs.span_name,
obs.field_name,
obs.value,
);
assert!(
!obs.value.contains(TOKEN_VALUE),
"span `{}` field `{}` leaked the query token `{TOKEN_VALUE}`: {:?}",
obs.span_name,
obs.field_name,
obs.value,
);
}
let saw_http_request_path = load_obs
.iter()
.any(|o| o.span_name == "http.request" && o.field_name == "path");
assert!(
saw_http_request_path,
"no `http.request` span recorded a `path` field — \
middleware sanitisation may have been removed entirely",
);
}
#[tokio::test]
async fn span_attributes_bound_oversized_traceparent_header() {
let attacker_tp: String = "A".repeat(65_536);
let captured: Arc<Mutex<Vec<CapturedField>>> = Arc::new(Mutex::new(Vec::new()));
install_capturing_subscriber(Arc::clone(&captured));
let pre_len = captured.lock().expect("lock").len();
let router = dev_router();
let req = Request::builder()
.method(Method::GET)
.uri("/healthz")
.header("traceparent", attacker_tp.as_str())
.body(Body::empty())
.expect("64 KiB all-'A' header is a valid HeaderValue");
let resp = router
.oneshot(req)
.await
.expect("router::oneshot infallible");
let _ = resp.status();
let snap = snapshot(&captured);
let load_obs: Vec<&CapturedField> = snap.iter().skip(pre_len).collect();
if load_obs.is_empty() {
eprintln!(
"trace_span_does_not_leak_query: no spans captured \
(another test owns the global subscriber); skipping assertion"
);
return;
}
let tp_obs: Vec<&CapturedField> = load_obs
.iter()
.copied()
.filter(|o| o.span_name == "http.request" && o.field_name == "traceparent")
.collect();
for obs in &tp_obs {
assert!(
obs.value.len() <= 96,
"captured traceparent `{}` exceeded length cap ({} bytes) — \
sanitise_traceparent did not clamp",
obs.value,
obs.value.len(),
);
assert!(
!obs.value.contains('\r') && !obs.value.contains('\n'),
"captured traceparent contained CR/LF — sanitise_traceparent \
must collapse control chars to `<invalid>`: {:?}",
obs.value,
);
assert!(
!obs.value.contains(&"A".repeat(100)),
"captured traceparent retained a 100+ run of attacker padding: {:?}",
obs.value,
);
}
}