mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-27 06:03:48 +03:00
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:
parent
1e10f6f725
commit
bcac2beee7
3 changed files with 189 additions and 0 deletions
|
@ -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",
|
||||||
|
|
|
@ -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)
|
||||||
)
|
)
|
||||||
|
|
41
modules/logging/filters_test.go
Normal file
41
modules/logging/filters_test.go
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue