Skip to content

Commit da6fd61

Browse files
committed
admin: Redact sensitive request headers in API logs
1 parent df65455 commit da6fd61

2 files changed

Lines changed: 84 additions & 1 deletion

File tree

admin.go

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -800,7 +800,7 @@ func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
800800
zap.String("uri", r.RequestURI),
801801
zap.String("remote_ip", ip),
802802
zap.String("remote_port", port),
803-
zap.Reflect("headers", r.Header),
803+
zap.Object("headers", adminLoggableHTTPHeader{Header: r.Header}),
804804
)
805805
if r.TLS != nil {
806806
log = log.With(
@@ -816,6 +816,42 @@ func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
816816
h.serveHTTP(w, r)
817817
}
818818

819+
// adminLoggableHTTPHeader makes an HTTP header loggable with zap.Object().
820+
// Headers with potentially sensitive information (Cookie, Set-Cookie,
821+
// Authorization, and Proxy-Authorization) are logged with empty values.
822+
type adminLoggableHTTPHeader struct {
823+
http.Header
824+
}
825+
826+
// MarshalLogObject satisfies the zapcore.ObjectMarshaler interface.
827+
func (h adminLoggableHTTPHeader) MarshalLogObject(enc zapcore.ObjectEncoder) error {
828+
if h.Header == nil {
829+
return nil
830+
}
831+
for key, val := range h.Header {
832+
switch strings.ToLower(key) {
833+
case "cookie", "set-cookie", "authorization", "proxy-authorization":
834+
val = []string{"REDACTED"} // see #5669. I still think ▒▒▒▒ would be cool.
835+
}
836+
enc.AddArray(key, adminLoggableStringArray(val))
837+
}
838+
return nil
839+
}
840+
841+
// adminLoggableStringArray makes a slice of strings marshalable for logging.
842+
type adminLoggableStringArray []string
843+
844+
// MarshalLogArray satisfies the zapcore.ArrayMarshaler interface.
845+
func (sa adminLoggableStringArray) MarshalLogArray(enc zapcore.ArrayEncoder) error {
846+
if sa == nil {
847+
return nil
848+
}
849+
for _, s := range sa {
850+
enc.AppendString(s)
851+
}
852+
return nil
853+
}
854+
819855
// serveHTTP is the internal entry point for API requests. It may
820856
// be called more than once per request, for example if a request
821857
// is rewritten (i.e. internal redirect).

admin_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import (
3131
"github.com/caddyserver/certmagic"
3232
"github.com/prometheus/client_golang/prometheus"
3333
dto "github.com/prometheus/client_model/go"
34+
"go.uber.org/zap"
35+
"go.uber.org/zap/zaptest/observer"
3436
)
3537

3638
var testCfg = []byte(`{
@@ -242,6 +244,51 @@ func TestAdminHandlerErrorHandling(t *testing.T) {
242244
}
243245
}
244246

247+
func TestAdminHandlerServeHTTPRedactsSensitiveHeadersInLogs(t *testing.T) {
248+
core, logs := observer.New(zap.InfoLevel)
249+
250+
defaultLoggerMu.Lock()
251+
origLogger := defaultLogger.logger
252+
defaultLogger.logger = zap.New(core)
253+
defaultLoggerMu.Unlock()
254+
t.Cleanup(func() {
255+
defaultLoggerMu.Lock()
256+
defaultLogger.logger = origLogger
257+
defaultLoggerMu.Unlock()
258+
})
259+
260+
handler := adminHandler{
261+
mux: http.NewServeMux(),
262+
}
263+
req := httptest.NewRequest(http.MethodGet, "/", nil)
264+
req.Header.Set("Authorization", "Bearer secret")
265+
req.Header.Set("Cookie", "session=secret")
266+
req.Header.Set("X-Test", "ok")
267+
rr := httptest.NewRecorder()
268+
269+
handler.ServeHTTP(rr, req)
270+
271+
if logs.Len() == 0 {
272+
t.Fatal("expected request log entry")
273+
}
274+
275+
ctx := logs.All()[0].ContextMap()
276+
headers, ok := ctx["headers"].(map[string]any)
277+
if !ok {
278+
t.Fatalf("expected headers field in log context, got %T", ctx["headers"])
279+
}
280+
281+
if got := headers["Authorization"]; !reflect.DeepEqual(got, []any{"REDACTED"}) {
282+
t.Fatalf("expected redacted Authorization header, got %#v", got)
283+
}
284+
if got := headers["Cookie"]; !reflect.DeepEqual(got, []any{"REDACTED"}) {
285+
t.Fatalf("expected redacted Cookie header, got %#v", got)
286+
}
287+
if got := headers["X-Test"]; !reflect.DeepEqual(got, []any{"ok"}) {
288+
t.Fatalf("expected X-Test header to remain visible, got %#v", got)
289+
}
290+
}
291+
245292
func initAdminMetrics() {
246293
if adminMetrics.requestErrors != nil {
247294
prometheus.Unregister(adminMetrics.requestErrors)

0 commit comments

Comments
 (0)