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

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/",