@marcel91200 / caddy Mirror · read-only / commits / bcac2bee
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 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 +}