Path protection with pathlib
This commit is contained in:
parent
3b85b3bbad
commit
b4360d4a20
|
@ -1 +1 @@
|
||||||
__version__ = "20.12.6"
|
__version__ = "20.12.7"
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
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 time import gmtime, strftime
|
from time import gmtime, strftime
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
|
|
||||||
|
@ -26,28 +25,33 @@ 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
|
root_path = file_path = path.abspath(unquote(file_or_directory))
|
||||||
# from herping a derp and treating the uri as an absolute path
|
|
||||||
root_path = file_path = file_or_directory
|
|
||||||
if file_uri:
|
|
||||||
file_path = path.join(file_or_directory, sub("^[/]*", "", 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("/")
|
||||||
|
|
||||||
|
segments = unquoted_file_uri.split("/")
|
||||||
|
if ".." in segments or any(sep in segment for segment in segments):
|
||||||
|
raise InvalidUsage("Invalid URL")
|
||||||
|
|
||||||
|
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 FileNotFound(
|
||||||
"File not found", path=file_or_directory, relative_url=file_uri
|
"File not found",
|
||||||
|
path=file_or_directory,
|
||||||
|
relative_url=file_uri,
|
||||||
)
|
)
|
||||||
|
|
||||||
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,41 @@ 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)
|
||||||
|
url = (
|
||||||
|
"/foo"
|
||||||
|
+ str(double_dotted_directory_file)[len(static_file_directory) :]
|
||||||
|
)
|
||||||
|
_, response = app.test_client.get(url)
|
||||||
|
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/..%2Fstatic/test.file")
|
||||||
|
assert response.status == 400
|
||||||
|
|
||||||
|
|
||||||
|
@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 == 400
|
||||||
|
_, response = app.test_client.get("/foo/static\\../static/test.file")
|
||||||
|
assert response.status == 400
|
||||||
|
|
Loading…
Reference in New Issue
Block a user