Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

logging: add support for hashing data #4434

Merged
merged 3 commits into from
Dec 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions caddytest/integration/caddyfile_adapt/log_filters.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,21 @@ log {
uri query {
replace foo REDACTED
delete bar
hash baz
}
request>headers>Authorization replace REDACTED
request>headers>Server delete
request>headers>Cookie cookie {
replace foo REDACTED
delete bar
hash baz
}
request>remote_addr ip_mask {
ipv4 24
ipv6 32
}
request>headers>Regexp regexp secret REDACTED
request>headers>Hash hash
}
}
}
Expand Down Expand Up @@ -52,10 +55,17 @@ log {
{
"name": "bar",
"type": "delete"
},
{
"name": "baz",
"type": "hash"
}
],
"filter": "cookie"
},
"request\u003eheaders\u003eHash": {
"filter": "hash"
},
"request\u003eheaders\u003eRegexp": {
"filter": "regexp",
"regexp": "secret",
Expand All @@ -79,6 +89,10 @@ log {
{
"parameter": "bar",
"type": "delete"
},
{
"parameter": "baz",
"type": "hash"
}
],
"filter": "query"
Expand Down
80 changes: 71 additions & 9 deletions modules/logging/filters.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
package logging

import (
"crypto/sha256"
"errors"
"fmt"
"net"
"net/http"
"net/url"
Expand All @@ -34,6 +36,7 @@ func init() {
caddy.RegisterModule(QueryFilter{})
caddy.RegisterModule(CookieFilter{})
caddy.RegisterModule(RegexpFilter{})
caddy.RegisterModule(HashFilter{})
}

// LogFieldFilter can filter (or manipulate)
Expand Down Expand Up @@ -65,6 +68,35 @@ func (DeleteFilter) Filter(in zapcore.Field) zapcore.Field {
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)))
mholt marked this conversation as resolved.
Show resolved Hide resolved
}

// 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
// replaces the field with the indicated string.
type ReplaceFilter struct {
Expand Down Expand Up @@ -195,23 +227,27 @@ func (m IPMaskFilter) Filter(in zapcore.Field) zapcore.Field {
type filterAction string

const (
// Replace value(s) of query parameter(s).
// Replace value(s).
replaceAction filterAction = "replace"
// Delete query parameter(s).

// Hash value(s).
hashAction filterAction = "hash"

// Delete.
deleteAction filterAction = "delete"
)

func (a filterAction) IsValid() error {
switch a {
case replaceAction, deleteAction:
case replaceAction, deleteAction, hashAction:
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.
// `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"`

// The name of the query parameter.
Expand All @@ -224,9 +260,9 @@ type queryFilterAction struct {
// 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,
// This filter updates the logged URL string to remove, replace or hash
// 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.
Expand Down Expand Up @@ -271,6 +307,14 @@ func (m *QueryFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
}
qfa.Value = d.Val()

case "hash":
if !d.NextArg() {
return d.ArgErr()
}

qfa.Type = hashAction
qfa.Parameter = d.Val()

case "delete":
if !d.NextArg() {
return d.ArgErr()
Expand Down Expand Up @@ -304,6 +348,11 @@ func (m QueryFilter) Filter(in zapcore.Field) zapcore.Field {
q[a.Parameter][i] = a.Value
}

case hashAction:
for i := range q[a.Parameter] {
q[a.Parameter][i] = hash(a.Value)
}

case deleteAction:
q.Del(a.Parameter)
}
Expand All @@ -316,7 +365,7 @@ func (m QueryFilter) Filter(in zapcore.Field) zapcore.Field {
}

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"`

// The name of the cookie.
Expand All @@ -330,7 +379,7 @@ type cookieFilterAction struct {
// cookies.
//
// 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.
//
// If several actions are configured for the same cookie name, only the first
Expand Down Expand Up @@ -378,6 +427,14 @@ func (m *CookieFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
}
cfa.Value = d.Val()

case "hash":
if !d.NextArg() {
return d.ArgErr()
}

cfa.Type = hashAction
cfa.Name = d.Val()

case "delete":
if !d.NextArg() {
return d.ArgErr()
Expand Down Expand Up @@ -415,6 +472,11 @@ OUTER:
transformedRequest.AddCookie(c)
continue OUTER

case hashAction:
c.Value = hash(c.Value)
transformedRequest.AddCookie(c)
continue OUTER

case deleteAction:
continue OUTER
}
Expand Down
19 changes: 15 additions & 4 deletions modules/logging/filters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ func TestQueryFilter(t *testing.T) {
{replaceAction, "notexist", "REDACTED"},
{deleteAction, "bar", ""},
{deleteAction, "notexist", ""},
{hashAction, "hash", ""},
}}

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" {
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&hash=e3b0c442" {
t.Fatalf("query parameters have not been filtered: %s", out.String)
}
}
Expand All @@ -45,10 +46,11 @@ func TestCookieFilter(t *testing.T) {
f := CookieFilter{[]cookieFilterAction{
{replaceAction, "foo", "REDACTED"},
{deleteAction, "bar", ""},
{hashAction, "hash", ""},
}}

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" {
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; hash=1a06df82" {
t.Fatalf("cookies have not been filtered: %s", out.String)
}
}
Expand Down Expand Up @@ -78,3 +80,12 @@ func TestRegexpFilter(t *testing.T) {
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)
}
}