summaryrefslogtreecommitdiff
path: root/modules/logging
diff options
context:
space:
mode:
authorKévin Dunglas <dunglas@gmail.com>2021-12-02 21:51:37 +0100
committerGitHub <noreply@github.com>2021-12-02 13:51:37 -0700
commita1b417c832b4ab3dab9eaa9690e1d07672a949b8 (patch)
tree24323a894ac9b8b59c9701887e324fa81d5c2caa /modules/logging
parent5bf0adad8748e96e10529d5fc5777afc9236a7b5 (diff)
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>
Diffstat (limited to 'modules/logging')
-rw-r--r--modules/logging/filters.go80
-rw-r--r--modules/logging/filters_test.go19
2 files changed, 86 insertions, 13 deletions
diff --git a/modules/logging/filters.go b/modules/logging/filters.go
index ded08ac..af64cc4 100644
--- a/modules/logging/filters.go
+++ b/modules/logging/filters.go
@@ -15,7 +15,9 @@
package logging
import (
+ "crypto/sha256"
"errors"
+ "fmt"
"net"
"net/http"
"net/url"
@@ -34,6 +36,7 @@ func init() {
caddy.RegisterModule(QueryFilter{})
caddy.RegisterModule(CookieFilter{})
caddy.RegisterModule(RegexpFilter{})
+ caddy.RegisterModule(HashFilter{})
}
// LogFieldFilter can filter (or manipulate)
@@ -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)))
+}
+
+// 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 {
@@ -195,15 +227,19 @@ 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
}
@@ -211,7 +247,7 @@ func (a filterAction) IsValid() error {
}
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.
@@ -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.
@@ -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()
@@ -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)
}
@@ -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.
@@ -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
@@ -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()
@@ -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
}
diff --git a/modules/logging/filters_test.go b/modules/logging/filters_test.go
index 99289c3..ecf1d87 100644
--- a/modules/logging/filters_test.go
+++ b/modules/logging/filters_test.go
@@ -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)
}
}
@@ -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)
}
}
@@ -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)
+ }
+}