logging: add support for hashing data (#4434)

* logging: add support for hashing data

* Update modules/logging/filters.go

Co-authored-by: wiese <wiese@users.noreply.github.com>

* Update modules/logging/filters.go

Co-authored-by: wiese <wiese@users.noreply.github.com>

Co-authored-by: wiese <wiese@users.noreply.github.com>
This commit is contained in:
Kévin Dunglas 2021-12-02 21:51:37 +01:00 committed by GitHub
parent 5bf0adad87
commit a1b417c832
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 100 additions and 13 deletions

View file

@ -8,18 +8,21 @@ log {
uri query { uri query {
replace foo REDACTED replace foo REDACTED
delete bar delete bar
hash baz
} }
request>headers>Authorization replace REDACTED request>headers>Authorization replace REDACTED
request>headers>Server delete request>headers>Server delete
request>headers>Cookie cookie { request>headers>Cookie cookie {
replace foo REDACTED replace foo REDACTED
delete bar delete bar
hash baz
} }
request>remote_ip ip_mask { request>remote_ip ip_mask {
ipv4 24 ipv4 24
ipv6 32 ipv6 32
} }
request>headers>Regexp regexp secret REDACTED request>headers>Regexp regexp secret REDACTED
request>headers>Hash hash
} }
} }
} }
@ -52,10 +55,17 @@ log {
{ {
"name": "bar", "name": "bar",
"type": "delete" "type": "delete"
},
{
"name": "baz",
"type": "hash"
} }
], ],
"filter": "cookie" "filter": "cookie"
}, },
"request\u003eheaders\u003eHash": {
"filter": "hash"
},
"request\u003eheaders\u003eRegexp": { "request\u003eheaders\u003eRegexp": {
"filter": "regexp", "filter": "regexp",
"regexp": "secret", "regexp": "secret",
@ -79,6 +89,10 @@ log {
{ {
"parameter": "bar", "parameter": "bar",
"type": "delete" "type": "delete"
},
{
"parameter": "baz",
"type": "hash"
} }
], ],
"filter": "query" "filter": "query"

View file

@ -15,7 +15,9 @@
package logging package logging
import ( import (
"crypto/sha256"
"errors" "errors"
"fmt"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
@ -34,6 +36,7 @@ func init() {
caddy.RegisterModule(QueryFilter{}) caddy.RegisterModule(QueryFilter{})
caddy.RegisterModule(CookieFilter{}) caddy.RegisterModule(CookieFilter{})
caddy.RegisterModule(RegexpFilter{}) caddy.RegisterModule(RegexpFilter{})
caddy.RegisterModule(HashFilter{})
} }
// LogFieldFilter can filter (or manipulate) // LogFieldFilter can filter (or manipulate)
@ -65,6 +68,35 @@ func (DeleteFilter) Filter(in zapcore.Field) zapcore.Field {
return in return in
} }
// hash returns the first 4 bytes of the SHA-256 hash of the given data as hexadecimal
func hash(s string) string {
return fmt.Sprintf("%.4x", sha256.Sum256([]byte(s)))
}
// HashFilter is a Caddy log field filter that
// replaces the field with the initial 4 bytes of the SHA-256 hash of the content.
type HashFilter struct {
}
// CaddyModule returns the Caddy module information.
func (HashFilter) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "caddy.logging.encoders.filter.hash",
New: func() caddy.Module { return new(HashFilter) },
}
}
// UnmarshalCaddyfile sets up the module from Caddyfile tokens.
func (f *HashFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return nil
}
// Filter filters the input field with the replacement value.
func (f *HashFilter) Filter(in zapcore.Field) zapcore.Field {
in.String = hash(in.String)
return in
}
// ReplaceFilter is a Caddy log field filter that // ReplaceFilter is a Caddy log field filter that
// replaces the field with the indicated string. // replaces the field with the indicated string.
type ReplaceFilter struct { type ReplaceFilter struct {
@ -195,15 +227,19 @@ func (m IPMaskFilter) Filter(in zapcore.Field) zapcore.Field {
type filterAction string type filterAction string
const ( const (
// Replace value(s) of query parameter(s). // Replace value(s).
replaceAction filterAction = "replace" replaceAction filterAction = "replace"
// Delete query parameter(s).
// Hash value(s).
hashAction filterAction = "hash"
// Delete.
deleteAction filterAction = "delete" deleteAction filterAction = "delete"
) )
func (a filterAction) IsValid() error { func (a filterAction) IsValid() error {
switch a { switch a {
case replaceAction, deleteAction: case replaceAction, deleteAction, hashAction:
return nil return nil
} }
@ -211,7 +247,7 @@ func (a filterAction) IsValid() error {
} }
type queryFilterAction struct { type queryFilterAction struct {
// `replace` to replace the value(s) associated with the parameter(s) or `delete` to remove them entirely. // `replace` to replace the value(s) associated with the parameter(s), `hash` to replace them with the 4 initial bytes of the SHA-256 of their content or `delete` to remove them entirely.
Type filterAction `json:"type"` Type filterAction `json:"type"`
// The name of the query parameter. // The name of the query parameter.
@ -224,9 +260,9 @@ type queryFilterAction struct {
// QueryFilter is a Caddy log field filter that filters // QueryFilter is a Caddy log field filter that filters
// query parameters from a URL. // query parameters from a URL.
// //
// This filter updates the logged URL string to remove or replace query // This filter updates the logged URL string to remove, replace or hash
// parameters containing sensitive data. For instance, it can be used // query parameters containing sensitive data. For instance, it can be
// to redact any kind of secrets which were passed as query parameters, // used to redact any kind of secrets which were passed as query parameters,
// such as OAuth access tokens, session IDs, magic link tokens, etc. // such as OAuth access tokens, session IDs, magic link tokens, etc.
type QueryFilter struct { type QueryFilter struct {
// A list of actions to apply to the query parameters of the URL. // A list of actions to apply to the query parameters of the URL.
@ -271,6 +307,14 @@ func (m *QueryFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
} }
qfa.Value = d.Val() qfa.Value = d.Val()
case "hash":
if !d.NextArg() {
return d.ArgErr()
}
qfa.Type = hashAction
qfa.Parameter = d.Val()
case "delete": case "delete":
if !d.NextArg() { if !d.NextArg() {
return d.ArgErr() return d.ArgErr()
@ -304,6 +348,11 @@ func (m QueryFilter) Filter(in zapcore.Field) zapcore.Field {
q[a.Parameter][i] = a.Value q[a.Parameter][i] = a.Value
} }
case hashAction:
for i := range q[a.Parameter] {
q[a.Parameter][i] = hash(a.Value)
}
case deleteAction: case deleteAction:
q.Del(a.Parameter) q.Del(a.Parameter)
} }
@ -316,7 +365,7 @@ func (m QueryFilter) Filter(in zapcore.Field) zapcore.Field {
} }
type cookieFilterAction struct { type cookieFilterAction struct {
// `replace` to replace the value of the cookie or `delete` to remove it entirely. // `replace` to replace the value of the cookie, `hash` to replace it with the 4 initial bytes of the SHA-256 of its content or `delete` to remove it entirely.
Type filterAction `json:"type"` Type filterAction `json:"type"`
// The name of the cookie. // The name of the cookie.
@ -330,7 +379,7 @@ type cookieFilterAction struct {
// cookies. // cookies.
// //
// This filter updates the logged HTTP header string // This filter updates the logged HTTP header string
// to remove or replace cookies containing sensitive data. For instance, // to remove, replace or hash cookies containing sensitive data. For instance,
// it can be used to redact any kind of secrets, such as session IDs. // 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 // If several actions are configured for the same cookie name, only the first
@ -378,6 +427,14 @@ func (m *CookieFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
} }
cfa.Value = d.Val() cfa.Value = d.Val()
case "hash":
if !d.NextArg() {
return d.ArgErr()
}
cfa.Type = hashAction
cfa.Name = d.Val()
case "delete": case "delete":
if !d.NextArg() { if !d.NextArg() {
return d.ArgErr() return d.ArgErr()
@ -415,6 +472,11 @@ OUTER:
transformedRequest.AddCookie(c) transformedRequest.AddCookie(c)
continue OUTER continue OUTER
case hashAction:
c.Value = hash(c.Value)
transformedRequest.AddCookie(c)
continue OUTER
case deleteAction: case deleteAction:
continue OUTER continue OUTER
} }

View file

@ -13,14 +13,15 @@ func TestQueryFilter(t *testing.T) {
{replaceAction, "notexist", "REDACTED"}, {replaceAction, "notexist", "REDACTED"},
{deleteAction, "bar", ""}, {deleteAction, "bar", ""},
{deleteAction, "notexist", ""}, {deleteAction, "notexist", ""},
{hashAction, "hash", ""},
}} }}
if f.Validate() != nil { if f.Validate() != nil {
t.Fatalf("the filter must be valid") t.Fatalf("the filter must be valid")
} }
out := f.Filter(zapcore.Field{String: "/path?foo=a&foo=b&bar=c&bar=d&baz=e"}) out := f.Filter(zapcore.Field{String: "/path?foo=a&foo=b&bar=c&bar=d&baz=e&hash=hashed"})
if out.String != "/path?baz=e&foo=REDACTED&foo=REDACTED" { if out.String != "/path?baz=e&foo=REDACTED&foo=REDACTED&hash=e3b0c442" {
t.Fatalf("query parameters have not been filtered: %s", out.String) t.Fatalf("query parameters have not been filtered: %s", out.String)
} }
} }
@ -45,10 +46,11 @@ func TestCookieFilter(t *testing.T) {
f := CookieFilter{[]cookieFilterAction{ f := CookieFilter{[]cookieFilterAction{
{replaceAction, "foo", "REDACTED"}, {replaceAction, "foo", "REDACTED"},
{deleteAction, "bar", ""}, {deleteAction, "bar", ""},
{hashAction, "hash", ""},
}} }}
out := f.Filter(zapcore.Field{String: "foo=a; foo=b; bar=c; bar=d; baz=e"}) out := f.Filter(zapcore.Field{String: "foo=a; foo=b; bar=c; bar=d; baz=e; hash=hashed"})
if out.String != "foo=REDACTED; foo=REDACTED; baz=e" { if out.String != "foo=REDACTED; foo=REDACTED; baz=e; hash=1a06df82" {
t.Fatalf("cookies have not been filtered: %s", out.String) t.Fatalf("cookies have not been filtered: %s", out.String)
} }
} }
@ -78,3 +80,12 @@ func TestRegexpFilter(t *testing.T) {
t.Fatalf("field has not been filtered: %s", out.String) t.Fatalf("field has not been filtered: %s", out.String)
} }
} }
func TestHashFilter(t *testing.T) {
f := HashFilter{}
out := f.Filter(zapcore.Field{String: "foo"})
if out.String != "2c26b46b" {
t.Fatalf("field has not been filtered: %s", out.String)
}
}