Skip to content
9 changes: 9 additions & 0 deletions crates/aisix-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,15 @@ impl RequestIdConfig {
if RESERVED_UPSTREAM_HEADERS.contains(&name.as_str()) {
return Err(s.clone());
}
// W3C trace-context headers can't be request-id sources
// either (AISIX-Cloud#1279): the resolved id is echoed to
// the caller, logged, and sent upstream — adopting the
// inbound `traceparent`/`tracestate` value would disclose
// the caller's trace context through all three, walking
// around the never-forward guard.
if matches!(name.as_str(), "traceparent" | "tracestate") {
return Err(s.clone());
}
Ok(name)
})
.collect()
Expand Down
38 changes: 38 additions & 0 deletions crates/aisix-gateway/src/upstream_headers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,18 @@ const NEVER_FORWARD_HEADERS: &[&str] = &[
"te",
"trailer",
"transfer-encoding",
// W3C trace context (AISIX-Cloud#1279): the caller's `traceparent`
// names a span in the CALLER's trace — relayed to a provider it links
// the provider's internal telemetry into the caller's tracing backend
// and leaks the caller's trace ids to a third party. The gateway is a
// fresh hop; provider-side propagation, if ever wanted, is a separate
// opt-in that injects the gateway's own context, never the inbound
// value. Listed here rather than in RESERVED_UPSTREAM_HEADERS so an
// operator's deliberate `default_headers` entry for a trusted
// first-party upstream still works — only the client's copy is
// blocked.
"traceparent",
"tracestate",
"upgrade",
];

Expand Down Expand Up @@ -370,6 +382,32 @@ mod tests {
assert_eq!(headers["x-keep"], "yes");
}

// AISIX-Cloud#1279: the caller's W3C trace context must never reach a
// provider — not under a `*` glob, and not even when an allowlist names
// the headers outright. The gateway is a fresh tracing hop; a future
// provider-side propagation opt-in injects the gateway's OWN context.
#[test]
fn trace_context_headers_are_never_forwarded() {
for allowlist in [&["*"][..], &["traceparent", "tracestate"][..]] {
let patterns: Vec<&str> = allowlist.to_vec();
let r = overrides(&[], &patterns);
let mut headers = HeaderMap::new();
let inbound = client(&[
(
"traceparent",
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
),
("tracestate", "vendor=x"),
]);
let ctx = UpstreamHeaderContext::from_overrides(Some(&r)).with_client_headers(&inbound);
apply_request_headers(&mut headers, &ctx);
assert!(
headers.is_empty(),
"trace context leaked under {allowlist:?}: {headers:?}"
);
}
}

#[test]
fn allowlist_matches_exact_names_and_a_single_glob() {
let r = overrides(&[], &["Anthropic-Beta", "x-trace-*"]);
Expand Down
5 changes: 5 additions & 0 deletions crates/aisix-obs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub mod metrics;
pub mod otlp;
pub mod otlp_http_sink;
pub mod sink;
pub mod trace;
pub mod usage;

use std::io::IsTerminal as _;
Expand All @@ -41,6 +42,10 @@ pub use sink::{
PipelineConfig, SinkAck, SinkCapabilities, SinkContent, SinkError, SinkHandle, SinkHealth,
SinkPipeline, SinkRecord, SinkResult, SinkStatsSnapshot, SCHEMA_VERSION,
};
pub use trace::{
parse_traceparent, screen_tracestate, RemoteTraceContext, RequestTraceBundle, SpanEmit,
SpanRole, TraceEmission, TRACEPARENT_HEADER, TRACESTATE_HEADER,
};
pub use usage::{UsageEvent, UsageSink};

#[derive(Debug, thiserror::Error)]
Expand Down
Loading