mirror of https://github.com/caddyserver/caddy.git
logging: add a filter for cookies (#4425)
* feat(logging): add a filter for cookies * Improve godoc and add validation
This commit is contained in:
parent
bcac2beee7
commit
8887adb027
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue