@marcel91200 / caddy Mirror · read-only / commits / a1b417c8
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 14, 01:30 UTC

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>

Kévin Dunglas committed Dec 2, 2021 at 21:51 UTC a1b417c832b4ab3dab9eaa9690e1d07672a949b8
3 files changed +100 -13
caddytest/integration/caddyfile_adapt/log_filters.txt
+14
@@ -8,18 +8,21 @@ log {
8 uri query {
9 replace foo REDACTED
10 delete bar
11 + hash baz
12 }
13 request>headers>Authorization replace REDACTED
14 request>headers>Server delete
15 request>headers>Cookie cookie {
16 replace foo REDACTED
17 delete bar
18 + hash baz
19 }
20 request>remote_ip ip_mask {
21 ipv4 24
22 ipv6 32
23 }
24 request>headers>Regexp regexp secret REDACTED
25 + request>headers>Hash hash
26 }
27 }
28 }
@@ -52,10 +55,17 @@ log {
55 {
56 "name": "bar",
57 "type": "delete"
58 + },
59 + {
60 + "name": "baz",
61 + "type": "hash"
62 }
63 ],
64 "filter": "cookie"
65 },
66 + "request\u003eheaders\u003eHash": {
67 + "filter": "hash"
68 + },
69 "request\u003eheaders\u003eRegexp": {
70 "filter": "regexp",
71 "regexp": "secret",
@@ -79,6 +89,10 @@ log {
89 {
90 "parameter": "bar",
91 "type": "delete"
92 + },
93 + {
94 + "parameter": "baz",
95 + "type": "hash"
96 }
97 ],
98 "filter": "query"
modules/logging/filters.go
+71 -9
@@ -15,7 +15,9 @@
15 package logging
16
17 import (
18 + "crypto/sha256"
19 "errors"
20 + "fmt"
21 "net"
22 "net/http"
23 "net/url"
@@ -34,6 +36,7 @@ func init() {
36 caddy.RegisterModule(QueryFilter{})
37 caddy.RegisterModule(CookieFilter{})
38 caddy.RegisterModule(RegexpFilter{})
39 + caddy.RegisterModule(HashFilter{})
40 }
41
42 // LogFieldFilter can filter (or manipulate)
@@ -65,6 +68,35 @@ func (DeleteFilter) Filter(in zapcore.Field) zapcore.Field {
68 return in
69 }
70
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 +
100 // ReplaceFilter is a Caddy log field filter that
101 // replaces the field with the indicated string.
102 type ReplaceFilter struct {
@@ -195,15 +227,19 @@ func (m IPMaskFilter) Filter(in zapcore.Field) zapcore.Field {
227 type filterAction string
228
229 const (
198 - // Replace value(s) of query parameter(s).
230 + // Replace value(s).
231 replaceAction filterAction = "replace"
200 - // Delete query parameter(s).
232 +
233 + // Hash value(s).
234 + hashAction filterAction = "hash"
235 +
236 + // Delete.
237 deleteAction filterAction = "delete"
238 )
239
240 func (a filterAction) IsValid() error {
241 switch a {
206 - case replaceAction, deleteAction:
242 + case replaceAction, deleteAction, hashAction:
243 return nil
244 }
245
@@ -211,7 +247,7 @@ func (a filterAction) IsValid() error {
247 }
248
249 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.
251 Type filterAction `json:"type"`
252
253 // The name of the query parameter.
@@ -224,9 +260,9 @@ type queryFilterAction struct {
260 // QueryFilter is a Caddy log field filter that filters
261 // query parameters from a URL.
262 //
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,
266 // such as OAuth access tokens, session IDs, magic link tokens, etc.
267 type QueryFilter struct {
268 // 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 {
307 }
308 qfa.Value = d.Val()
309
310 + case "hash":
311 + if !d.NextArg() {
312 + return d.ArgErr()
313 + }
314 +
315 + qfa.Type = hashAction
316 + qfa.Parameter = d.Val()
317 +
318 case "delete":
319 if !d.NextArg() {
320 return d.ArgErr()
@@ -304,6 +348,11 @@ func (m QueryFilter) Filter(in zapcore.Field) zapcore.Field {
348 q[a.Parameter][i] = a.Value
349 }
350
351 + case hashAction:
352 + for i := range q[a.Parameter] {
353 + q[a.Parameter][i] = hash(a.Value)
354 + }
355 +
356 case deleteAction:
357 q.Del(a.Parameter)
358 }
@@ -316,7 +365,7 @@ func (m QueryFilter) Filter(in zapcore.Field) zapcore.Field {
365 }
366
367 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.
369 Type filterAction `json:"type"`
370
371 // The name of the cookie.
@@ -330,7 +379,7 @@ type cookieFilterAction struct {
379 // cookies.
380 //
381 // 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,
383 // it can be used to redact any kind of secrets, such as session IDs.
384 //
385 // 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 {
427 }
428 cfa.Value = d.Val()
429
430 + case "hash":
431 + if !d.NextArg() {
432 + return d.ArgErr()
433 + }
434 +
435 + cfa.Type = hashAction
436 + cfa.Name = d.Val()
437 +
438 case "delete":
439 if !d.NextArg() {
440 return d.ArgErr()
@@ -415,6 +472,11 @@ OUTER:
472 transformedRequest.AddCookie(c)
473 continue OUTER
474
475 + case hashAction:
476 + c.Value = hash(c.Value)
477 + transformedRequest.AddCookie(c)
478 + continue OUTER
479 +
480 case deleteAction:
481 continue OUTER
482 }
modules/logging/filters_test.go
+15 -4
@@ -13,14 +13,15 @@ func TestQueryFilter(t *testing.T) {
13 {replaceAction, "notexist", "REDACTED"},
14 {deleteAction, "bar", ""},
15 {deleteAction, "notexist", ""},
16 + {hashAction, "hash", ""},
17 }}
18
19 if f.Validate() != nil {
20 t.Fatalf("the filter must be valid")
21 }
22
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" {
25 t.Fatalf("query parameters have not been filtered: %s", out.String)
26 }
27 }
@@ -45,10 +46,11 @@ func TestCookieFilter(t *testing.T) {
46 f := CookieFilter{[]cookieFilterAction{
47 {replaceAction, "foo", "REDACTED"},
48 {deleteAction, "bar", ""},
49 + {hashAction, "hash", ""},
50 }}
51
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" {
54 t.Fatalf("cookies have not been filtered: %s", out.String)
55 }
56 }
@@ -78,3 +80,12 @@ func TestRegexpFilter(t *testing.T) {
80 t.Fatalf("field has not been filtered: %s", out.String)
81 }
82 }
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 +}