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
caddyhttp: Sanitize the path before evaluating path matchers (#4407)
Francis Lavoie committed
Nov 8, 2021 at 15:45 UTC
e7457b43e4703080ae8713ada798ce3e20b83690
2 files changed
+72
-4
modules/caddyhttp/matchers.go
+40
-2
@@ -21,6 +21,7 @@ import (
21
"net/http"
22
"net/textproto"
23
"net/url"
24
+ "path"
25
"path/filepath"
26
"regexp"
27
"sort"
@@ -314,7 +315,15 @@ func (m MatchPath) Provision(_ caddy.Context) error {
315
316
// Match returns true if r matches m.
317
func (m MatchPath) Match(r *http.Request) bool {
317
- lowerPath := strings.ToLower(r.URL.Path)
318
+ // PathUnescape returns an error if the escapes aren't
319
+ // well-formed, meaning the count % matches the RFC.
320
+ // Return early if the escape is improper.
321
+ unescapedPath, err := url.PathUnescape(r.URL.Path)
322
+ if err != nil {
323
+ return false
324
+ }
325
+
326
+ lowerPath := strings.ToLower(unescapedPath)
327
328
// see #2917; Windows ignores trailing dots and spaces
329
// when accessing files (sigh), potentially causing a
@@ -323,6 +332,16 @@ func (m MatchPath) Match(r *http.Request) bool {
332
// being matched by *.php to be treated as PHP scripts
333
lowerPath = strings.TrimRight(lowerPath, ". ")
334
335
+ // Clean the path, merges doubled slashes, etc.
336
+ // This ensures maliciously crafted requests can't bypass
337
+ // the path matcher. See #4407
338
+ lowerPath = path.Clean(lowerPath)
339
+
340
+ // Cleaning may remove the trailing slash, but we want to keep it
341
+ if lowerPath != "/" && strings.HasSuffix(r.URL.Path, "/") {
342
+ lowerPath = lowerPath + "/"
343
+ }
344
+
345
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
346
347
for _, matchPath := range m {
@@ -396,7 +415,26 @@ func (MatchPathRE) CaddyModule() caddy.ModuleInfo {
415
// Match returns true if r matches m.
416
func (m MatchPathRE) Match(r *http.Request) bool {
417
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
399
- return m.MatchRegexp.Match(r.URL.Path, repl)
418
+
419
+ // PathUnescape returns an error if the escapes aren't
420
+ // well-formed, meaning the count % matches the RFC.
421
+ // Return early if the escape is improper.
422
+ unescapedPath, err := url.PathUnescape(r.URL.Path)
423
+ if err != nil {
424
+ return false
425
+ }
426
+
427
+ // Clean the path, merges doubled slashes, etc.
428
+ // This ensures maliciously crafted requests can't bypass
429
+ // the path matcher. See #4407
430
+ cleanedPath := path.Clean(unescapedPath)
431
+
432
+ // Cleaning may remove the trailing slash, but we want to keep it
433
+ if cleanedPath != "/" && strings.HasSuffix(r.URL.Path, "/") {
434
+ cleanedPath = cleanedPath + "/"
435
+ }
436
+
437
+ return m.MatchRegexp.Match(cleanedPath, repl)
438
}
439
440
// CaddyModule returns the Caddy module information.
modules/caddyhttp/matchers_test.go
+32
-2
@@ -257,6 +257,21 @@ func TestPathMatcher(t *testing.T) {
257
input: "/foo/BAR.txt",
258
expect: true,
259
},
260
+ {
261
+ match: MatchPath{"/foo*"},
262
+ input: "//foo/bar",
263
+ expect: true,
264
+ },
265
+ {
266
+ match: MatchPath{"/foo*"},
267
+ input: "//foo",
268
+ expect: true,
269
+ },
270
+ {
271
+ match: MatchPath{"/foo*"},
272
+ input: "/%2F/foo",
273
+ expect: true,
274
+ },
275
{
276
match: MatchPath{"*"},
277
input: "/",
@@ -326,15 +341,30 @@ func TestPathREMatcher(t *testing.T) {
341
expect: true,
342
},
343
{
329
- match: MatchPathRE{MatchRegexp{Pattern: "/foo"}},
344
+ match: MatchPathRE{MatchRegexp{Pattern: "^/foo"}},
345
input: "/foo",
346
expect: true,
347
},
348
{
334
- match: MatchPathRE{MatchRegexp{Pattern: "/foo"}},
349
+ match: MatchPathRE{MatchRegexp{Pattern: "^/foo"}},
350
input: "/foo/",
351
expect: true,
352
},
353
+ {
354
+ match: MatchPathRE{MatchRegexp{Pattern: "^/foo"}},
355
+ input: "//foo",
356
+ expect: true,
357
+ },
358
+ {
359
+ match: MatchPathRE{MatchRegexp{Pattern: "^/foo"}},
360
+ input: "//foo/",
361
+ expect: true,
362
+ },
363
+ {
364
+ match: MatchPathRE{MatchRegexp{Pattern: "^/foo"}},
365
+ input: "/%2F/foo/",
366
+ expect: true,
367
+ },
368
{
369
match: MatchPathRE{MatchRegexp{Pattern: "/bar"}},
370
input: "/foo/",