From 3d3156ed23aae7ee2f4782abb568996af597f3fd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 19:09:22 +0900 Subject: [PATCH 1/2] test(dav): reject advertised unsupported capabilities --- backend/tests/test_dav_api.py | 74 +++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 34 deletions(-) diff --git a/backend/tests/test_dav_api.py b/backend/tests/test_dav_api.py index 2c90a5a81..015e4b4b2 100644 --- a/backend/tests/test_dav_api.py +++ b/backend/tests/test_dav_api.py @@ -63,11 +63,19 @@ def test_dav_route_uses_signed_session_dependency(): assert response.status_code == 401 -def test_dav_options(dev_auth_dependency_overrides): +def test_dav_options_advertises_only_implemented_capabilities( + dev_auth_dependency_overrides, +): with TestClient(app) as client: response = client.options("/dav/user123/projects/", headers=AUTH_HEADERS) - assert response.status_code == 200 - assert "calendar-access" in response.headers.get("DAV", "") + + assert response.status_code == 200 + assert response.headers["DAV"] == "1" + assert { + method.strip() + for method in response.headers["Allow"].split(",") + if method.strip() + } == {"OPTIONS", "PROPFIND"} def test_dav_rejects_different_user_path(dev_auth_dependency_overrides): @@ -207,42 +215,40 @@ def test_dav_propfind_escapes_path_values( ET.fromstring(response.text) -def test_dav_put(dev_auth_dependency_overrides, caplog): - import logging - - caplog.set_level(logging.WARNING, logger="api.dav") - with TestClient(app) as client: - response = client.put( - "/dav/user123/projects/file.ics", - content=b"BEGIN:VCALENDAR\r\nEND:VCALENDAR", - headers=AUTH_HEADERS, - ) - assert response.status_code == 501 - assert "Provider-backed DAV writeback is not implemented" in response.text - assert "etag" not in {header.lower() for header in response.headers} - assert any( - "provider-backed DAV writeback is not implemented" in record.getMessage() - for record in caplog.records - ) - - -def test_dav_unsupported_method_logs_reason(dev_auth_dependency_overrides, caplog): - import logging - - caplog.set_level(logging.WARNING, logger="api.dav") +@pytest.mark.parametrize( + "method", + [ + "GET", + "PUT", + "DELETE", + "MKCOL", + "REPORT", + "PROPPATCH", + "COPY", + "MOVE", + "LOCK", + "UNLOCK", + ], +) +def test_dav_unimplemented_methods_are_not_registered( + dev_auth_dependency_overrides, + method, +): with TestClient(app) as client: - response = client.delete( + response = client.request( + method, "/dav/user123/projects/file.ics", + content=b"BEGIN:VCALENDAR\r\nEND:VCALENDAR" if method == "PUT" else None, headers=AUTH_HEADERS, ) - assert response.status_code == 501 - assert "Provider-backed DAV method is not implemented" in response.text - assert any( - "method is not implemented for the provider-backed DAV gateway" - in record.getMessage() - for record in caplog.records - ) + assert response.status_code == 405 + assert "etag" not in {header.lower() for header in response.headers} + assert { + allowed.strip() + for allowed in response.headers["Allow"].split(",") + if allowed.strip() + } == {"OPTIONS", "PROPFIND"} def test_dav_log_injection_prevention(dev_auth_dependency_overrides, caplog): From 3a749eb54c892157c1aed85d619e8d82efeb4fa3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 19:10:15 +0900 Subject: [PATCH 2/2] fix(dav): expose only implemented protocol capabilities --- backend/api/dav.py | 70 ++++++++++++---------------------------------- 1 file changed, 18 insertions(+), 52 deletions(-) diff --git a/backend/api/dav.py b/backend/api/dav.py index 0e05d624e..3cede071c 100644 --- a/backend/api/dav.py +++ b/backend/api/dav.py @@ -12,6 +12,7 @@ router = APIRouter(prefix="/dav", tags=["dav"]) +IMPLEMENTED_DAV_METHODS = ("OPTIONS", "PROPFIND") _DAV_AUTHORIZATION_PATH_MAX_CHARACTERS = 8192 _HEX_DIGITS = frozenset("0123456789abcdefABCDEF") _DAV_STRUCTURAL_OCTETS = frozenset( @@ -209,20 +210,20 @@ async def _handle_project_propfind( @router.api_route( "/{path:path}", - methods=["PROPFIND", "REPORT", "MKCOL", "GET", "PUT", "DELETE", "OPTIONS"], + methods=list(IMPLEMENTED_DAV_METHODS), ) async def dav_handler( request: Request, path: str, auth_context: AuthContext = Depends(get_auth_context), db: AsyncSession = Depends(get_db), -): - """ - Route the authenticated DAV surface that is implemented for this slice. +) -> Response: + """Serve only the authenticated DAV capabilities implemented in production. - Collection discovery is served from the server-side project registry. - Provider-backed writeback stays fail-closed until source capability and - ETag/If-Match enforcement are available through signed writeback intents. + Project collection discovery is available through ``PROPFIND``. Unsupported + writeback and richer DAV verbs are deliberately not registered, so clients + receive ``405 Method Not Allowed`` instead of a misleading advertised + capability that can only return ``501 Not Implemented``. """ normalized_path = _normalize_dav_authorization_path(path) _ensure_dav_owner_scope(normalized_path, auth_context) @@ -230,52 +231,17 @@ async def dav_handler( logger.info("DAV Request: %s /%s", request.method, safe_path) if request.method == "OPTIONS": - headers = { - "DAV": "1, 2, 3, calendar-access, addressbook", - "Allow": ( - "OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, COPY, MOVE, MKCOL, " - "PROPFIND, PROPPATCH, LOCK, UNLOCK, REPORT" - ), - } - return Response(status_code=200, headers=headers) - - if request.method == "PROPFIND": - return await _handle_project_propfind( - request=request, - path=normalized_path, - auth_context=auth_context, - db=db, - ) - - if request.method == "PUT": - body = await request.body() - logger.info("DAV PUT received %s bytes at /%s", len(body), safe_path) - logger.warning( - "DAV PUT rejected at /%s: provider-backed DAV writeback is not " - "implemented; signed writeback-intent API is required", - safe_path, - ) return Response( - content=( - "Provider-backed DAV writeback is not implemented; use signed " - "writeback-intent APIs until source, capability, and " - "ETag/If-Match checks are enforced." - ), - media_type="text/plain", - status_code=501, + status_code=200, + headers={ + "DAV": "1", + "Allow": ", ".join(IMPLEMENTED_DAV_METHODS), + }, ) - logger.warning( - "DAV %s rejected at /%s: method is not implemented for the " - "provider-backed DAV gateway", - request.method, - safe_path, - ) - return Response( - content=( - "Provider-backed DAV method is not implemented; use supported " - "PROPFIND/OPTIONS discovery or signed writeback-intent APIs." - ), - media_type="text/plain", - status_code=501, + return await _handle_project_propfind( + request=request, + path=normalized_path, + auth_context=auth_context, + db=db, )