MatchWithError returns true if r matches m.
(r *http.Request)
| 427 | |
| 428 | // MatchWithError returns true if r matches m. |
| 429 | func (m MatchPath) MatchWithError(r *http.Request) (bool, error) { |
| 430 | // Even though RFC 9110 says that path matching is case-sensitive |
| 431 | // (https://www.rfc-editor.org/rfc/rfc9110.html#section-4.2.3), |
| 432 | // we do case-insensitive matching to mitigate security issues |
| 433 | // related to differences between operating systems, applications, |
| 434 | // etc; if case-sensitive matching is needed, the regex matcher |
| 435 | // can be used instead. |
| 436 | reqPath := strings.ToLower(r.URL.Path) |
| 437 | |
| 438 | if runtime.GOOS == "windows" { // issue #5613 |
| 439 | // Windows treats backslashes as path separators and |
| 440 | // ignores trailing dots and spaces when accessing files |
| 441 | // (sigh), potentially causing a security risk (cry) if |
| 442 | // protected files are not matched as intended. |
| 443 | reqPath = strings.ReplaceAll(reqPath, `\`, "/") |
| 444 | reqPath = strings.TrimRight(reqPath, ". ") |
| 445 | } |
| 446 | |
| 447 | repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) |
| 448 | |
| 449 | for _, matchPattern := range m { |
| 450 | matchPattern = repl.ReplaceAll(matchPattern, "") |
| 451 | |
| 452 | // special case: whole path is wildcard; this is unnecessary |
| 453 | // as it matches all requests, which is the same as no matcher |
| 454 | if matchPattern == "*" { |
| 455 | return true, nil |
| 456 | } |
| 457 | |
| 458 | // Clean the path, merge doubled slashes, etc. |
| 459 | // This ensures maliciously crafted requests can't bypass |
| 460 | // the path matcher. See #4407. Good security posture |
| 461 | // requires that we should do all we can to reduce any |
| 462 | // funny-looking paths into "normalized" forms such that |
| 463 | // weird variants can't sneak by. |
| 464 | // |
| 465 | // How we clean the path depends on the kind of pattern: |
| 466 | // we either merge slashes or we don't. If the pattern |
| 467 | // has double slashes, we preserve them in the path. |
| 468 | // |
| 469 | // TODO: Despite the fact that the *vast* majority of path |
| 470 | // matchers have only 1 pattern, a possible optimization is |
| 471 | // to remember the cleaned form of the path for future |
| 472 | // iterations; it's just that the way we clean depends on |
| 473 | // the kind of pattern. |
| 474 | |
| 475 | mergeSlashes := !strings.Contains(matchPattern, "//") |
| 476 | |
| 477 | // if '%' appears in the match pattern, we interpret that to mean |
| 478 | // the intent is to compare that part of the path in raw/escaped |
| 479 | // space; i.e. "%40"=="%40", not "@", and "%2F"=="%2F", not "/" |
| 480 | if strings.Contains(matchPattern, "%") { |
| 481 | escapedPath := r.URL.EscapedPath() |
| 482 | if runtime.GOOS == "windows" { |
| 483 | escapedPath = windowsEscapedPathSeparatorRepl.Replace(escapedPath) |
| 484 | matchPattern = windowsEscapedPathSeparatorRepl.Replace(matchPattern) |
| 485 | } |
| 486 | reqPathForPattern := CleanPath(escapedPath, mergeSlashes) |
no test coverage detected