logging: add a filter for cookies (#4425)

* feat(logging): add a filter for cookies

* Improve godoc and add validation
This commit is contained in:
Kévin Dunglas 2021-11-23 17:40:20 +01:00 committed by GitHub
parent bcac2beee7
commit 8887adb027
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 163 additions and 0 deletions

View File

@ -11,6 +11,10 @@ log {
}
request>headers>Authorization replace REDACTED
request>headers>Server delete
request>headers>Cookie cookie {
replace foo REDACTED
delete bar
}
request>remote_addr ip_mask {
ipv4 24
ipv6 32
@ -37,6 +41,20 @@ log {
"filter": "replace",
"value": "REDACTED"
},
"request\u003eheaders\u003eCookie": {
"actions": [
{
"name": "foo",
"type": "replace",
"value": "REDACTED"
},
{
"name": "bar",
"type": "delete"
}
],
"filter": "cookie"
},
"request\u003eheaders\u003eServer": {
"filter": "delete"
},

View File

@ -17,6 +17,7 @@ package logging
import (
"errors"
"net"
"net/http"
"net/url"
"strconv"
@ -30,6 +31,7 @@ func init() {
caddy.RegisterModule(ReplaceFilter{})
caddy.RegisterModule(IPMaskFilter{})
caddy.RegisterModule(QueryFilter{})
caddy.RegisterModule(CookieFilter{})
}
// LogFieldFilter can filter (or manipulate)
@ -311,17 +313,132 @@ func (m QueryFilter) Filter(in zapcore.Field) zapcore.Field {
return in
}
type cookieFilterAction struct {
// `replace` to replace the value of the cookie or `delete` to remove it entirely.
Type filterAction `json:"type"`
// The name of the cookie.
Name string `json:"name"`
// The value to use as replacement if the action is `replace`.
Value string `json:"value,omitempty"`
}
// CookieFilter is a Caddy log field filter that filters
// cookies.
//
// This filter updates the logged HTTP header string
// to remove or replace cookies containing sensitive data. For instance,
// it can be used to redact any kind of secrets, such as session IDs.
//
// If several actions are configured for the same cookie name, only the first
// will be applied.
type CookieFilter struct {
// A list of actions to apply to the cookies.
Actions []cookieFilterAction `json:"actions"`
}
// Validate checks that action types are correct.
func (f *CookieFilter) Validate() error {
for _, a := range f.Actions {
if err := a.Type.IsValid(); err != nil {
return err
}
}
return nil
}
// CaddyModule returns the Caddy module information.
func (CookieFilter) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "caddy.logging.encoders.filter.cookie",
New: func() caddy.Module { return new(CookieFilter) },
}
}
// UnmarshalCaddyfile sets up the module from Caddyfile tokens.
func (m *CookieFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
for d.Next() {
for d.NextBlock(0) {
cfa := cookieFilterAction{}
switch d.Val() {
case "replace":
if !d.NextArg() {
return d.ArgErr()
}
cfa.Type = replaceAction
cfa.Name = d.Val()
if !d.NextArg() {
return d.ArgErr()
}
cfa.Value = d.Val()
case "delete":
if !d.NextArg() {
return d.ArgErr()
}
cfa.Type = deleteAction
cfa.Name = d.Val()
default:
return d.Errf("unrecognized subdirective %s", d.Val())
}
m.Actions = append(m.Actions, cfa)
}
}
return nil
}
// Filter filters the input field.
func (m CookieFilter) Filter(in zapcore.Field) zapcore.Field {
originRequest := http.Request{Header: http.Header{"Cookie": []string{in.String}}}
cookies := originRequest.Cookies()
transformedRequest := http.Request{Header: make(http.Header)}
OUTER:
for _, c := range cookies {
for _, a := range m.Actions {
if c.Name != a.Name {
continue
}
switch a.Type {
case replaceAction:
c.Value = a.Value
transformedRequest.AddCookie(c)
continue OUTER
case deleteAction:
continue OUTER
}
}
transformedRequest.AddCookie(c)
}
in.String = transformedRequest.Header.Get("Cookie")
return in
}
// Interface guards
var (
_ LogFieldFilter = (*DeleteFilter)(nil)
_ LogFieldFilter = (*ReplaceFilter)(nil)
_ LogFieldFilter = (*IPMaskFilter)(nil)
_ LogFieldFilter = (*QueryFilter)(nil)
_ LogFieldFilter = (*CookieFilter)(nil)
_ caddyfile.Unmarshaler = (*DeleteFilter)(nil)
_ caddyfile.Unmarshaler = (*ReplaceFilter)(nil)
_ caddyfile.Unmarshaler = (*IPMaskFilter)(nil)
_ caddyfile.Unmarshaler = (*QueryFilter)(nil)
_ caddyfile.Unmarshaler = (*CookieFilter)(nil)
_ caddy.Provisioner = (*IPMaskFilter)(nil)

View File

@ -39,3 +39,31 @@ func TestValidateQueryFilter(t *testing.T) {
t.Fatalf("unknown action type must be invalid")
}
}
func TestCookieFilter(t *testing.T) {
f := CookieFilter{[]cookieFilterAction{
{replaceAction, "foo", "REDACTED"},
{deleteAction, "bar", ""},
}}
out := f.Filter(zapcore.Field{String: "foo=a; foo=b; bar=c; bar=d; baz=e"})
if out.String != "foo=REDACTED; foo=REDACTED; baz=e" {
t.Fatalf("cookies have not been filtered: %s", out.String)
}
}
func TestValidateCookieFilter(t *testing.T) {
f := CookieFilter{[]cookieFilterAction{
{},
}}
if f.Validate() == nil {
t.Fatalf("empty action type must be invalid")
}
f = CookieFilter{[]cookieFilterAction{
{Type: "foo"},
}}
if f.Validate() == nil {
t.Fatalf("unknown action type must be invalid")
}
}