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>
This commit is contained in:
Kévin Dunglas 2021-11-23 10:01:43 +01:00 committed by GitHub
parent 1e10f6f725
commit bcac2beee7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 189 additions and 0 deletions

View file

@ -5,6 +5,10 @@ log {
format filter { format filter {
wrap console wrap console
fields { fields {
uri query {
replace foo REDACTED
delete bar
}
request>headers>Authorization replace REDACTED request>headers>Authorization replace REDACTED
request>headers>Server delete request>headers>Server delete
request>remote_addr ip_mask { request>remote_addr ip_mask {
@ -40,6 +44,20 @@ log {
"filter": "ip_mask", "filter": "ip_mask",
"ipv4_cidr": 24, "ipv4_cidr": 24,
"ipv6_cidr": 32 "ipv6_cidr": 32
},
"uri": {
"actions": [
{
"parameter": "foo",
"type": "replace",
"value": "REDACTED"
},
{
"parameter": "bar",
"type": "delete"
}
],
"filter": "query"
} }
}, },
"format": "filter", "format": "filter",

View file

@ -15,7 +15,9 @@
package logging package logging
import ( import (
"errors"
"net" "net"
"net/url"
"strconv" "strconv"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
@ -27,6 +29,7 @@ func init() {
caddy.RegisterModule(DeleteFilter{}) caddy.RegisterModule(DeleteFilter{})
caddy.RegisterModule(ReplaceFilter{}) caddy.RegisterModule(ReplaceFilter{})
caddy.RegisterModule(IPMaskFilter{}) caddy.RegisterModule(IPMaskFilter{})
caddy.RegisterModule(QueryFilter{})
} }
// LogFieldFilter can filter (or manipulate) // LogFieldFilter can filter (or manipulate)
@ -185,15 +188,142 @@ func (m IPMaskFilter) Filter(in zapcore.Field) zapcore.Field {
return in 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 // Interface guards
var ( var (
_ LogFieldFilter = (*DeleteFilter)(nil) _ LogFieldFilter = (*DeleteFilter)(nil)
_ LogFieldFilter = (*ReplaceFilter)(nil) _ LogFieldFilter = (*ReplaceFilter)(nil)
_ LogFieldFilter = (*IPMaskFilter)(nil) _ LogFieldFilter = (*IPMaskFilter)(nil)
_ LogFieldFilter = (*QueryFilter)(nil)
_ caddyfile.Unmarshaler = (*DeleteFilter)(nil) _ caddyfile.Unmarshaler = (*DeleteFilter)(nil)
_ caddyfile.Unmarshaler = (*ReplaceFilter)(nil) _ caddyfile.Unmarshaler = (*ReplaceFilter)(nil)
_ caddyfile.Unmarshaler = (*IPMaskFilter)(nil) _ caddyfile.Unmarshaler = (*IPMaskFilter)(nil)
_ caddyfile.Unmarshaler = (*QueryFilter)(nil)
_ caddy.Provisioner = (*IPMaskFilter)(nil) _ caddy.Provisioner = (*IPMaskFilter)(nil)
_ caddy.Validator = (*QueryFilter)(nil)
) )

View file

@ -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")
}
}