fix: block path traversal in static handler
The LAN-bound HTTP server joined the raw URL path onto the project dir, so /../../.ssh/id_ed25519 (and %2e%2e variants) served any file the user could read. Resolve and require the path stay inside the project. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -102,8 +102,10 @@ class WorkbenchServer:
|
|||||||
async def static_handler(request):
|
async def static_handler(request):
|
||||||
proj = request.app["project_name"]
|
proj = request.app["project_name"]
|
||||||
path = request.match_info.get("path", "index.html") or "index.html"
|
path = request.match_info.get("path", "index.html") or "index.html"
|
||||||
file_path = project_path(proj, self.workbench_dir) / path
|
root = project_path(proj, self.workbench_dir).resolve()
|
||||||
if not file_path.exists():
|
file_path = (root / path).resolve()
|
||||||
|
# served on the LAN — never let "../" escape the project dir
|
||||||
|
if not file_path.is_relative_to(root) or not file_path.is_file():
|
||||||
return web.Response(status=404, text="Not found")
|
return web.Response(status=404, text="Not found")
|
||||||
return web.FileResponse(file_path)
|
return web.FileResponse(file_path)
|
||||||
|
|
||||||
|
|||||||
@@ -103,3 +103,24 @@ async def test_reconnect_on_startup(tmp_workbench):
|
|||||||
assert srv._active["live-proj"]["port"] == 8070
|
assert srv._active["live-proj"]["port"] == 8070
|
||||||
assert "dead-proj" not in srv._active
|
assert "dead-proj" not in srv._active
|
||||||
assert not (tmp_workbench / "dead-proj" / ".server.json").exists()
|
assert not (tmp_workbench / "dead-proj" / ".server.json").exists()
|
||||||
|
|
||||||
|
|
||||||
|
async def _live(server, name="p"):
|
||||||
|
import aiohttp
|
||||||
|
from workbench.project import create_project
|
||||||
|
create_project(name, "T", workbench_dir=server.workbench_dir)
|
||||||
|
port = await server._start_http_server(name)
|
||||||
|
return aiohttp.ClientSession(), f"http://127.0.0.1:{port}"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_static_blocks_path_traversal(server, tmp_workbench):
|
||||||
|
(tmp_workbench / "secret.txt").write_text("nope")
|
||||||
|
session, base = await _live(server)
|
||||||
|
async with session:
|
||||||
|
for evil in ["/../secret.txt", "/%2e%2e/secret.txt", "/..%2fsecret.txt"]:
|
||||||
|
async with session.get(base + evil, allow_redirects=False) as r:
|
||||||
|
assert r.status == 404, evil
|
||||||
|
async with session.get(base + "/") as r:
|
||||||
|
assert r.status == 200
|
||||||
|
await server.workbench_stop("p")
|
||||||
|
|||||||
Reference in New Issue
Block a user