Skip to content

Commit 86baaef

Browse files
committed
Use pathlib for path resolution (#2506)
1 parent 2b4b78d commit 86baaef

3 files changed

Lines changed: 64 additions & 41 deletions

File tree

codecov.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
coverage:
2+
status:
3+
patch:
4+
default:
5+
target: auto
6+
threshold: 0.75
7+
informational: true
8+
project:
9+
default:
10+
target: auto
11+
threshold: 0.5
12+
precision: 3
13+
codecov:
14+
require_ci_to_pass: false
15+
ignore:
16+
- "sanic/__main__.py"
17+
- "sanic/compat.py"
18+
- "sanic/reloader_helpers.py"
19+
- "sanic/simple.py"
20+
- "sanic/utils.py"
21+
- "sanic/cli"
22+
- ".github/"
23+
- "changelogs/"
24+
- "docker/"
25+
- "docs/"
26+
- "examples/"
27+
- "scripts/"
28+
- "tests/"

sanic/mixins/routes.py

Lines changed: 30 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
from functools import partial, wraps
44
from inspect import getsource, signature
55
from mimetypes import guess_type
6-
from os import path, sep
7-
from pathlib import PurePath
6+
from os import path
7+
from pathlib import Path, PurePath
88
from textwrap import dedent
99
from time import gmtime, strftime
1010
from typing import Any, Callable, Iterable, List, Optional, Set, Tuple, Union
@@ -16,12 +16,7 @@
1616
from sanic.compat import stat_async
1717
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE, HTTP_METHODS
1818
from sanic.errorpages import RESPONSE_MAPPING
19-
from sanic.exceptions import (
20-
ContentRangeError,
21-
FileNotFound,
22-
HeaderNotFound,
23-
InvalidUsage,
24-
)
19+
from sanic.exceptions import FileNotFound, HeaderNotFound, RangeNotSatisfiable
2520
from sanic.handlers import ContentRangeHandler
2621
from sanic.log import deprecation, error_logger
2722
from sanic.models.futures import FutureRoute, FutureStatic
@@ -774,39 +769,40 @@ async def _static_request_handler(
774769
content_type=None,
775770
__file_uri__=None,
776771
):
777-
<<<<<<< HEAD
778-
# Using this to determine if the URL is trying to break out of the path
779-
# served. os.path.realpath seems to be very slow
780-
if __file_uri__ and "../" in __file_uri__:
781-
raise InvalidUsage("Invalid URL")
782-
=======
783-
>>>>>>> 9d415e4 (Prevent directory traversion with static files (#2495))
784772
# Merge served directory and requested file if provided
785-
root_path = file_path = path.abspath(unquote(file_or_directory))
773+
file_path_raw = Path(unquote(file_or_directory))
774+
root_path = file_path = file_path_raw.resolve()
775+
not_found = FileNotFound(
776+
"File not found",
777+
path=file_or_directory,
778+
relative_url=__file_uri__,
779+
)
786780

787781
if __file_uri__:
788782
# Strip all / that in the beginning of the URL to help prevent
789783
# python from herping a derp and treating the uri as an
790784
# absolute path
791785
unquoted_file_uri = unquote(__file_uri__).lstrip("/")
786+
file_path_raw = Path(file_or_directory, unquoted_file_uri)
787+
file_path = file_path_raw.resolve()
788+
if (
789+
file_path < root_path and not file_path_raw.is_symlink()
790+
) or file_path_raw.match("../**/*"):
791+
error_logger.exception(
792+
f"File not found: path={file_or_directory}, "
793+
f"relative_url={__file_uri__}"
794+
)
795+
raise not_found
792796

793-
segments = unquoted_file_uri.split("/")
794-
if ".." in segments or any(sep in segment for segment in segments):
795-
raise BadRequest("Invalid URL")
796-
797-
file_path = path.join(file_or_directory, unquoted_file_uri)
798-
file_path = path.abspath(file_path)
799-
800-
if not file_path.startswith(root_path):
801-
error_logger.exception(
802-
f"File not found: path={file_or_directory}, "
803-
f"relative_url={__file_uri__}"
804-
)
805-
raise FileNotFound(
806-
"File not found",
807-
path=file_or_directory,
808-
relative_url=__file_uri__,
809-
)
797+
try:
798+
file_path.relative_to(root_path)
799+
except ValueError:
800+
if not file_path_raw.is_symlink():
801+
error_logger.exception(
802+
f"File not found: path={file_or_directory}, "
803+
f"relative_url={__file_uri__}"
804+
)
805+
raise not_found
810806
try:
811807
headers = {}
812808
# Check if the client has been sent this file before
@@ -874,11 +870,7 @@ async def _static_request_handler(
874870
except ContentRangeError:
875871
raise
876872
except FileNotFoundError:
877-
raise FileNotFound(
878-
"File not found",
879-
path=file_or_directory,
880-
relative_url=__file_uri__,
881-
)
873+
raise not_found
882874
except Exception:
883875
error_logger.exception(
884876
f"Exception in static request handler: "

tests/test_static.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -616,8 +616,11 @@ def test_dotted_dir_ok(
616616
def test_breakout(app: Sanic, static_file_directory: str):
617617
app.static("/foo", static_file_directory)
618618

619+
_, response = app.test_client.get("/foo/..%2Ffake/server.py")
620+
assert response.status == 404
621+
619622
_, response = app.test_client.get("/foo/..%2Fstatic/test.file")
620-
assert response.status == 400
623+
assert response.status == 404
621624

622625

623626
@pytest.mark.skipif(
@@ -629,6 +632,6 @@ def test_double_backslash_prohibited_on_win32(
629632
app.static("/foo", static_file_directory)
630633

631634
_, response = app.test_client.get("/foo/static/..\\static/test.file")
632-
assert response.status == 400
635+
assert response.status == 404
633636
_, response = app.test_client.get("/foo/static\\../static/test.file")
634-
assert response.status == 400
637+
assert response.status == 404

0 commit comments

Comments
 (0)