From ca4845f64688de95469b0762a6459cfc79524148 Mon Sep 17 00:00:00 2001 From: Peter Buchegger Date: Wed, 4 Nov 2026 12:12:01 +0100 Subject: [PATCH] fix(federation): Block legacy invite endpoint fixes #1 --- README.md | 6 ++- docs/setup.md | 3 +- docs/technical-flows.md | 4 +- dynamic.yml | 14 +++--- .../adapter/inbound/LegacyInviteController.kt | 46 +++++++++++++++++ .../inbound/LegacyInviteControllerTest.kt | 50 +++++++++++++++++++ 6 files changed, 113 insertions(+), 10 deletions(-) create mode 100644 src/main/kotlin/io/cloudflight/pgk/proxy/invitation/adapter/inbound/LegacyInviteController.kt create mode 100644 src/test/kotlin/io/cloudflight/pgk/proxy/invitation/adapter/inbound/LegacyInviteControllerTest.kt diff --git a/README.md b/README.md index 4cd3bbb..f1cc5ab 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Matrix-Client (Element, Element X, FluffyChat …) │ ├─ /_matrix/client/v3/user_directory/search ──► pgk-proxy ──► FHIR (Provider Directory) │ ├─ /_matrix/client/v3/createRoom, /_matrix/client/v3/rooms/{roomId}/invite (Ausgangsprüfung) ──► pgk-proxy ──► Matrix-Server │ ├─ /_matrix/federation/v2/invite* (Einlassprüfung) ──► pgk-proxy ──► Matrix-Server + │ ├─ /_matrix/federation/v1/invite* (Legacy, deaktiviert) ──► pgk-proxy (403) │ └─ alles andere ──► Matrix Server ▼ Matrix-Server ◄── Föderation (8448) ──► externe föderierte Matrix-Server @@ -61,8 +62,9 @@ Ausführliche Schritte: [docs/setup.md](docs/setup.md). erstellt/aktualisiert/löscht. - **Föderation:** Föderierte Einladungen an `matrix.traefik.me` laufen über u.a. `/_matrix/federation/v2/invite` durch den Proxy, der Whitelist-, Organisations- und - persönliche Regeln prüft, bevor er an Synapse weiterleitet. + persönliche Regeln prüft, bevor er an Synapse weiterleitet. Die Legacy-Route + `/_matrix/federation/v1/invite` ist deaktiviert und antwortet immer mit 403. ## Lizenz -Diese Software ist unter der [Apache License 2.0](LICENSE.txt) lizensiert. \ No newline at end of file +Diese Software ist unter der [Apache License 2.0](LICENSE.txt) lizensiert. diff --git a/docs/setup.md b/docs/setup.md index c5cabcb..e509673 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -142,7 +142,8 @@ Die Föderation zwischen den beiden Servern läuft über Port **8448** durch Tra (`Host(matrix.traefik.me)` / `Host(partner.traefik.me)`); beide Synapse-Container betreiben einen eigenen Föderations-Listener auf 8448. Eingehende Föderations-Einladungen an `matrix.traefik.me` werden vom pgk-proxy abgefangen (`/_matrix/federation/v2/invite` → Proxy), -der vor der Weiterleitung an Synapse die Föderations-Whitelist prüft. +der vor der Weiterleitung an Synapse die Föderations-Whitelist prüft. Die Legacy-Route +`/_matrix/federation/v1/invite` ist deaktiviert und antwortet immer mit 403. > **Föderations-Auflösung der Peers:** Da `*.traefik.me` über den öffentlichen DNS auf > `127.0.0.1` zeigt, erreichen sich die Peers nie über den öffentlichen Eintrag. Der diff --git a/docs/technical-flows.md b/docs/technical-flows.md index d2184b4..cb6af30 100644 --- a/docs/technical-flows.md +++ b/docs/technical-flows.md @@ -6,7 +6,8 @@ konkreten Beispielanfragen: 1. **Föderierte Benutzersuche** über den Proxy und den FHIR-Server (`/_matrix/client/v3/user_directory/search`) 2. **Prüfung einer föderierten Raumeinladung** (`/_matrix/federation/v2/invite`) sowie die - analoge Prüfung ausgehender Einladungen (`createRoom` / `rooms/{roomId}/invite`) + analoge Prüfung ausgehender Einladungen (`createRoom` / `rooms/{roomId}/invite`). + Die Legacy-Route `/_matrix/federation/v1/invite` ist deaktiviert und antwortet mit 403. Verwandte Dokumente: [Setup](setup.md), [Benutzerhandbuch](user-guide.md), [Administrationshandbuch](admin-guide.md), [FHIR-Leitfaden](fhir-guide.md). @@ -19,6 +20,7 @@ Traefik leitet `matrix.traefik.me` mit Ausnahme der Proxy-Pfade direkt an den Ma - `/_matrix/client/v3/user_directory/search` → **pgk-proxy** - `/_matrix/client/v3/createRoom`, `/_matrix/client/v3/rooms/{roomId}/invite` → **pgk-proxy** - `/_matrix/federation/v2/invite` (Client-Traffic 443 und Föderations-Traffic 8448) → **pgk-proxy** +- `/_matrix/federation/v1/invite` (Legacy, deaktiviert) → **pgk-proxy** (antwortet immer mit 403) - alles andere → Matrix-Server --- diff --git a/dynamic.yml b/dynamic.yml index a25f0da..205e84d 100644 --- a/dynamic.yml +++ b/dynamic.yml @@ -2,11 +2,12 @@ http: routers: # ===================== Primary server (matrix.traefik.me) ===================== # Client traffic (443) - only the app's FHIR-backed user directory search goes to pgk-proxy; - # everything else (including federation-v2 invite via 443) goes to Synapse. Invitations are + # everything else (including federation invite via 443) goes to Synapse. Invitations are # enforced exclusively at the federation boundary, so createRoom / rooms/{id}/invite are NOT - # intercepted: same-domain invites are always allowed. + # intercepted: same-domain invites are always allowed. The legacy v1 invite endpoint is + # disabled by the proxy (403) and never reaches Synapse. matrix-proxy: - rule: "Host(`matrix.traefik.me`) && (PathPrefix(`/_matrix/client/v3/user_directory/search`) || PathPrefix(`/_matrix/federation/v2/invite`))" + rule: "Host(`matrix.traefik.me`) && (PathPrefix(`/_matrix/client/v3/user_directory/search`) || PathPrefix(`/_matrix/federation/v1/invite`) || PathPrefix(`/_matrix/federation/v2/invite`))" entrypoints: [websecure] tls: true service: matrix-proxy @@ -29,7 +30,7 @@ http: # forwards the accepted invite directly to the in-cluster Synapse (pgk.federation.forward-base-url), # so no header-based passthrough routing is needed here anymore. fed-matrix-proxy: - rule: "Host(`matrix.traefik.me`) && PathPrefix(`/_matrix/federation/v2/invite`)" + rule: "Host(`matrix.traefik.me`) && (PathPrefix(`/_matrix/federation/v1/invite`) || PathPrefix(`/_matrix/federation/v2/invite`))" entrypoints: [federation] tls: true service: matrix-proxy @@ -39,9 +40,10 @@ http: tls: true service: matrix-synapse # Federation to partner.traefik.me - the first hop goes to the proxy for the policy check, - # forwarded copies go straight to the partner Synapse (directly, not through Traefik). + # forwarded copies go straight to the partner Synapse (directly, not through Traefik). The + # legacy v1 invite endpoint is disabled by the proxy (403) and never reaches Synapse. fed-partner-proxy: - rule: "Host(`partner.traefik.me`) && PathPrefix(`/_matrix/federation/v2/invite`)" + rule: "Host(`partner.traefik.me`) && (PathPrefix(`/_matrix/federation/v1/invite`) || PathPrefix(`/_matrix/federation/v2/invite`))" entrypoints: [federation] tls: true service: matrix-proxy diff --git a/src/main/kotlin/io/cloudflight/pgk/proxy/invitation/adapter/inbound/LegacyInviteController.kt b/src/main/kotlin/io/cloudflight/pgk/proxy/invitation/adapter/inbound/LegacyInviteController.kt new file mode 100644 index 0000000..9344f1a --- /dev/null +++ b/src/main/kotlin/io/cloudflight/pgk/proxy/invitation/adapter/inbound/LegacyInviteController.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2026 MA 01 - Wien Digital + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudflight.pgk.proxy.invitation.adapter.inbound + +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +/** + * Disables the legacy federation invite endpoint (`/_matrix/federation/v1/invite`). Invitations + * are only accepted via `/_matrix/federation/v2/invite` (see [InvitationController]); the v1 + * route is permanently retired and always answers with 403 so no federated invite can reach + * Synapse through it. + */ +@RestController +@RequestMapping("/_matrix/federation/v1") +class LegacyInviteController { + + @PutMapping("/invite/{roomId}/{eventId}") + fun disabledInvitation( + @PathVariable roomId: String, + @PathVariable eventId: String, + ): ResponseEntity = + ResponseEntity + .status(HttpStatus.FORBIDDEN) + .contentType(MediaType.APPLICATION_JSON) + .body("""{"errcode": "M_FORBIDDEN", "error": "Legacy federation invite endpoint (v1) is disabled."}""") +} diff --git a/src/test/kotlin/io/cloudflight/pgk/proxy/invitation/adapter/inbound/LegacyInviteControllerTest.kt b/src/test/kotlin/io/cloudflight/pgk/proxy/invitation/adapter/inbound/LegacyInviteControllerTest.kt new file mode 100644 index 0000000..287d62c --- /dev/null +++ b/src/test/kotlin/io/cloudflight/pgk/proxy/invitation/adapter/inbound/LegacyInviteControllerTest.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2026 MA 01 - Wien Digital + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudflight.pgk.proxy.invitation.adapter.inbound + +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +import org.springframework.test.web.servlet.setup.MockMvcBuilders + +class LegacyInviteControllerTest { + + private lateinit var mockMvc: MockMvc + + @BeforeEach + fun setUp() { + mockMvc = MockMvcBuilders + .standaloneSetup(LegacyInviteController()) + .build() + } + + @Test + @DisplayName("returns 403 for the legacy v1 federation invite endpoint") + fun returns403ForLegacyV1Invite() { + mockMvc.perform(put("/_matrix/federation/v1/invite/room1/evt1") + .contentType("application/json") + .content("""{"origin":"matrix.partner.example","event":{"type":"m.room.member"}}""")) + .andExpect(status().isForbidden) + .andExpect(content().json( + """{"errcode": "M_FORBIDDEN", "error": "Legacy federation invite endpoint (v1) is disabled."}""" + )) + } +}