Skip to content

Commit

Permalink
Path protection with pathlib
Browse files Browse the repository at this point in the history
  • Loading branch information
ahopkins committed Jul 31, 2022
1 parent 3b85b3b commit b4360d4
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 16 deletions.
2 changes: 1 addition & 1 deletion sanic/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "20.12.6"
__version__ = "20.12.7"
34 changes: 19 additions & 15 deletions sanic/static.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from functools import partial, wraps
from mimetypes import guess_type
from os import path
from re import sub
from os import path, sep
from time import gmtime, strftime
from urllib.parse import unquote

Expand All @@ -26,28 +25,33 @@ async def _static_request_handler(
content_type=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
# Strip all / that in the beginning of the URL to help prevent python
# from herping a derp and treating the uri as an absolute path
root_path = file_path = file_or_directory
root_path = file_path = path.abspath(unquote(file_or_directory))

if file_uri:
file_path = path.join(file_or_directory, sub("^[/]*", "", file_uri))
# Strip all / that in the beginning of the URL to help prevent
# python from herping a derp and treating the uri as an
# 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)

# URL decode the path sent by the browser otherwise we won't be able to
# match filenames which got encoded (filenames with spaces etc)
file_path = path.abspath(unquote(file_path))
if not file_path.startswith(path.abspath(unquote(root_path))):
if not file_path.startswith(root_path):
error_logger.exception(
f"File not found: path={file_or_directory}, "
f"relative_url={file_uri}"
)
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:
headers = {}
# Check if the client has been sent this file before
Expand Down
58 changes: 58 additions & 0 deletions tests/test_static.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import inspect
import os
import sys

from pathlib import Path
from time import gmtime, strftime

import pytest

from sanic.app import Sanic


@pytest.fixture(scope="module")
def static_file_directory():
Expand All @@ -15,6 +19,22 @@ def static_file_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):
return os.path.join(static_file_directory, file_name)

Expand Down Expand Up @@ -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}")

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

0 comments on commit b4360d4

Please sign in to comment.