Rust in Everything
基于 tower-http 0.7.1

日志里还是有 authorization

主线位置:作用点主线的前两个时刻。Trace 在 call 时打印请求头,在响应 future 完成时打印响应头;敏感标记必须在它打印之前打上。标记本身只是 HeaderValue::set_sensitive(true),原理和顺序规则在 搬运请求标识与头 的最后一节;这一页把四种常见摆法跑出真实的日志行。

sequenceDiagram
  participant O as 外层标记
  participant T as Trace
  participant I as 内层标记
  participant H as handler
  O->>T: call:请求头已标记
  T->>T: make_span 打印请求头
  T->>I: call
  I->>H: call(这里才标记请求头,太晚)
  H-->>I: 响应带 set-cookie
  I-->>T: 响应头已标记
  T->>T: on_response 打印响应头
  T-->>O: 响应(这里才标记响应头,太晚)

复现

handler 回一个 set-cookie,请求带 Authorization: Bearer s3cr3t。Trace 打开两处 include_headers:请求头进 span 字段(make_span.rs 第 91 行),响应头进 on_response 事件(on_response.rs 第 110 行),两处都用 Debug 打印 HeaderMap。

tower-http = { version = "=0.7.1", features = ["trace", "sensitive-headers"] }
tower = { version = "=0.5.3", features = ["util"] }
tokio = { version = "=1.54.1", features = ["macros", "rt"] }
http = "1"
http-body-util = "0.1"
bytes = "1"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["fmt"] }
use bytes::Bytes;
use http::{header, Request, Response};
use http_body_util::{BodyExt, Full};
use std::convert::Infallible;
use tower::{Service, ServiceBuilder, ServiceExt};
use tower_http::sensitive_headers::{SetSensitiveHeadersLayer, SetSensitiveRequestHeadersLayer, SetSensitiveResponseHeadersLayer};
use tower_http::trace::{DefaultMakeSpan, DefaultOnResponse, TraceLayer};
use tracing::Level;

async fn handler(_: Request<Full<Bytes>>) -> Result<Response<Full<Bytes>>, Infallible> {
  let mut res = Response::new(Full::from("ok"));
  res.headers_mut().insert(header::SET_COOKIE, "sid=abc123".parse().unwrap());
  Ok(res)
}

fn trace() -> TraceLayer<tower_http::classify::SharedClassifier<tower_http::classify::ServerErrorsAsFailures>, DefaultMakeSpan, tower_http::trace::DefaultOnRequest, DefaultOnResponse> {
  TraceLayer::new_for_http()
    .make_span_with(DefaultMakeSpan::new().include_headers(true))
    .on_response(DefaultOnResponse::new().level(Level::INFO).include_headers(true))
}

async fn run<S>(name: &str, svc: S)
where
  S: Service<Request<Full<Bytes>>, Response = Response<tower_http::trace::ResponseBody<Full<Bytes>, tower_http::classify::NeverClassifyEos<tower_http::classify::ServerErrorsFailureClass>>>, Error = Infallible>,
{
  println!("--- {name}");
  let req = Request::get("/").header(header::AUTHORIZATION, "Bearer s3cr3t").body(Full::default()).unwrap();
  let res = svc.oneshot(req).await.unwrap();
  let _ = res.into_body().collect().await;
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
  tracing_subscriber::fmt().with_max_level(Level::DEBUG).with_ansi(false).without_time().with_target(false).init();
  let auth = [header::AUTHORIZATION];
  let cookie = [header::SET_COOKIE];

  run("no marking", ServiceBuilder::new().layer(trace()).service_fn(handler)).await;
  run(
    "request marker inside Trace",
    ServiceBuilder::new().layer(trace()).layer(SetSensitiveRequestHeadersLayer::new(auth.clone())).service_fn(handler),
  )
  .await;
  run(
    "SetSensitiveHeadersLayer outside Trace",
    ServiceBuilder::new().layer(SetSensitiveHeadersLayer::new([header::AUTHORIZATION, header::SET_COOKIE])).layer(trace()).service_fn(handler),
  )
  .await;
  run(
    "request outside, response inside",
    ServiceBuilder::new()
      .layer(SetSensitiveRequestHeadersLayer::new(auth))
      .layer(trace())
      .layer(SetSensitiveResponseHeadersLayer::new(cookie))
      .service_fn(handler),
  )
  .await;
}

M2 Max、rustc 1.99.0,跑 3 次输出相同(每组第三行 end of stream 和第一行的 span 相同,省略):

--- no marking
DEBUG request{method=GET uri=/ version=HTTP/1.1 headers={"authorization": "Bearer s3cr3t"}}: started processing request
 INFO request{method=GET uri=/ version=HTTP/1.1 headers={"authorization": "Bearer s3cr3t"}}: finished processing request latency=0 ms status=200 response_headers={"set-cookie": "sid=abc123"}
--- request marker inside Trace
DEBUG request{method=GET uri=/ version=HTTP/1.1 headers={"authorization": "Bearer s3cr3t"}}: started processing request
 INFO request{method=GET uri=/ version=HTTP/1.1 headers={"authorization": "Bearer s3cr3t"}}: finished processing request latency=0 ms status=200 response_headers={"set-cookie": "sid=abc123"}
--- SetSensitiveHeadersLayer outside Trace
DEBUG request{method=GET uri=/ version=HTTP/1.1 headers={"authorization": Sensitive}}: started processing request
 INFO request{method=GET uri=/ version=HTTP/1.1 headers={"authorization": Sensitive}}: finished processing request latency=0 ms status=200 response_headers={"set-cookie": "sid=abc123"}
--- request outside, response inside
DEBUG request{method=GET uri=/ version=HTTP/1.1 headers={"authorization": Sensitive}}: started processing request
 INFO request{method=GET uri=/ version=HTTP/1.1 headers={"authorization": Sensitive}}: finished processing request latency=0 ms status=200 response_headers={"set-cookie": Sensitive}
  • 标记层在 Trace 里面:和没加一样。Trace::call 先建 span、把请求头写进字段,然后才调内层;标记层的 call 轮到时,span 里的字符串早就生成了。请求头写进的是 span 字段,之后这个请求的每一行日志都带着它,泄露一次就是每行一次。
  • 两侧合一的 SetSensitiveHeadersLayer 放在外面:请求头藏住了,set-cookie 没藏住。SetSensitiveHeaders<S> 只是 SetSensitiveRequestHeaders<SetSensitiveResponseHeaders<S>> 的别名,两个标记层贴在一起,整体放在 Trace 外面时,响应侧的标记在 Trace 打印之后才发生。放在里面则反过来,请求头漏。
  • 请求侧在外、响应侧在里:两边都是 Sensitive。这就是模块文档示例的摆法。

修法

ServiceBuilder::new()
  .layer(SetSensitiveRequestHeadersLayer::new([header::AUTHORIZATION, header::COOKIE]))
  .layer(TraceLayer::new_for_http().make_span_with(DefaultMakeSpan::new().include_headers(true)))
  .layer(SetSensitiveResponseHeadersLayer::new([header::SET_COOKIE]))

在 axum 里 .layer 的顺序和 ServiceBuilder 相反,后写的在外:先写响应侧、再写 TraceLayer、最后写请求侧。

两个附带的检查:

  • 标记只影响 Debug(和 HTTP/2 的 HPACK 索引),Display 不存在、to_str() 照样拿到原值。自己写的 on_request 闭包里用 to_str() 拼日志,标记不起作用。
  • 不需要看头的话,最省事的是不开 include_headers,默认就是关的。

小结

  • Trace 在 call 时打印请求头、在响应 future 完成时打印响应头,标记要在它打印之前完成。
  • 请求侧标记层放在 Trace 外面,响应侧放在里面;合一的 SetSensitiveHeadersLayer 只是两层的别名,放哪边都会漏一侧。
  • 标记只改 Debug 输出,自己用 to_str() 打出来的照样是明文。

💬 关联讨论

发起讨论
加载中...