diff --git a/starlette/staticfiles.py b/starlette/staticfiles.py index 4d075b3ed..4c856063c 100644 --- a/starlette/staticfiles.py +++ b/starlette/staticfiles.py @@ -169,7 +169,7 @@ def lookup_path( else: full_path = os.path.realpath(joined_path) directory = os.path.realpath(directory) - if os.path.commonprefix([full_path, directory]) != directory: + if os.path.commonpath([full_path, directory]) != directory: # Don't allow misbehaving clients to break out of the static files # directory. continue diff --git a/tests/test_staticfiles.py b/tests/test_staticfiles.py index eb6c73f7f..23d0b57fc 100644 --- a/tests/test_staticfiles.py +++ b/tests/test_staticfiles.py @@ -1,8 +1,8 @@ import os -import pathlib import stat import tempfile import time +from pathlib import Path import anyio import pytest @@ -28,13 +28,12 @@ def test_staticfiles(tmpdir, test_client_factory): assert response.text == "" -def test_staticfiles_with_pathlib(tmpdir, test_client_factory): - base_dir = pathlib.Path(tmpdir) - path = base_dir / "example.txt" +def test_staticfiles_with_pathlib(tmp_path: Path, test_client_factory): + path = tmp_path / "example.txt" with open(path, "w") as file: file.write("") - app = StaticFiles(directory=base_dir) + app = StaticFiles(directory=tmp_path) client = test_client_factory(app) response = client.get("/example.txt") assert response.status_code == 200 @@ -516,3 +515,36 @@ def test_staticfiles_disallows_path_traversal_with_symlinks(tmpdir): assert exc_info.value.status_code == 404 assert exc_info.value.detail == "Not Found" + + +def test_staticfiles_avoids_path_traversal(tmp_path: Path): + statics_path = tmp_path / "static" + statics_disallow_path = tmp_path / "static_disallow" + + statics_path.mkdir() + statics_disallow_path.mkdir() + + static_index_file = statics_path / "index.html" + statics_disallow_path_index_file = statics_disallow_path / "index.html" + static_file = tmp_path / "static1.txt" + + static_index_file.write_text("

Hello

") + statics_disallow_path_index_file.write_text("

Private

") + static_file.write_text("Private") + + app = StaticFiles(directory=statics_path) + + # We can't test this with 'httpx', so we test the app directly here. + path = app.get_path({"path": "/../static1.txt"}) + with pytest.raises(HTTPException) as exc_info: + anyio.run(app.get_response, path, {"method": "GET"}) + + assert exc_info.value.status_code == 404 + assert exc_info.value.detail == "Not Found" + + path = app.get_path({"path": "/../static_disallow/index.html"}) + with pytest.raises(HTTPException) as exc_info: + anyio.run(app.get_response, path, {"method": "GET"}) + + assert exc_info.value.status_code == 404 + assert exc_info.value.detail == "Not Found"