Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 44 additions & 2 deletions src/clj_http/client.clj
Original file line number Diff line number Diff line change
Expand Up @@ -237,21 +237,63 @@
[{:keys [status]}]
(<= 500 status 599))

(def default-redact-headers
"Header names (lower-cased) whose values are redacted in thrown exceptions by
default, to keep credentials out of logs."
#{"authorization" "proxy-authorization"})

(defn- redact-headers-map
"Replace the values of any headers whose (lower-cased) name is in `redact`
with \"REDACTED\". Preserves the original header-map type and key casing."
[headers redact]
(if (and (seq headers) (seq redact))
(reduce (fn [hs k]
(if (and (string? k) (contains? redact (str/lower-case k)))
(assoc hs k "REDACTED")
hs))
headers (keys headers))
headers))

(defn- redact-exception-data
"Redact sensitive headers in the response and the attached request before the
map is embedded in a thrown exception. Controlled by the request's
:redact-headers option (a set of header names), defaulting to
`default-redact-headers`. Pass `#{}` to disable."
[req resp]
(let [redact (set (map str/lower-case
(clojure.core/get req :redact-headers
default-redact-headers)))]
(cond-> resp
(:headers resp)
(update :headers redact-headers-map redact)

(get-in resp [:request :headers])
(update-in [:request :headers] redact-headers-map redact))))

(defn- exceptions-response
[req {:keys [status] :as resp}]
(if (unexceptional-status-for-request? req status)
resp
(if (false? (opt req :throw-exceptions))
resp
(let [data (assoc resp :type ::unexceptional-status)]
(let [resp (redact-exception-data req resp)
data (assoc resp :type ::unexceptional-status)]
(if (opt req :throw-entire-message)
(throw+ data "clj-http: status %d %s" (:status %) resp)
(throw+ data "clj-http: status %s" (:status %)))))))

(defn wrap-exceptions
"Middleware that throws a slingshot exception if the response is not a
regular response. If :throw-entire-message? is set to true, the entire
response is used as the message, instead of just the status number."
response is used as the message, instead of just the status number.

The thrown exception carries the response and the originating request, which
may include credentials (e.g. an Authorization header). Headers whose
lower-cased name is in the request's :redact-headers option have their values
replaced with \"REDACTED\" in both the response and request headers before the
exception is thrown. :redact-headers defaults to `default-redact-headers`
(authorization, proxy-authorization); pass your own set to override it, or
`#{}` to disable redaction."
[client]
(fn
([req]
Expand Down
36 changes: 36 additions & 0 deletions test/clj_http/test/client_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,42 @@
(catch Object _
(is false ":type selector was not caught.")))))

(deftest redact-headers-in-exceptions
(testing "request Authorization header is redacted by default (#290)"
(let [client (fn [req] {:status 500 :request req})
e-client (client/wrap-exceptions client)]
(try+
(e-client {:headers {"Authorization" "Bearer secret" "X-Ok" "fine"}})
(is false "should have thrown")
(catch map? data
(is (= "REDACTED" (get-in data [:request :headers "Authorization"])))
(is (= "fine" (get-in data [:request :headers "X-Ok"])))))))
(testing "default also redacts proxy-authorization"
(let [client (fn [req] {:status 500 :request req})
e-client (client/wrap-exceptions client)]
(try+
(e-client {:headers {"proxy-authorization" "Basic zzz"}})
(catch map? data
(is (= "REDACTED" (get-in data [:request :headers "proxy-authorization"])))))))
(testing "custom :redact-headers redacts the named response headers"
(let [client (fn [req] {:status 500
:headers {"x-secret" "v" "content-type" "text/plain"}})
e-client (client/wrap-exceptions client)]
(try+
(e-client {:redact-headers #{"x-secret"}})
(catch map? data
(is (= "REDACTED" (get-in data [:headers "x-secret"])))
(is (= "text/plain" (get-in data [:headers "content-type"])))))))
(testing ":redact-headers #{} disables redaction"
(let [client (fn [req] {:status 500 :request req})
e-client (client/wrap-exceptions client)]
(try+
(e-client {:redact-headers #{}
:headers {"Authorization" "Bearer secret"}})
(catch map? data
(is (= "Bearer secret"
(get-in data [:request :headers "Authorization"]))))))))

(deftest throw-on-exceptional-async
(let [client (fn [req respond raise]
(try
Expand Down