JuliaWeb / HTTP.jl

HTTP for Julia
https://juliaweb.github.io/HTTP.jl/stable/
Other
635 stars 176 forks source link

Mask Authorization header when showing RequestError #1125

Closed nkottary closed 10 months ago

nkottary commented 10 months ago

The problem: The value of the Authorization header is printed as is when displaying a RequestError. This leaks the token in the key to the logs.

Steps to reproduce: 1) The following HTTP server has an error in it, so it will result in an eof error for clients:

using HTTP

# start a blocking server
HTTP.listen() do http::HTTP.Stream
    @show http.message
    @show HTTP.header(http, "Content-Type")
    while !eof(http)
        println("body data: ", String(readavailable(http)))
    end
    HTTP.setstatus(http, 404)
    HTTP.setheader(http, "Foo-Header" => "bar")
    HTTP.startwrite(http)
    HTTP.close!(http)    # Error
    return
    write(http, "response body")
    write(http, "more response body")
end

Run this as a server: julia server.jl

2) Send a request to this server with an Authorization header:

using HTTP
using JSON

function run_get()
    headers = [
        "Content-Type" => "application/json",
        "Authorization" => "Basic LeakedOhNo",
        "Accept" => "*/*",
    ]

    body = JSON.json(
        Dict(
            "values" => ["a", "b"]
        )
    )
    resp = HTTP.post(
        "http://localhost:8081/";
        headers=headers,
        body=body,
    )
end

Example:

$ julia
               _
   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 1.9.3 (2023-08-24)
 _/ |\__'_|_|_|\__'_|  |  Official https://julialang.org/ release
|__/                   |

julia> include("client.jl")
run_get (generic function with 1 method)

julia> run_get()

3) This results in error with Authorization header revealed in stacktrace:

ERROR: HTTP.RequestError:
HTTP.Request:
HTTP.Messages.Request:
"""
POST / HTTP/1.1
Content-Type: application/json
Authorization: Basic LeakedOhNo
Accept: */*
Host: localhost:8081
User-Agent: HTTP.jl/1.9.3
Content-Length: 20
Accept-Encoding: gzip

{"values":["a","b"]}"""Underlying error:
EOFError: read end of file
Stacktrace:
  [1] (::HTTP.ConnectionRequest.var"#connections#4"{HTTP.ConnectionRequest.var"#connections#1#5"{HTTP.TimeoutRequest.var"#timeouts#3"{HTTP.TimeoutRequest.var"#timeouts#1#4"{HTTP.ExceptionRequest.var"#exceptions#2"{HTTP.ExceptionRequest.var"#exceptions#1#3"{typeof(HTTP.StreamRequest.streamlayer)}}}}}})(req::HTTP.Messages.Request; proxy::Nothing, socket_type::Type, socket_type_tls::Type, readtimeout::Int64, connect_timeout::Int64, logerrors::Bool, logtag::Nothing, kw::Base.Pairs{Symbol, Union{Nothing, Int64}, Tuple{Symbol, Symbol, Symbol}, NamedTuple{(:iofunction, :decompress, :verbose), Tuple{Nothing, Nothing, Int64}}})
    @ HTTP.ConnectionRequest ~/.julia/packages/HTTP/SN7VW/src/clientlayers/ConnectionRequest.jl:143
  [2] (::Base.var"#90#92"{Base.var"#90#91#93"{ExponentialBackOff, HTTP.RetryRequest.var"#2#5"{Int64, typeof(HTTP.RetryRequest.FALSE), HTTP.Messages.Request, Base.RefValue{Int64}}, HTTP.ConnectionRequest.var"#connections#4"{HTTP.ConnectionRequest.var"#connections#1#5"{HTTP.TimeoutRequest.var"#timeouts#3"{HTTP.TimeoutRequest.var"#timeouts#1#4"{HTTP.ExceptionRequest.var"#exceptions#2"{HTTP.ExceptionRequest.var"#exceptions#1#3"{typeof(HTTP.StreamRequest.streamlayer)}}}}}}}})(args::HTTP.Messages.Request; kwargs::Base.Pairs{Symbol, Union{Nothing, Int64}, Tuple{Symbol, Symbol, Symbol}, NamedTuple{(:iofunction, :decompress, :verbose), Tuple{Nothing, Nothing, Int64}}})
    @ Base ./error.jl:296
fredrikekre commented 10 months ago

IIUC this isn't really a leak since it just prints the request which resulted in the error. Still a good idea to skip printing of sensitive headers I suppose.

fredrikekre commented 10 months ago

Fixed by #1126 (and #1127 should mitigate any similar possible leaks).