|
1 | 1 | """Utilities for OAuth 2.0 Resource Indicators (RFC 8707) and PKCE (RFC 7636).""" |
2 | 2 |
|
| 3 | +import posixpath |
| 4 | +import re |
3 | 5 | import time |
4 | 6 | from urllib.parse import urlparse, urlsplit, urlunsplit |
5 | 7 |
|
6 | 8 | from pydantic import AnyUrl, HttpUrl |
7 | 9 |
|
8 | 10 |
|
| 11 | +def _normalize_resource_path(path: str) -> str: |
| 12 | + """Resolve dot-segments without decoding encoded path separators.""" |
| 13 | + has_trailing_slash = path.endswith("/") |
| 14 | + |
| 15 | + # RFC 3986 treats percent-encoded unreserved characters as equivalent. Decode |
| 16 | + # only encoded dots here: unquoting the whole path would turn %2F into a |
| 17 | + # separator and change the resource hierarchy being authorized. |
| 18 | + path = re.sub(r"%2e", ".", path, flags=re.IGNORECASE) |
| 19 | + path = posixpath.normpath(path) |
| 20 | + if path == ".": |
| 21 | + path = "" |
| 22 | + |
| 23 | + if has_trailing_slash and not path.endswith("/"): |
| 24 | + path += "/" |
| 25 | + return path |
| 26 | + |
| 27 | + |
9 | 28 | def resource_url_from_server_url(url: str | HttpUrl | AnyUrl) -> str: |
10 | 29 | """Convert server URL to canonical resource URL per RFC 8707. |
11 | 30 |
|
@@ -51,10 +70,14 @@ def check_resource_allowed(requested_resource: str, configured_resource: str) -> |
51 | 70 | if requested.scheme.lower() != configured.scheme.lower() or requested.netloc.lower() != configured.netloc.lower(): |
52 | 71 | return False |
53 | 72 |
|
| 73 | + # Resolve dot-segments before normalizing trailing slashes so that a |
| 74 | + # resource cannot escape its configured path through ../ or its encoded |
| 75 | + # equivalent. |
| 76 | + requested_path = _normalize_resource_path(requested.path) |
| 77 | + configured_path = _normalize_resource_path(configured.path) |
| 78 | + |
54 | 79 | # Normalize trailing slashes before comparison so that |
55 | 80 | # "/foo" and "/foo/" are treated as equivalent. |
56 | | - requested_path = requested.path |
57 | | - configured_path = configured.path |
58 | 81 | if not requested_path.endswith("/"): |
59 | 82 | requested_path += "/" |
60 | 83 | if not configured_path.endswith("/"): |
|
0 commit comments