summaryrefslogtreecommitdiff
path: root/modules/logging
diff options
context:
space:
mode:
authorKévin Dunglas <dunglas@gmail.com>2021-11-23 17:40:20 +0100
committerGitHub <noreply@github.com>2021-11-23 09:40:20 -0700
commit8887adb027982e844965b4707b8595cee5845d54 (patch)
tree00c61101dae9d248356512beed7b149000c25986 /modules/logging
parentbcac2beee7e419f8cdab2ed16f388d1af282a46b (diff)
logging: add a filter for cookies (#4425)
* feat(logging): add a filter for cookies * Improve godoc and add validation
Diffstat (limited to 'modules/logging')
-rw-r--r--modules/logging/filters.go117
-rw-r--r--modules/logging/filters_test.go28
2 files changed, 145 insertions, 0 deletions
diff --git a/modules/logging/filters.go b/modules/logging/filters.go
index ceb0d8a..cf3b5cc 100644
--- a/modules/logging/filters.go
+++ b/modules/logging/filters.go
@@ -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)
diff --git a/modules/logging/filters_test.go b/modules/logging/filters_test.go
index 883a138..6871bea 100644
--- a/modules/logging/filters_test.go
+++ b/modules/logging/filters_test.go
@@ -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")
+ }
+}