Path protection with pathlib

This commit is contained in:
Adam Hopkins 2022-07-31 14:09:01 +03:00
parent b4360d4a20
commit 05002d7ee4
No known key found for this signature in database
GPG Key ID: 9F85EE6C807303FB
2 changed files with 36 additions and 25 deletions

View File

@ -1,6 +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, sep from os import path, sep
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,31 +27,39 @@ async def _static_request_handler(
file_uri=None, file_uri=None,
): ):
# Merge served directory and requested file if provided # Merge served directory and requested file if provided
root_path = file_path = path.abspath(unquote(file_or_directory)) file_path_raw = Path(unquote(file_or_directory))
root_path = file_path = file_path_raw.resolve()
not_found = FileNotFound(
"File not found",
path=file_or_directory,
relative_url=file_uri,
)
if file_uri: if file_uri:
# Strip all / that in the beginning of the URL to help prevent # Strip all / that in the beginning of the URL to help prevent
# python from herping a derp and treating the uri as an # python from herping a derp and treating the uri as an
# absolute path # absolute path
unquoted_file_uri = unquote(file_uri).lstrip("/") unquoted_file_uri = unquote(file_uri).lstrip("/")
file_path_raw = Path(file_or_directory, unquoted_file_uri)
segments = unquoted_file_uri.split("/") file_path = file_path_raw.resolve()
if ".." in segments or any(sep in segment for segment in segments): if (
raise InvalidUsage("Invalid URL") file_path < root_path and not file_path_raw.is_symlink()
) or ".." in file_path_raw.parts:
file_path = path.join(file_or_directory, unquoted_file_uri)
file_path = path.abspath(file_path)
if not file_path.startswith(root_path):
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, try:
relative_url=file_uri, 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 = {}

View File

@ -404,11 +404,10 @@ def test_dotted_dir_ok(
app: Sanic, static_file_directory: str, double_dotted_directory_file: Path app: Sanic, static_file_directory: str, double_dotted_directory_file: Path
): ):
app.static("/foo", static_file_directory) app.static("/foo", static_file_directory)
url = ( dot_relative_path = str(
"/foo" double_dotted_directory_file.relative_to(static_file_directory)
+ str(double_dotted_directory_file)[len(static_file_directory) :]
) )
_, response = app.test_client.get(url) _, response = app.test_client.get("/foo/" + dot_relative_path)
assert response.status == 200 assert response.status == 200
assert response.body == b"DOT\n" assert response.body == b"DOT\n"
@ -416,8 +415,11 @@ def test_dotted_dir_ok(
def test_breakout(app: Sanic, static_file_directory: str): def test_breakout(app: Sanic, static_file_directory: str):
app.static("/foo", static_file_directory) 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") _, response = app.test_client.get("/foo/..%2Fstatic/test.file")
assert response.status == 400 assert response.status == 404
@pytest.mark.skipif( @pytest.mark.skipif(
@ -429,6 +431,6 @@ def test_double_backslash_prohibited_on_win32(
app.static("/foo", static_file_directory) app.static("/foo", static_file_directory)
_, response = app.test_client.get("/foo/static/..\\static/test.file") _, response = app.test_client.get("/foo/static/..\\static/test.file")
assert response.status == 400 assert response.status == 404
_, response = app.test_client.get("/foo/static\\../static/test.file") _, response = app.test_client.get("/foo/static\\../static/test.file")
assert response.status == 400 assert response.status == 404