feat: Propagate Request Headers#19572
Conversation
FrankChen021
left a comment
There was a problem hiding this comment.
| Severity | Findings |
|---|---|
| P0 | 0 |
| P1 | 0 |
| P2 | 1 |
| P3 | 0 |
| Total | 1 |
Reviewed 33 files, including all 22 changed files.
I found one issue: configured request-header targets can bypass existing query-context authorization when mapped to operational context keys such as priority or lane.
This is an automated review by Codex GPT-5.5
| } | ||
| final Map<String, String> captured = RequestHeaderContext.current(); | ||
| if (!captured.isEmpty()) { | ||
| finalContext.putAll(captured); |
There was a problem hiding this comment.
P2 Header-mapped context bypasses context authorization
Captured request headers are merged into the final query context here, but these keys were not included in the user context keys that AuthConfig.contextKeysToAuthorize checks. Since RequestHeaderContextConfig only rejects queryId/subQueryId/sqlQueryId, an operator can map a client-controlled header to existing operational context keys such as priority, lane, or cache flags; clients can then set those values via headers without the QUERY_CONTEXT WRITE authorization required for the same keys in the query body. Please either reject mappings to existing Druid query-context keys or include captured header target keys in context authorization unless they come from a trusted internal source.
Description
Adds a configurable mechanism to capture inbound HTTP headers and propagate them through Druid:
Inbound capture — a servlet filter (RequestHeaderContextFilter) reads the headers listed in druid.audit.requestHeaders.headerToContextKey, binds the values to a request thread-local, and clears them in a finally block.
Injection into Query.getContext() — QueryLifecycle.initialize() strips any user-supplied values for the configured reserved context keys (anti-spoof), then injects the filter-captured values. Druid's existing native sub-query context propagation flows the values to historicals/peons for free.
Wire propagation on broker → historical RPCs — DirectDruidClient reads from the current query context and re-attaches the configured headers onto each outbound Request, so the receiving node's filter captures them just as if a client had sent them. End-to-end propagation without trusting the JSON body context.
Typed RequestInfo.traceId audit column — AuthorizationUtils.buildRequestInfo reads the value of the "traceId" context key (canonical name) from the filter thread-local, populating a new typed field on RequestInfo. Every AuditEntry built via AuthorizationUtils automatically carries the trace ID, with @JsonInclude(NON_NULL) so existing audit JSON is byte-identical when no trace header is sent.
Config
Default: X-Druid-Trace-Id → traceId (enabled out of the box)
druid.audit.requestHeaders.headerToContextKey={"X-Druid-Trace-Id": "traceId"}Add additional headers
Explicitly disable (empty map)
druid.audit.requestHeaders.headerToContextKey={}Mapping any header to a Druid reserved context key (queryId, subQueryId, sqlQueryId) throws at config-bind time so a client can't overwrite the server-assigned queryId.
Release note
Druid now supports propagating configured inbound HTTP headers (default X-Druid-Trace-Id) through the query context and into audit events. Configure via
druid.audit.requestHeaders.headerToContextKey. Mapping a header to a Druid reserved context key (queryId, subQueryId, sqlQueryId) is rejected at startup. A new typed traceId field on RequestInfo lands in the audit table for correlation with distributed-trace systems.Key changed/added classes
This PR has: