Files
openhop_repeater/tests/test_frontend_static_confinement.py

82 lines
3.4 KiB
Python

"""Static frontend roots must use path components, not string prefixes."""
from http.client import HTTPConnection
from threading import Thread
from types import SimpleNamespace
from wsgiref.simple_server import make_server
import cherrypy
import pytest
from repeater.web.http_server import StatsApp
@pytest.mark.parametrize("directory", ["assets", "_next"])
@pytest.mark.parametrize("symlink", [False, True])
def test_static_rejects_prefix_sharing_sibling(tmp_path, monkeypatch, directory, symlink):
root = tmp_path / directory
root.mkdir()
private = tmp_path / (directory + "-private")
private.mkdir()
(private / "probe.txt").write_text("OUTSIDE-ROOT")
parts = ("..", private.name, "probe.txt")
if symlink:
(root / "linked").symlink_to(private, target_is_directory=True)
parts = ("linked", "probe.txt")
monkeypatch.setattr(cherrypy, "response", SimpleNamespace(headers={}))
app = object.__new__(StatsApp)
with pytest.raises(cherrypy.NotFound):
app._serve_static_file(str(root), parts)
def test_static_keeps_nested_assets(tmp_path, monkeypatch):
(tmp_path / "nested").mkdir()
(tmp_path / "nested" / "app.js").write_text("PUBLIC-SCRIPT")
monkeypatch.setattr(cherrypy, "response", SimpleNamespace(headers={}))
app = object.__new__(StatsApp)
assert app._serve_static_file(str(tmp_path), ("nested", "app.js")) == b"PUBLIC-SCRIPT"
@pytest.mark.parametrize("directory", ["assets", "_next"])
def test_public_dispatch_rejects_encoded_sibling_paths(tmp_path, monkeypatch, directory):
root = tmp_path / directory
root.mkdir()
(root / "app.js").write_text("PUBLIC-SCRIPT")
private = tmp_path / (directory + "-private")
private.mkdir()
(private / "probe.txt").write_text("OUTSIDE-ROOT")
monkeypatch.setattr(cherrypy, "request", cherrypy._ThreadLocalProxy("request"))
monkeypatch.setattr(cherrypy, "response", cherrypy._ThreadLocalProxy("response"))
app = object.__new__(StatsApp)
app.html_dir = str(tmp_path)
monkeypatch.setattr(app, "_resolve_html_dir", lambda: app.html_dir)
application = cherrypy.Application(app, script_name="")
with make_server("127.0.0.1", 0, application) as server:
worker = Thread(target=server.serve_forever, daemon=True)
worker.start()
try:
for path in (
f"/{directory}/../{directory}-private/probe.txt",
f"/{directory}/..%2f{directory}-private%2fprobe.txt",
f"/{directory}/%2e%2e/{directory}-private/probe.txt",
):
connection = HTTPConnection("127.0.0.1", server.server_port, timeout=3)
try:
connection.request("GET", path)
response = connection.getresponse()
assert response.status == 404, path
assert b"OUTSIDE-ROOT" not in response.read()
finally:
connection.close()
connection = HTTPConnection("127.0.0.1", server.server_port, timeout=3)
try:
connection.request("GET", f"/{directory}/app.js")
response = connection.getresponse()
assert response.status == 200
assert response.read() == b"PUBLIC-SCRIPT"
finally:
connection.close()
finally:
server.shutdown()
worker.join(timeout=3)