summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKévin Dunglas <dunglas@gmail.com>2021-11-23 10:01:43 +0100
committerGitHub <noreply@github.com>2021-11-23 04:01:43 -0500
commitbcac2beee7e419f8cdab2ed16f388d1af282a46b (patch)
treec8f3ca5424b3648cbfff4bd12f3d41ba77b70879
parent1e10f6f725189a371a923a329084f1c3f608ae38 (diff)
logging: add a filter for query parameters (#4424)
Co-authored-by: Matt Holt <mholt@users.noreply.github.com> Co-authored-by: Francis Lavoie <lavofr@gmail.com>
-rw-r--r--caddytest/integration/caddyfile_adapt/log_filters.txt18
-rw-r--r--modules/logging/filters.go130
-rw-r--r--modules/logging/filters_test.go41
3 files changed, 189 insertions, 0 deletions
diff --git a/caddytest/integration/caddyfile_adapt/log_filters.txt b/caddytest/integration/caddyfile_adapt/log_filters.txt
index 0949c1d..7873b1c 100644
--- a/caddytest/integration/caddyfile_adapt/log_filters.txt
+++ b/caddytest/integration/caddyfile_adapt/log_filters.txt
@@ -5,6 +5,10 @@ log {
format filter {
wrap console
fields {
+ uri query {
+ replace foo REDACTED
+ delete bar
+ }
request>headers>Authorization replace REDACTED
request>headers>Server delete
request>remote_addr ip_mask {
@@ -40,6 +44,20 @@ log {
"filter": "ip_mask",
"ipv4_cidr": 24,
"ipv6_cidr": 32
+ },
+ "uri": {
+ "actions": [
+ {
+ "parameter": "foo",
+ "type": "replace",
+ "value": "REDACTED"
+ },
+ {
+ "parameter": "bar",
+ "type": "delete"
+ }
+ ],
+ "filter": "query"
}
},
"format": "filter",
diff --git a/modules/logging/filters.go b/modules/logging/filters.go
index ef5a4cb..ceb0d8a 100644
--- a/modules/logging/filters.go
+++ b/modules/logging/filters.go
@@ -15,7 +15,9 @@
package logging
import (
+ "errors"
"net"
+ "net/url"
"strconv"
"github.com/caddyserver/caddy/v2"
@@ -27,6 +29,7 @@ func init() {
caddy.RegisterModule(DeleteFilter{})
caddy.RegisterModule(ReplaceFilter{})
caddy.RegisterModule(IPMaskFilter{})
+ caddy.RegisterModule(QueryFilter{})
}
// LogFieldFilter can filter (or manipulate)
@@ -185,15 +188,142 @@ func (m IPMaskFilter) Filter(in zapcore.Field) zapcore.Field {
return in
}
+type filterAction string
+
+const (
+ // Replace value(s) of query parameter(s).
+ replaceAction filterAction = "replace"
+ // Delete query parameter(s).
+ deleteAction filterAction = "delete"
+)
+
+func (a filterAction) IsValid() error {
+ switch a {
+ case replaceAction, deleteAction:
+ return nil
+ }
+
+ return errors.New("invalid action type")
+}
+
+type queryFilterAction struct {
+ // `replace` to replace the value(s) associated with the parameter(s) or `delete` to remove them entirely.
+ Type filterAction `json:"type"`
+
+ // The name of the query parameter.
+ Parameter string `json:"parameter"`
+
+ // The value to use as replacement if the action is `replace`.
+ Value string `json:"value,omitempty"`
+}
+
+// QueryFilter is a Caddy log field filter that filters
+// query parameters from a URL.
+//
+// This filter updates the logged URL string to remove or replace query
+// parameters containing sensitive data. For instance, it can be used
+// to redact any kind of secrets which were passed as query parameters,
+// such as OAuth access tokens, session IDs, magic link tokens, etc.
+type QueryFilter struct {
+ // A list of actions to apply to the query parameters of the URL.
+ Actions []queryFilterAction `json:"actions"`
+}
+
+// Validate checks that action types are correct.
+func (f *QueryFilter) 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 (QueryFilter) CaddyModule() caddy.ModuleInfo {
+ return caddy.ModuleInfo{
+ ID: "caddy.logging.encoders.filter.query",
+ New: func() caddy.Module { return new(QueryFilter) },
+ }
+}
+
+// UnmarshalCaddyfile sets up the module from Caddyfile tokens.
+func (m *QueryFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
+ for d.Next() {
+ for d.NextBlock(0) {
+ qfa := queryFilterAction{}
+ switch d.Val() {
+ case "replace":
+ if !d.NextArg() {
+ return d.ArgErr()
+ }
+
+ qfa.Type = replaceAction
+ qfa.Parameter = d.Val()
+
+ if !d.NextArg() {
+ return d.ArgErr()
+ }
+ qfa.Value = d.Val()
+
+ case "delete":
+ if !d.NextArg() {
+ return d.ArgErr()
+ }
+
+ qfa.Type = deleteAction
+ qfa.Parameter = d.Val()
+
+ default:
+ return d.Errf("unrecognized subdirective %s", d.Val())
+ }
+
+ m.Actions = append(m.Actions, qfa)
+ }
+ }
+ return nil
+}
+
+// Filter filters the input field.
+func (m QueryFilter) Filter(in zapcore.Field) zapcore.Field {
+ u, err := url.Parse(in.String)
+ if err != nil {
+ return in
+ }
+
+ q := u.Query()
+ for _, a := range m.Actions {
+ switch a.Type {
+ case replaceAction:
+ for i := range q[a.Parameter] {
+ q[a.Parameter][i] = a.Value
+ }
+
+ case deleteAction:
+ q.Del(a.Parameter)
+ }
+ }
+
+ u.RawQuery = q.Encode()
+ in.String = u.String()
+
+ return in
+}
+
// Interface guards
var (
_ LogFieldFilter = (*DeleteFilter)(nil)
_ LogFieldFilter = (*ReplaceFilter)(nil)
_ LogFieldFilter = (*IPMaskFilter)(nil)
+ _ LogFieldFilter = (*QueryFilter)(nil)
_ caddyfile.Unmarshaler = (*DeleteFilter)(nil)
_ caddyfile.Unmarshaler = (*ReplaceFilter)(nil)
_ caddyfile.Unmarshaler = (*IPMaskFilter)(nil)
+ _ caddyfile.Unmarshaler = (*QueryFilter)(nil)
_ caddy.Provisioner = (*IPMaskFilter)(nil)
+
+ _ caddy.Validator = (*QueryFilter)(nil)
)
diff --git a/modules/logging/filters_test.go b/modules/logging/filters_test.go
new file mode 100644
index 0000000..883a138
--- /dev/null
+++ b/modules/logging/filters_test.go
@@ -0,0 +1,41 @@
+package logging
+
+import (
+ "testing"
+
+ "go.uber.org/zap/zapcore"
+)
+
+func TestQueryFilter(t *testing.T) {
+ f := QueryFilter{[]queryFilterAction{
+ {replaceAction, "foo", "REDACTED"},
+ {replaceAction, "notexist", "REDACTED"},
+ {deleteAction, "bar", ""},
+ {deleteAction, "notexist", ""},
+ }}
+
+ if f.Validate() != nil {
+ t.Fatalf("the filter must be valid")
+ }
+
+ out := f.Filter(zapcore.Field{String: "/path?foo=a&foo=b&bar=c&bar=d&baz=e"})
+ if out.String != "/path?baz=e&foo=REDACTED&foo=REDACTED" {
+ t.Fatalf("query parameters have not been filtered: %s", out.String)
+ }
+}
+
+func TestValidateQueryFilter(t *testing.T) {
+ f := QueryFilter{[]queryFilterAction{
+ {},
+ }}
+ if f.Validate() == nil {
+ t.Fatalf("empty action type must be invalid")
+ }
+
+ f = QueryFilter{[]queryFilterAction{
+ {Type: "foo"},
+ }}
+ if f.Validate() == nil {
+ t.Fatalf("unknown action type must be invalid")
+ }
+}