Compare commits
	
		
			3 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 05002d7ee4 | ||
|   | b4360d4a20 | ||
|   | 3b85b3bbad | 
| @@ -1 +1 @@ | |||||||
| __version__ = "20.12.5" | __version__ = "20.12.7" | ||||||
|   | |||||||
							
								
								
									
										13
									
								
								sanic/app.py
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								sanic/app.py
									
									
									
									
									
								
							| @@ -2,6 +2,7 @@ import logging | |||||||
| import logging.config | import logging.config | ||||||
| import os | import os | ||||||
| import re | import re | ||||||
|  | import sys | ||||||
|  |  | ||||||
| from asyncio import CancelledError, Protocol, ensure_future, get_event_loop | from asyncio import CancelledError, Protocol, ensure_future, get_event_loop | ||||||
| from collections import defaultdict, deque | from collections import defaultdict, deque | ||||||
| @@ -65,6 +66,18 @@ class Sanic: | |||||||
|         if configure_logging: |         if configure_logging: | ||||||
|             logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS) |             logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS) | ||||||
|  |  | ||||||
|  |         if sys.version_info >= (3, 10): | ||||||
|  |             error_logger.error( | ||||||
|  |                 "Unsupported version of Python has been detected.\n\nPython " | ||||||
|  |                 f"version {sys.version} is not supported by this version of " | ||||||
|  |                 "Sanic. There is a security advisory that has been issued for " | ||||||
|  |                 "Sanic v20.12 while running Python 3.10+. You should either " | ||||||
|  |                 "use a supported version of Python (v3.6 - v3.9) or upgrade " | ||||||
|  |                 "Sanic to v21+.\n\nPlease see https://github.com/sanic-org/" | ||||||
|  |                 "sanic/security/advisories/GHSA-7p79-6x2v-5h88 for " | ||||||
|  |                 "more information.\n" | ||||||
|  |             ) | ||||||
|  |  | ||||||
|         self.name = name |         self.name = name | ||||||
|         self.asgi = False |         self.asgi = False | ||||||
|         self.router = router or Router(self) |         self.router = router or Router(self) | ||||||
|   | |||||||
| @@ -169,7 +169,11 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|         self.request_class = self.app.request_class or Request |         self.request_class = self.app.request_class or Request | ||||||
|         self.is_request_stream = self.app.is_request_stream |         self.is_request_stream = self.app.is_request_stream | ||||||
|         self._is_stream_handler = False |         self._is_stream_handler = False | ||||||
|         self._not_paused = asyncio.Event(loop=deprecated_loop) |         self._not_paused = ( | ||||||
|  |             asyncio.Event() | ||||||
|  |             if sys.version_info >= (3, 10) | ||||||
|  |             else asyncio.Event(loop=deprecated_loop) | ||||||
|  |         ) | ||||||
|         self._total_request_size = 0 |         self._total_request_size = 0 | ||||||
|         self._request_timeout_handler = None |         self._request_timeout_handler = None | ||||||
|         self._response_timeout_handler = None |         self._response_timeout_handler = None | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| from functools import partial, wraps | from functools import partial, wraps | ||||||
| from mimetypes import guess_type | from mimetypes import guess_type | ||||||
| from os import path | from os import path, sep | ||||||
| from re import sub | from pathlib import Path | ||||||
| from time import gmtime, strftime | from time import gmtime, strftime | ||||||
| from urllib.parse import unquote | from urllib.parse import unquote | ||||||
|  |  | ||||||
| @@ -26,28 +26,41 @@ async def _static_request_handler( | |||||||
|     content_type=None, |     content_type=None, | ||||||
|     file_uri=None, |     file_uri=None, | ||||||
| ): | ): | ||||||
|     # Using this to determine if the URL is trying to break out of the path |  | ||||||
|     # served.  os.path.realpath seems to be very slow |  | ||||||
|     if file_uri and "../" in file_uri: |  | ||||||
|         raise InvalidUsage("Invalid URL") |  | ||||||
|     # Merge served directory and requested file if provided |     # Merge served directory and requested file if provided | ||||||
|     # Strip all / that in the beginning of the URL to help prevent python |     file_path_raw = Path(unquote(file_or_directory)) | ||||||
|     # from herping a derp and treating the uri as an absolute path |     root_path = file_path = file_path_raw.resolve() | ||||||
|     root_path = file_path = file_or_directory |     not_found = FileNotFound( | ||||||
|     if file_uri: |         "File not found", | ||||||
|         file_path = path.join(file_or_directory, sub("^[/]*", "", file_uri)) |         path=file_or_directory, | ||||||
|  |         relative_url=file_uri, | ||||||
|  |     ) | ||||||
|  |  | ||||||
|     # URL decode the path sent by the browser otherwise we won't be able to |     if file_uri: | ||||||
|     # match filenames which got encoded (filenames with spaces etc) |         # Strip all / that in the beginning of the URL to help prevent | ||||||
|     file_path = path.abspath(unquote(file_path)) |         # python from herping a derp and treating the uri as an | ||||||
|     if not file_path.startswith(path.abspath(unquote(root_path))): |         # absolute path | ||||||
|  |         unquoted_file_uri = unquote(file_uri).lstrip("/") | ||||||
|  |         file_path_raw = Path(file_or_directory, unquoted_file_uri) | ||||||
|  |         file_path = file_path_raw.resolve() | ||||||
|  |         if ( | ||||||
|  |             file_path < root_path and not file_path_raw.is_symlink() | ||||||
|  |         ) or ".." in file_path_raw.parts: | ||||||
|             error_logger.exception( |             error_logger.exception( | ||||||
|                 f"File not found: path={file_or_directory}, " |                 f"File not found: path={file_or_directory}, " | ||||||
|                 f"relative_url={file_uri}" |                 f"relative_url={file_uri}" | ||||||
|             ) |             ) | ||||||
|         raise FileNotFound( |             raise not_found | ||||||
|             "File not found", path=file_or_directory, relative_url=file_uri |  | ||||||
|  |     try: | ||||||
|  |         file_path.relative_to(root_path) | ||||||
|  |     except ValueError: | ||||||
|  |         if not file_path_raw.is_symlink(): | ||||||
|  |             error_logger.exception( | ||||||
|  |                 f"File not found: path={file_or_directory}, " | ||||||
|  |                 f"relative_url={file_uri}" | ||||||
|             ) |             ) | ||||||
|  |             raise not_found | ||||||
|  |  | ||||||
|     try: |     try: | ||||||
|         headers = {} |         headers = {} | ||||||
|         # Check if the client has been sent this file before |         # Check if the client has been sent this file before | ||||||
|   | |||||||
| @@ -1,10 +1,14 @@ | |||||||
| import inspect | import inspect | ||||||
| import os | import os | ||||||
|  | import sys | ||||||
|  |  | ||||||
|  | from pathlib import Path | ||||||
| from time import gmtime, strftime | from time import gmtime, strftime | ||||||
|  |  | ||||||
| import pytest | import pytest | ||||||
|  |  | ||||||
|  | from sanic.app import Sanic | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.fixture(scope="module") | @pytest.fixture(scope="module") | ||||||
| def static_file_directory(): | def static_file_directory(): | ||||||
| @@ -15,6 +19,22 @@ def static_file_directory(): | |||||||
|     return static_directory |     return static_directory | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.fixture(scope="module") | ||||||
|  | def double_dotted_directory_file(static_file_directory: str): | ||||||
|  |     """Generate double dotted directory and its files""" | ||||||
|  |     if sys.platform == "win32": | ||||||
|  |         raise Exception("Windows doesn't support double dotted directories") | ||||||
|  |  | ||||||
|  |     file_path = Path(static_file_directory) / "dotted.." / "dot.txt" | ||||||
|  |     double_dotted_dir = file_path.parent | ||||||
|  |     Path.mkdir(double_dotted_dir, exist_ok=True) | ||||||
|  |     with open(file_path, "w") as f: | ||||||
|  |         f.write("DOT\n") | ||||||
|  |     yield file_path | ||||||
|  |     Path.unlink(file_path) | ||||||
|  |     Path.rmdir(double_dotted_dir) | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_file_path(static_file_directory, file_name): | def get_file_path(static_file_directory, file_name): | ||||||
|     return os.path.join(static_file_directory, file_name) |     return os.path.join(static_file_directory, file_name) | ||||||
|  |  | ||||||
| @@ -374,3 +394,43 @@ def test_static_name(app, static_file_directory, static_name, file_name): | |||||||
|     request, response = app.test_client.get(f"/static/{file_name}") |     request, response = app.test_client.get(f"/static/{file_name}") | ||||||
|  |  | ||||||
|     assert response.status == 200 |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.skipif( | ||||||
|  |     sys.platform == "win32", | ||||||
|  |     reason="Windows does not support double dotted directories", | ||||||
|  | ) | ||||||
|  | def test_dotted_dir_ok( | ||||||
|  |     app: Sanic, static_file_directory: str, double_dotted_directory_file: Path | ||||||
|  | ): | ||||||
|  |     app.static("/foo", static_file_directory) | ||||||
|  |     dot_relative_path = str( | ||||||
|  |         double_dotted_directory_file.relative_to(static_file_directory) | ||||||
|  |     ) | ||||||
|  |     _, response = app.test_client.get("/foo/" + dot_relative_path) | ||||||
|  |     assert response.status == 200 | ||||||
|  |     assert response.body == b"DOT\n" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_breakout(app: Sanic, static_file_directory: str): | ||||||
|  |     app.static("/foo", static_file_directory) | ||||||
|  |  | ||||||
|  |     _, response = app.test_client.get("/foo/..%2Ffake/server.py") | ||||||
|  |     assert response.status == 404 | ||||||
|  |  | ||||||
|  |     _, response = app.test_client.get("/foo/..%2Fstatic/test.file") | ||||||
|  |     assert response.status == 404 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.skipif( | ||||||
|  |     sys.platform != "win32", reason="Block backslash on Windows only" | ||||||
|  | ) | ||||||
|  | def test_double_backslash_prohibited_on_win32( | ||||||
|  |     app: Sanic, static_file_directory: str | ||||||
|  | ): | ||||||
|  |     app.static("/foo", static_file_directory) | ||||||
|  |  | ||||||
|  |     _, response = app.test_client.get("/foo/static/..\\static/test.file") | ||||||
|  |     assert response.status == 404 | ||||||
|  |     _, response = app.test_client.get("/foo/static\\../static/test.file") | ||||||
|  |     assert response.status == 404 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user