Skip to content

Commit a1b417c

Browse files
dunglaswiese
andauthored
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>
1 parent 5bf0ada commit a1b417c

File tree

3 files changed

+100
-13
lines changed

3 files changed

+100
-13
lines changed

caddytest/integration/caddyfile_adapt/log_filters.txt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,21 @@ log {
88
uri query {
99
replace foo REDACTED
1010
delete bar
11+
hash baz
1112
}
1213
request>headers>Authorization replace REDACTED
1314
request>headers>Server delete
1415
request>headers>Cookie cookie {
1516
replace foo REDACTED
1617
delete bar
18+
hash baz
1719
}
1820
request>remote_ip ip_mask {
1921
ipv4 24
2022
ipv6 32
2123
}
2224
request>headers>Regexp regexp secret REDACTED
25+
request>headers>Hash hash
2326
}
2427
}
2528
}
@@ -52,10 +55,17 @@ log {
5255
{
5356
"name": "bar",
5457
"type": "delete"
58+
},
59+
{
60+
"name": "baz",
61+
"type": "hash"
5562
}
5663
],
5764
"filter": "cookie"
5865
},
66+
"request\u003eheaders\u003eHash": {
67+
"filter": "hash"
68+
},
5969
"request\u003eheaders\u003eRegexp": {
6070
"filter": "regexp",
6171
"regexp": "secret",
@@ -79,6 +89,10 @@ log {
7989
{
8090
"parameter": "bar",
8191
"type": "delete"
92+
},
93+
{
94+
"parameter": "baz",
95+
"type": "hash"
8296
}
8397
],
8498
"filter": "query"

modules/logging/filters.go

Lines changed: 71 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
package logging
1616

1717
import (
18+
"crypto/sha256"
1819
"errors"
20+
"fmt"
1921
"net"
2022
"net/http"
2123
"net/url"
@@ -34,6 +36,7 @@ func init() {
3436
caddy.RegisterModule(QueryFilter{})
3537
caddy.RegisterModule(CookieFilter{})
3638
caddy.RegisterModule(RegexpFilter{})
39+
caddy.RegisterModule(HashFilter{})
3740
}
3841

3942
// LogFieldFilter can filter (or manipulate)
@@ -65,6 +68,35 @@ func (DeleteFilter) Filter(in zapcore.Field) zapcore.Field {
6568
return in
6669
}
6770

71+
// hash returns the first 4 bytes of the SHA-256 hash of the given data as hexadecimal
72+
func hash(s string) string {
73+
return fmt.Sprintf("%.4x", sha256.Sum256([]byte(s)))
74+
}
75+
76+
// HashFilter is a Caddy log field filter that
77+
// replaces the field with the initial 4 bytes of the SHA-256 hash of the content.
78+
type HashFilter struct {
79+
}
80+
81+
// CaddyModule returns the Caddy module information.
82+
func (HashFilter) CaddyModule() caddy.ModuleInfo {
83+
return caddy.ModuleInfo{
84+
ID: "caddy.logging.encoders.filter.hash",
85+
New: func() caddy.Module { return new(HashFilter) },
86+
}
87+
}
88+
89+
// UnmarshalCaddyfile sets up the module from Caddyfile tokens.
90+
func (f *HashFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
91+
return nil
92+
}
93+
94+
// Filter filters the input field with the replacement value.
95+
func (f *HashFilter) Filter(in zapcore.Field) zapcore.Field {
96+
in.String = hash(in.String)
97+
return in
98+
}
99+
68100
// ReplaceFilter is a Caddy log field filter that
69101
// replaces the field with the indicated string.
70102
type ReplaceFilter struct {
@@ -195,23 +227,27 @@ func (m IPMaskFilter) Filter(in zapcore.Field) zapcore.Field {
195227
type filterAction string
196228

197229
const (
198-
// Replace value(s) of query parameter(s).
230+
// Replace value(s).
199231
replaceAction filterAction = "replace"
200-
// Delete query parameter(s).
232+
233+
// Hash value(s).
234+
hashAction filterAction = "hash"
235+
236+
// Delete.
201237
deleteAction filterAction = "delete"
202238
)
203239

204240
func (a filterAction) IsValid() error {
205241
switch a {
206-
case replaceAction, deleteAction:
242+
case replaceAction, deleteAction, hashAction:
207243
return nil
208244
}
209245

210246
return errors.New("invalid action type")
211247
}
212248

213249
type queryFilterAction struct {
214-
// `replace` to replace the value(s) associated with the parameter(s) or `delete` to remove them entirely.
250+
// `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.
215251
Type filterAction `json:"type"`
216252

217253
// The name of the query parameter.
@@ -224,9 +260,9 @@ type queryFilterAction struct {
224260
// QueryFilter is a Caddy log field filter that filters
225261
// query parameters from a URL.
226262
//
227-
// This filter updates the logged URL string to remove or replace query
228-
// parameters containing sensitive data. For instance, it can be used
229-
// to redact any kind of secrets which were passed as query parameters,
263+
// This filter updates the logged URL string to remove, replace or hash
264+
// query parameters containing sensitive data. For instance, it can be
265+
// used to redact any kind of secrets which were passed as query parameters,
230266
// such as OAuth access tokens, session IDs, magic link tokens, etc.
231267
type QueryFilter struct {
232268
// 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 {
271307
}
272308
qfa.Value = d.Val()
273309

310+
case "hash":
311+
if !d.NextArg() {
312+
return d.ArgErr()
313+
}
314+
315+
qfa.Type = hashAction
316+
qfa.Parameter = d.Val()
317+
274318
case "delete":
275319
if !d.NextArg() {
276320
return d.ArgErr()
@@ -304,6 +348,11 @@ func (m QueryFilter) Filter(in zapcore.Field) zapcore.Field {
304348
q[a.Parameter][i] = a.Value
305349
}
306350

351+
case hashAction:
352+
for i := range q[a.Parameter] {
353+
q[a.Parameter][i] = hash(a.Value)
354+
}
355+
307356
case deleteAction:
308357
q.Del(a.Parameter)
309358
}
@@ -316,7 +365,7 @@ func (m QueryFilter) Filter(in zapcore.Field) zapcore.Field {
316365
}
317366

318367
type cookieFilterAction struct {
319-
// `replace` to replace the value of the cookie or `delete` to remove it entirely.
368+
// `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.
320369
Type filterAction `json:"type"`
321370

322371
// The name of the cookie.
@@ -330,7 +379,7 @@ type cookieFilterAction struct {
330379
// cookies.
331380
//
332381
// This filter updates the logged HTTP header string
333-
// to remove or replace cookies containing sensitive data. For instance,
382+
// to remove, replace or hash cookies containing sensitive data. For instance,
334383
// it can be used to redact any kind of secrets, such as session IDs.
335384
//
336385
// 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 {
378427
}
379428
cfa.Value = d.Val()
380429

430+
case "hash":
431+
if !d.NextArg() {
432+
return d.ArgErr()
433+
}
434+
435+
cfa.Type = hashAction
436+
cfa.Name = d.Val()
437+
381438
case "delete":
382439
if !d.NextArg() {
383440
return d.ArgErr()
@@ -415,6 +472,11 @@ OUTER:
415472
transformedRequest.AddCookie(c)
416473
continue OUTER
417474

475+
case hashAction:
476+
c.Value = hash(c.Value)
477+
transformedRequest.AddCookie(c)
478+
continue OUTER
479+
418480
case deleteAction:
419481
continue OUTER
420482
}

modules/logging/filters_test.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@ func TestQueryFilter(t *testing.T) {
1313
{replaceAction, "notexist", "REDACTED"},
1414
{deleteAction, "bar", ""},
1515
{deleteAction, "notexist", ""},
16+
{hashAction, "hash", ""},
1617
}}
1718

1819
if f.Validate() != nil {
1920
t.Fatalf("the filter must be valid")
2021
}
2122

22-
out := f.Filter(zapcore.Field{String: "/path?foo=a&foo=b&bar=c&bar=d&baz=e"})
23-
if out.String != "/path?baz=e&foo=REDACTED&foo=REDACTED" {
23+
out := f.Filter(zapcore.Field{String: "/path?foo=a&foo=b&bar=c&bar=d&baz=e&hash=hashed"})
24+
if out.String != "/path?baz=e&foo=REDACTED&foo=REDACTED&hash=e3b0c442" {
2425
t.Fatalf("query parameters have not been filtered: %s", out.String)
2526
}
2627
}
@@ -45,10 +46,11 @@ func TestCookieFilter(t *testing.T) {
4546
f := CookieFilter{[]cookieFilterAction{
4647
{replaceAction, "foo", "REDACTED"},
4748
{deleteAction, "bar", ""},
49+
{hashAction, "hash", ""},
4850
}}
4951

50-
out := f.Filter(zapcore.Field{String: "foo=a; foo=b; bar=c; bar=d; baz=e"})
51-
if out.String != "foo=REDACTED; foo=REDACTED; baz=e" {
52+
out := f.Filter(zapcore.Field{String: "foo=a; foo=b; bar=c; bar=d; baz=e; hash=hashed"})
53+
if out.String != "foo=REDACTED; foo=REDACTED; baz=e; hash=1a06df82" {
5254
t.Fatalf("cookies have not been filtered: %s", out.String)
5355
}
5456
}
@@ -78,3 +80,12 @@ func TestRegexpFilter(t *testing.T) {
7880
t.Fatalf("field has not been filtered: %s", out.String)
7981
}
8082
}
83+
84+
func TestHashFilter(t *testing.T) {
85+
f := HashFilter{}
86+
87+
out := f.Filter(zapcore.Field{String: "foo"})
88+
if out.String != "2c26b46b" {
89+
t.Fatalf("field has not been filtered: %s", out.String)
90+
}
91+
}

0 commit comments

Comments
 (0)