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 a filter for query parameters (#4424)
Co-authored-by: Matt Holt <mholt@users.noreply.github.com> Co-authored-by: Francis Lavoie <lavofr@gmail.com>
Kévin Dunglas committed
Nov 23, 2021 at 10:01 UTC
bcac2beee7e419f8cdab2ed16f388d1af282a46b
3 files changed
+189
caddytest/integration/caddyfile_adapt/log_filters.txt
+18
@@ -5,6 +5,10 @@ log {
5
format filter {
6
wrap console
7
fields {
8
+ uri query {
9
+ replace foo REDACTED
10
+ delete bar
11
+ }
12
request>headers>Authorization replace REDACTED
13
request>headers>Server delete
14
request>remote_addr ip_mask {
@@ -40,6 +44,20 @@ log {
44
"filter": "ip_mask",
45
"ipv4_cidr": 24,
46
"ipv6_cidr": 32
47
+ },
48
+ "uri": {
49
+ "actions": [
50
+ {
51
+ "parameter": "foo",
52
+ "type": "replace",
53
+ "value": "REDACTED"
54
+ },
55
+ {
56
+ "parameter": "bar",
57
+ "type": "delete"
58
+ }
59
+ ],
60
+ "filter": "query"
61
}
62
},
63
"format": "filter",
modules/logging/filters.go
+130
@@ -15,7 +15,9 @@
15
package logging
16
17
import (
18
+ "errors"
19
"net"
20
+ "net/url"
21
"strconv"
22
23
"github.com/caddyserver/caddy/v2"
@@ -27,6 +29,7 @@ func init() {
29
caddy.RegisterModule(DeleteFilter{})
30
caddy.RegisterModule(ReplaceFilter{})
31
caddy.RegisterModule(IPMaskFilter{})
32
+ caddy.RegisterModule(QueryFilter{})
33
}
34
35
// LogFieldFilter can filter (or manipulate)
@@ -185,15 +188,142 @@ func (m IPMaskFilter) Filter(in zapcore.Field) zapcore.Field {
188
return in
189
}
190
191
+type filterAction string
192
+
193
+const (
194
+ // Replace value(s) of query parameter(s).
195
+ replaceAction filterAction = "replace"
196
+ // Delete query parameter(s).
197
+ deleteAction filterAction = "delete"
198
+)
199
+
200
+func (a filterAction) IsValid() error {
201
+ switch a {
202
+ case replaceAction, deleteAction:
203
+ return nil
204
+ }
205
+
206
+ return errors.New("invalid action type")
207
+}
208
+
209
+type queryFilterAction struct {
210
+ // `replace` to replace the value(s) associated with the parameter(s) or `delete` to remove them entirely.
211
+ Type filterAction `json:"type"`
212
+
213
+ // The name of the query parameter.
214
+ Parameter string `json:"parameter"`
215
+
216
+ // The value to use as replacement if the action is `replace`.
217
+ Value string `json:"value,omitempty"`
218
+}
219
+
220
+// QueryFilter is a Caddy log field filter that filters
221
+// query parameters from a URL.
222
+//
223
+// This filter updates the logged URL string to remove or replace query
224
+// parameters containing sensitive data. For instance, it can be used
225
+// to redact any kind of secrets which were passed as query parameters,
226
+// such as OAuth access tokens, session IDs, magic link tokens, etc.
227
+type QueryFilter struct {
228
+ // A list of actions to apply to the query parameters of the URL.
229
+ Actions []queryFilterAction `json:"actions"`
230
+}
231
+
232
+// Validate checks that action types are correct.
233
+func (f *QueryFilter) Validate() error {
234
+ for _, a := range f.Actions {
235
+ if err := a.Type.IsValid(); err != nil {
236
+ return err
237
+ }
238
+ }
239
+
240
+ return nil
241
+}
242
+
243
+// CaddyModule returns the Caddy module information.
244
+func (QueryFilter) CaddyModule() caddy.ModuleInfo {
245
+ return caddy.ModuleInfo{
246
+ ID: "caddy.logging.encoders.filter.query",
247
+ New: func() caddy.Module { return new(QueryFilter) },
248
+ }
249
+}
250
+
251
+// UnmarshalCaddyfile sets up the module from Caddyfile tokens.
252
+func (m *QueryFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
253
+ for d.Next() {
254
+ for d.NextBlock(0) {
255
+ qfa := queryFilterAction{}
256
+ switch d.Val() {
257
+ case "replace":
258
+ if !d.NextArg() {
259
+ return d.ArgErr()
260
+ }
261
+
262
+ qfa.Type = replaceAction
263
+ qfa.Parameter = d.Val()
264
+
265
+ if !d.NextArg() {
266
+ return d.ArgErr()
267
+ }
268
+ qfa.Value = d.Val()
269
+
270
+ case "delete":
271
+ if !d.NextArg() {
272
+ return d.ArgErr()
273
+ }
274
+
275
+ qfa.Type = deleteAction
276
+ qfa.Parameter = d.Val()
277
+
278
+ default:
279
+ return d.Errf("unrecognized subdirective %s", d.Val())
280
+ }
281
+
282
+ m.Actions = append(m.Actions, qfa)
283
+ }
284
+ }
285
+ return nil
286
+}
287
+
288
+// Filter filters the input field.
289
+func (m QueryFilter) Filter(in zapcore.Field) zapcore.Field {
290
+ u, err := url.Parse(in.String)
291
+ if err != nil {
292
+ return in
293
+ }
294
+
295
+ q := u.Query()
296
+ for _, a := range m.Actions {
297
+ switch a.Type {
298
+ case replaceAction:
299
+ for i := range q[a.Parameter] {
300
+ q[a.Parameter][i] = a.Value
301
+ }
302
+
303
+ case deleteAction:
304
+ q.Del(a.Parameter)
305
+ }
306
+ }
307
+
308
+ u.RawQuery = q.Encode()
309
+ in.String = u.String()
310
+
311
+ return in
312
+}
313
+
314
// Interface guards
315
var (
316
_ LogFieldFilter = (*DeleteFilter)(nil)
317
_ LogFieldFilter = (*ReplaceFilter)(nil)
318
_ LogFieldFilter = (*IPMaskFilter)(nil)
319
+ _ LogFieldFilter = (*QueryFilter)(nil)
320
321
_ caddyfile.Unmarshaler = (*DeleteFilter)(nil)
322
_ caddyfile.Unmarshaler = (*ReplaceFilter)(nil)
323
_ caddyfile.Unmarshaler = (*IPMaskFilter)(nil)
324
+ _ caddyfile.Unmarshaler = (*QueryFilter)(nil)
325
326
_ caddy.Provisioner = (*IPMaskFilter)(nil)
327
+
328
+ _ caddy.Validator = (*QueryFilter)(nil)
329
)
modules/logging/filters_test.go
new
+41
@@ -0,0 +1,41 @@
1
+package logging
2
+
3
+import (
4
+ "testing"
5
+
6
+ "go.uber.org/zap/zapcore"
7
+)
8
+
9
+func TestQueryFilter(t *testing.T) {
10
+ f := QueryFilter{[]queryFilterAction{
11
+ {replaceAction, "foo", "REDACTED"},
12
+ {replaceAction, "notexist", "REDACTED"},
13
+ {deleteAction, "bar", ""},
14
+ {deleteAction, "notexist", ""},
15
+ }}
16
+
17
+ if f.Validate() != nil {
18
+ t.Fatalf("the filter must be valid")
19
+ }
20
+
21
+ out := f.Filter(zapcore.Field{String: "/path?foo=a&foo=b&bar=c&bar=d&baz=e"})
22
+ if out.String != "/path?baz=e&foo=REDACTED&foo=REDACTED" {
23
+ t.Fatalf("query parameters have not been filtered: %s", out.String)
24
+ }
25
+}
26
+
27
+func TestValidateQueryFilter(t *testing.T) {
28
+ f := QueryFilter{[]queryFilterAction{
29
+ {},
30
+ }}
31
+ if f.Validate() == nil {
32
+ t.Fatalf("empty action type must be invalid")
33
+ }
34
+
35
+ f = QueryFilter{[]queryFilterAction{
36
+ {Type: "foo"},
37
+ }}
38
+ if f.Validate() == nil {
39
+ t.Fatalf("unknown action type must be invalid")
40
+ }
41
+}