caddyhttp: Make logging of credential headers opt-in (#4438)

This commit is contained in:
Francis Lavoie 2021-12-02 15:26:24 -05:00 committed by GitHub
parent 8e5aafa5cd
commit 5bf0adad87
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 81 additions and 24 deletions

View file

@ -33,15 +33,16 @@ type serverOptions struct {
ListenerAddress string ListenerAddress string
// These will all map 1:1 to the caddyhttp.Server struct // These will all map 1:1 to the caddyhttp.Server struct
ListenerWrappersRaw []json.RawMessage ListenerWrappersRaw []json.RawMessage
ReadTimeout caddy.Duration ReadTimeout caddy.Duration
ReadHeaderTimeout caddy.Duration ReadHeaderTimeout caddy.Duration
WriteTimeout caddy.Duration WriteTimeout caddy.Duration
IdleTimeout caddy.Duration IdleTimeout caddy.Duration
MaxHeaderBytes int MaxHeaderBytes int
AllowH2C bool AllowH2C bool
ExperimentalHTTP3 bool ExperimentalHTTP3 bool
StrictSNIHost *bool StrictSNIHost *bool
ShouldLogCredentials bool
} }
func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (interface{}, error) { func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (interface{}, error) {
@ -134,6 +135,12 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (interface{}, error
} }
serverOpts.MaxHeaderBytes = int(size) serverOpts.MaxHeaderBytes = int(size)
case "log_credentials":
if d.NextArg() {
return nil, d.ArgErr()
}
serverOpts.ShouldLogCredentials = true
case "protocol": case "protocol":
for nesting := d.Nesting(); d.NextBlock(nesting); { for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() { switch d.Val() {
@ -222,6 +229,12 @@ func applyServerOptions(
server.AllowH2C = opts.AllowH2C server.AllowH2C = opts.AllowH2C
server.ExperimentalHTTP3 = opts.ExperimentalHTTP3 server.ExperimentalHTTP3 = opts.ExperimentalHTTP3
server.StrictSNIHost = opts.StrictSNIHost server.StrictSNIHost = opts.StrictSNIHost
if opts.ShouldLogCredentials {
if server.Logs == nil {
server.Logs = &caddyhttp.ServerLogConfig{}
}
server.Logs.ShouldLogCredentials = opts.ShouldLogCredentials
}
} }
return nil return nil

View file

@ -10,6 +10,7 @@
idle 30s idle 30s
} }
max_header_size 100MB max_header_size 100MB
log_credentials
protocol { protocol {
allow_h2c allow_h2c
experimental_http3 experimental_http3
@ -53,6 +54,9 @@ foo.com {
} }
], ],
"strict_sni_host": true, "strict_sni_host": true,
"logs": {
"should_log_credentials": true
},
"experimental_http3": true, "experimental_http3": true,
"allow_h2c": true "allow_h2c": true
} }

View file

@ -24,7 +24,11 @@ import (
) )
// LoggableHTTPRequest makes an HTTP request loggable with zap.Object(). // LoggableHTTPRequest makes an HTTP request loggable with zap.Object().
type LoggableHTTPRequest struct{ *http.Request } type LoggableHTTPRequest struct {
*http.Request
ShouldLogCredentials bool
}
// MarshalLogObject satisfies the zapcore.ObjectMarshaler interface. // MarshalLogObject satisfies the zapcore.ObjectMarshaler interface.
func (r LoggableHTTPRequest) MarshalLogObject(enc zapcore.ObjectEncoder) error { func (r LoggableHTTPRequest) MarshalLogObject(enc zapcore.ObjectEncoder) error {
@ -40,7 +44,10 @@ func (r LoggableHTTPRequest) MarshalLogObject(enc zapcore.ObjectEncoder) error {
enc.AddString("method", r.Method) enc.AddString("method", r.Method)
enc.AddString("host", r.Host) enc.AddString("host", r.Host)
enc.AddString("uri", r.RequestURI) enc.AddString("uri", r.RequestURI)
enc.AddObject("headers", LoggableHTTPHeader(r.Header)) enc.AddObject("headers", LoggableHTTPHeader{
Header: r.Header,
ShouldLogCredentials: r.ShouldLogCredentials,
})
if r.TLS != nil { if r.TLS != nil {
enc.AddObject("tls", LoggableTLSConnState(*r.TLS)) enc.AddObject("tls", LoggableTLSConnState(*r.TLS))
} }
@ -48,19 +55,25 @@ func (r LoggableHTTPRequest) MarshalLogObject(enc zapcore.ObjectEncoder) error {
} }
// LoggableHTTPHeader makes an HTTP header loggable with zap.Object(). // LoggableHTTPHeader makes an HTTP header loggable with zap.Object().
// Headers with potentially sensitive information (Cookie, Authorization, // Headers with potentially sensitive information (Cookie, Set-Cookie,
// and Proxy-Authorization) are logged with empty values. // Authorization, and Proxy-Authorization) are logged with empty values.
type LoggableHTTPHeader http.Header type LoggableHTTPHeader struct {
http.Header
ShouldLogCredentials bool
}
// MarshalLogObject satisfies the zapcore.ObjectMarshaler interface. // MarshalLogObject satisfies the zapcore.ObjectMarshaler interface.
func (h LoggableHTTPHeader) MarshalLogObject(enc zapcore.ObjectEncoder) error { func (h LoggableHTTPHeader) MarshalLogObject(enc zapcore.ObjectEncoder) error {
if h == nil { if h.Header == nil {
return nil return nil
} }
for key, val := range h { for key, val := range h.Header {
switch strings.ToLower(key) { if !h.ShouldLogCredentials {
case "cookie", "authorization", "proxy-authorization": switch strings.ToLower(key) {
val = []string{} case "cookie", "set-cookie", "authorization", "proxy-authorization":
val = []string{}
}
} }
enc.AddArray(key, LoggableStringArray(val)) enc.AddArray(key, LoggableStringArray(val))
} }

View file

@ -69,6 +69,8 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhtt
} }
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
server := r.Context().Value(caddyhttp.ServerCtxKey).(*caddyhttp.Server)
shouldLogCredentials := server.Logs != nil && server.Logs.ShouldLogCredentials
// create header for push requests // create header for push requests
hdr := h.initializePushHeaders(r, repl) hdr := h.initializePushHeaders(r, repl)
@ -79,7 +81,10 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhtt
zap.String("uri", r.RequestURI), zap.String("uri", r.RequestURI),
zap.String("push_method", resource.Method), zap.String("push_method", resource.Method),
zap.String("push_target", resource.Target), zap.String("push_target", resource.Target),
zap.Object("push_headers", caddyhttp.LoggableHTTPHeader(hdr))) zap.Object("push_headers", caddyhttp.LoggableHTTPHeader{
Header: hdr,
ShouldLogCredentials: shouldLogCredentials,
}))
err := pusher.Push(repl.ReplaceAll(resource.Target, "."), &http.PushOptions{ err := pusher.Push(repl.ReplaceAll(resource.Target, "."), &http.PushOptions{
Method: resource.Method, Method: resource.Method,
Header: hdr, Header: hdr,

View file

@ -574,6 +574,9 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, repl *
// point the request to this upstream // point the request to this upstream
h.directRequest(req, di) h.directRequest(req, di)
server := req.Context().Value(caddyhttp.ServerCtxKey).(*caddyhttp.Server)
shouldLogCredentials := server.Logs != nil && server.Logs.ShouldLogCredentials
// do the round-trip; emit debug log with values we know are // do the round-trip; emit debug log with values we know are
// safe, or if there is no error, emit fuller log entry // safe, or if there is no error, emit fuller log entry
start := time.Now() start := time.Now()
@ -582,14 +585,20 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, repl *
logger := h.logger.With( logger := h.logger.With(
zap.String("upstream", di.Upstream.String()), zap.String("upstream", di.Upstream.String()),
zap.Duration("duration", duration), zap.Duration("duration", duration),
zap.Object("request", caddyhttp.LoggableHTTPRequest{Request: req}), zap.Object("request", caddyhttp.LoggableHTTPRequest{
Request: req,
ShouldLogCredentials: shouldLogCredentials,
}),
) )
if err != nil { if err != nil {
logger.Debug("upstream roundtrip", zap.Error(err)) logger.Debug("upstream roundtrip", zap.Error(err))
return err return err
} }
logger.Debug("upstream roundtrip", logger.Debug("upstream roundtrip",
zap.Object("headers", caddyhttp.LoggableHTTPHeader(res.Header)), zap.Object("headers", caddyhttp.LoggableHTTPHeader{
Header: res.Header,
ShouldLogCredentials: shouldLogCredentials,
}),
zap.Int("status", res.StatusCode)) zap.Int("status", res.StatusCode))
// duration until upstream wrote response headers (roundtrip duration) // duration until upstream wrote response headers (roundtrip duration)

View file

@ -157,7 +157,11 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// it enters any handler chain; this is necessary // it enters any handler chain; this is necessary
// to capture the original request in case it gets // to capture the original request in case it gets
// modified during handling // modified during handling
loggableReq := zap.Object("request", LoggableHTTPRequest{r}) shouldLogCredentials := s.Logs != nil && s.Logs.ShouldLogCredentials
loggableReq := zap.Object("request", LoggableHTTPRequest{
Request: r,
ShouldLogCredentials: shouldLogCredentials,
})
errLog := s.errorLogger.With(loggableReq) errLog := s.errorLogger.With(loggableReq)
var duration time.Duration var duration time.Duration
@ -191,7 +195,10 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
zap.Duration("duration", duration), zap.Duration("duration", duration),
zap.Int("size", wrec.Size()), zap.Int("size", wrec.Size()),
zap.Int("status", wrec.Status()), zap.Int("status", wrec.Status()),
zap.Object("resp_headers", LoggableHTTPHeader(wrec.Header())), zap.Object("resp_headers", LoggableHTTPHeader{
Header: wrec.Header(),
ShouldLogCredentials: shouldLogCredentials,
}),
) )
}() }()
} }
@ -508,6 +515,12 @@ type ServerLogConfig struct {
// If true, requests to any host not appearing in the // If true, requests to any host not appearing in the
// LoggerNames (logger_names) map will not be logged. // LoggerNames (logger_names) map will not be logged.
SkipUnmappedHosts bool `json:"skip_unmapped_hosts,omitempty"` SkipUnmappedHosts bool `json:"skip_unmapped_hosts,omitempty"`
// If true, credentials that are otherwise omitted, will be logged.
// The definition of credentials is defined by https://fetch.spec.whatwg.org/#credentials,
// and this includes some request and response headers, i.e `Cookie`,
// `Set-Cookie`, `Authorization`, and `Proxy-Authorization`.
ShouldLogCredentials bool `json:"should_log_credentials,omitempty"`
} }
// wrapLogger wraps logger in a logger named according to user preferences for the given host. // wrapLogger wraps logger in a logger named according to user preferences for the given host.