Confine calendar connectors to configured origins

This commit is contained in:
2026-07-21 03:16:45 +02:00
parent 68b165db7b
commit a36a9e8c19
6 changed files with 467 additions and 41 deletions

View File

@@ -0,0 +1,90 @@
from __future__ import annotations
import contextlib
import threading
import unittest
from collections.abc import Iterator
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from govoplan_calendar.backend.service import CalendarError, http_request
@contextlib.contextmanager
def running_http_server(handler: type[BaseHTTPRequestHandler]) -> Iterator[str]:
server = ThreadingHTTPServer(("127.0.0.1", 0), handler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
host, port = server.server_address
yield f"http://{host}:{port}"
finally:
server.shutdown()
server.server_close()
thread.join(timeout=2)
class CalendarHttpSecurityTests(unittest.TestCase):
def test_cross_origin_redirect_does_not_forward_authorization(self) -> None:
forwarded_authorization: list[str | None] = []
class TargetHandler(BaseHTTPRequestHandler):
def do_GET(self) -> None: # noqa: N802 - BaseHTTPRequestHandler API
forwarded_authorization.append(self.headers.get("Authorization"))
self.send_response(200)
self.end_headers()
def log_message(self, _format: str, *_args: object) -> None:
return
with running_http_server(TargetHandler) as target_url:
class RedirectHandler(BaseHTTPRequestHandler):
def do_GET(self) -> None: # noqa: N802 - BaseHTTPRequestHandler API
self.send_response(302)
self.send_header("Location", f"{target_url}/stolen")
self.end_headers()
def log_message(self, _format: str, *_args: object) -> None:
return
with running_http_server(RedirectHandler) as source_url:
with self.assertRaisesRegex(CalendarError, "HTTP 302"):
http_request(
f"{source_url}/feed",
headers={"Authorization": "Bearer top-secret"},
timeout=2,
)
self.assertEqual(forwarded_authorization, [])
def test_same_origin_redirect_remains_supported(self) -> None:
forwarded_authorization: list[str | None] = []
class RedirectHandler(BaseHTTPRequestHandler):
def do_GET(self) -> None: # noqa: N802 - BaseHTTPRequestHandler API
if self.path == "/feed":
self.send_response(302)
self.send_header("Location", "/calendar.ics")
self.end_headers()
return
forwarded_authorization.append(self.headers.get("Authorization"))
self.send_response(200)
self.end_headers()
self.wfile.write(b"calendar")
def log_message(self, _format: str, *_args: object) -> None:
return
with running_http_server(RedirectHandler) as source_url:
status, _headers, body = http_request(
f"{source_url}/feed",
headers={"Authorization": "Bearer expected"},
timeout=2,
)
self.assertEqual(status, 200)
self.assertEqual(body, "calendar")
self.assertEqual(forwarded_authorization, ["Bearer expected"])
if __name__ == "__main__":
unittest.main()