Skip to content

Commit 991f858

Browse files
fredrikekreclaude
andcommitted
Redact sensitive header values when displaying Headers collections
Printing a Headers collection (REPL, @show, logging, stacktraces) used the default AbstractVector show and leaked raw values of sensitive headers such as Authorization. Request/Response display already masks these via _HTTP_REDACTED_HEADERS; add Base.show methods for Headers that route values through the same _http_render_header_value redactor, restoring the HTTP.jl 1.x behavior of masking Authorization, Proxy-Authorization, Cookie, and Set-Cookie with "******". Redaction is display-only: header(), iteration, and indexing still return the real values, and wire output is unaffected. Co-Authored-By: Claude Code <noreply@anthropic.com>
1 parent 626dac7 commit 991f858

2 files changed

Lines changed: 74 additions & 0 deletions

File tree

src/http_display.jl

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,34 @@ function _body_summary_label(body, content_length::Int64)::String
193193
return total > 0 ? string(total, "-byte body") : string(typeof(body), " body")
194194
end
195195

196+
function _show_redacted_header_pair(io::IO, key::String, value::String)::Nothing
197+
show(io, key)
198+
print(io, " => ")
199+
show(io, _http_render_header_value(key, value))
200+
return nothing
201+
end
202+
203+
function Base.show(io::IO, headers::Headers)
204+
print(io, "HTTP.Headers([")
205+
for (i, (key, value)) in enumerate(headers)
206+
i > 1 && print(io, ", ")
207+
_show_redacted_header_pair(io, key, value)
208+
end
209+
print(io, "])")
210+
end
211+
212+
function Base.show(io::IO, ::MIME"text/plain", headers::Headers)
213+
summary(io, headers)
214+
isempty(headers) && return
215+
print(io, ":")
216+
key_width = maximum(length(repr(key)) for (key, _) in headers)
217+
for (key, value) in headers
218+
print(io, "\n ", lpad(repr(key), key_width), " => ")
219+
show(io, _http_render_header_value(key, value))
220+
end
221+
return
222+
end
223+
196224
@inline function _request_summary_target(request::Request)::String
197225
request.host === nothing || return string(request.host::String, request.target)
198226
return request.target

test/http_core_tests.jl

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,52 @@ end
290290
@test occursin("elapsed", msg)
291291
end
292292

293+
@testset "HTTP core headers display" begin
294+
headers = HT.Headers([
295+
"Authorization" => "Bearer super-secret",
296+
"Proxy-Authorization" => "Basic dXNlcjpwYXNz",
297+
"Cookie" => "session=secret-cookie",
298+
"Set-Cookie" => "id=secret-id",
299+
"Content-Type" => "text/plain",
300+
])
301+
302+
# Sensitive header values must be masked in both the compact (2-arg) and
303+
# text/plain (3-arg) show forms, so they never leak into stacktraces,
304+
# logging output, or the REPL.
305+
compact = sprint(show, headers)
306+
@test occursin("HTTP.Headers([", compact)
307+
@test occursin("\"Authorization\" => \"******\"", compact)
308+
@test occursin("\"Proxy-Authorization\" => \"******\"", compact)
309+
@test occursin("\"Cookie\" => \"******\"", compact)
310+
@test occursin("\"Set-Cookie\" => \"******\"", compact)
311+
@test occursin("\"Content-Type\" => \"text/plain\"", compact)
312+
@test !occursin("secret", compact)
313+
@test !occursin("dXNlcjpwYXNz", compact)
314+
315+
plain = sprint(io -> show(io, MIME"text/plain"(), headers))
316+
@test occursin("5-element", plain)
317+
@test occursin("\"Authorization\" => \"******\"", plain)
318+
@test occursin("\"Proxy-Authorization\" => \"******\"", plain)
319+
@test occursin("\"Cookie\" => \"******\"", plain)
320+
@test occursin("\"Set-Cookie\" => \"******\"", plain)
321+
@test occursin("\"Content-Type\" => \"text/plain\"", plain)
322+
@test !occursin("secret", plain)
323+
@test !occursin("dXNlcjpwYXNz", plain)
324+
325+
# Redaction is case-insensitive.
326+
lower = HT.Headers()
327+
push!(lower.entries, "authorization" => "Bearer raw-secret")
328+
@test occursin("\"authorization\" => \"******\"", sprint(show, lower))
329+
@test !occursin("raw-secret", sprint(io -> show(io, MIME"text/plain"(), lower)))
330+
331+
# Redaction is display-only; programmatic access returns the real value.
332+
@test HT.header(headers, "Authorization") == "Bearer super-secret"
333+
334+
# Empty collections render without a trailing colon or entries.
335+
@test sprint(show, HT.Headers()) == "HTTP.Headers([])"
336+
@test endswith(sprint(io -> show(io, MIME"text/plain"(), HT.Headers())), "0-element HTTP.Headers")
337+
end
338+
293339
@testset "HTTP core request/response display" begin
294340
request_headers = HT.Headers()
295341
HT.setheader(request_headers, "Authorization", "Bearer super-secret")

0 commit comments

Comments
 (0)