Skip to content

Commit ca4845f

Browse files
committed
fix(federation): Block legacy invite endpoint
fixes #1
1 parent 00fc602 commit ca4845f

6 files changed

Lines changed: 113 additions & 10 deletions

File tree

‎README.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Matrix-Client (Element, Element X, FluffyChat …)
2020
│ ├─ /_matrix/client/v3/user_directory/search ──► pgk-proxy ──► FHIR (Provider Directory)
2121
│ ├─ /_matrix/client/v3/createRoom, /_matrix/client/v3/rooms/{roomId}/invite (Ausgangsprüfung) ──► pgk-proxy ──► Matrix-Server
2222
│ ├─ /_matrix/federation/v2/invite* (Einlassprüfung) ──► pgk-proxy ──► Matrix-Server
23+
│ ├─ /_matrix/federation/v1/invite* (Legacy, deaktiviert) ──► pgk-proxy (403)
2324
│ └─ alles andere ──► Matrix Server
2425
▼
2526
Matrix-Server ◄── Föderation (8448) ──► externe föderierte Matrix-Server
@@ -61,8 +62,9 @@ Ausführliche Schritte: [docs/setup.md](docs/setup.md).
6162
erstellt/aktualisiert/löscht.
6263
- **Föderation:** Föderierte Einladungen an `matrix.traefik.me` laufen über
6364
u.a. `/_matrix/federation/v2/invite` durch den Proxy, der Whitelist-, Organisations- und
64-
persönliche Regeln prüft, bevor er an Synapse weiterleitet.
65+
persönliche Regeln prüft, bevor er an Synapse weiterleitet. Die Legacy-Route
66+
`/_matrix/federation/v1/invite` ist deaktiviert und antwortet immer mit 403.
6567

6668
## Lizenz
6769

68-
Diese Software ist unter der [Apache License 2.0](LICENSE.txt) lizensiert.
70+
Diese Software ist unter der [Apache License 2.0](LICENSE.txt) lizensiert.

‎docs/setup.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,8 @@ Die Föderation zwischen den beiden Servern läuft über Port **8448** durch Tra
142142
(`Host(matrix.traefik.me)` / `Host(partner.traefik.me)`); beide Synapse-Container betreiben einen
143143
eigenen Föderations-Listener auf 8448. Eingehende Föderations-Einladungen an
144144
`matrix.traefik.me` werden vom pgk-proxy abgefangen (`/_matrix/federation/v2/invite` → Proxy),
145-
der vor der Weiterleitung an Synapse die Föderations-Whitelist prüft.
145+
der vor der Weiterleitung an Synapse die Föderations-Whitelist prüft. Die Legacy-Route
146+
`/_matrix/federation/v1/invite` ist deaktiviert und antwortet immer mit 403.
146147

147148
> **Föderations-Auflösung der Peers:** Da `*.traefik.me` über den öffentlichen DNS auf
148149
> `127.0.0.1` zeigt, erreichen sich die Peers nie über den öffentlichen Eintrag. Der

‎docs/technical-flows.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ konkreten Beispielanfragen:
66
1. **Föderierte Benutzersuche** über den Proxy und den FHIR-Server
77
(`/_matrix/client/v3/user_directory/search`)
88
2. **Prüfung einer föderierten Raumeinladung** (`/_matrix/federation/v2/invite`) sowie die
9-
analoge Prüfung ausgehender Einladungen (`createRoom` / `rooms/{roomId}/invite`)
9+
analoge Prüfung ausgehender Einladungen (`createRoom` / `rooms/{roomId}/invite`).
10+
Die Legacy-Route `/_matrix/federation/v1/invite` ist deaktiviert und antwortet mit 403.
1011

1112
Verwandte Dokumente: [Setup](setup.md), [Benutzerhandbuch](user-guide.md),
1213
[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
1920
- `/_matrix/client/v3/user_directory/search` → **pgk-proxy**
2021
- `/_matrix/client/v3/createRoom`, `/_matrix/client/v3/rooms/{roomId}/invite` → **pgk-proxy**
2122
- `/_matrix/federation/v2/invite` (Client-Traffic 443 und Föderations-Traffic 8448) → **pgk-proxy**
23+
- `/_matrix/federation/v1/invite` (Legacy, deaktiviert) → **pgk-proxy** (antwortet immer mit 403)
2224
- alles andere → Matrix-Server
2325

2426
---

‎dynamic.yml‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ http:
22
routers:
33
# ===================== Primary server (matrix.traefik.me) =====================
44
# Client traffic (443) - only the app's FHIR-backed user directory search goes to pgk-proxy;
5-
# everything else (including federation-v2 invite via 443) goes to Synapse. Invitations are
5+
# everything else (including federation invite via 443) goes to Synapse. Invitations are
66
# enforced exclusively at the federation boundary, so createRoom / rooms/{id}/invite are NOT
7-
# intercepted: same-domain invites are always allowed.
7+
# intercepted: same-domain invites are always allowed. The legacy v1 invite endpoint is
8+
# disabled by the proxy (403) and never reaches Synapse.
89
matrix-proxy:
9-
rule: "Host(`matrix.traefik.me`) && (PathPrefix(`/_matrix/client/v3/user_directory/search`) || PathPrefix(`/_matrix/federation/v2/invite`))"
10+
rule: "Host(`matrix.traefik.me`) && (PathPrefix(`/_matrix/client/v3/user_directory/search`) || PathPrefix(`/_matrix/federation/v1/invite`) || PathPrefix(`/_matrix/federation/v2/invite`))"
1011
entrypoints: [websecure]
1112
tls: true
1213
service: matrix-proxy
@@ -29,7 +30,7 @@ http:
2930
# forwards the accepted invite directly to the in-cluster Synapse (pgk.federation.forward-base-url),
3031
# so no header-based passthrough routing is needed here anymore.
3132
fed-matrix-proxy:
32-
rule: "Host(`matrix.traefik.me`) && PathPrefix(`/_matrix/federation/v2/invite`)"
33+
rule: "Host(`matrix.traefik.me`) && (PathPrefix(`/_matrix/federation/v1/invite`) || PathPrefix(`/_matrix/federation/v2/invite`))"
3334
entrypoints: [federation]
3435
tls: true
3536
service: matrix-proxy
@@ -39,9 +40,10 @@ http:
3940
tls: true
4041
service: matrix-synapse
4142
# Federation to partner.traefik.me - the first hop goes to the proxy for the policy check,
42-
# forwarded copies go straight to the partner Synapse (directly, not through Traefik).
43+
# forwarded copies go straight to the partner Synapse (directly, not through Traefik). The
44+
# legacy v1 invite endpoint is disabled by the proxy (403) and never reaches Synapse.
4345
fed-partner-proxy:
44-
rule: "Host(`partner.traefik.me`) && PathPrefix(`/_matrix/federation/v2/invite`)"
46+
rule: "Host(`partner.traefik.me`) && (PathPrefix(`/_matrix/federation/v1/invite`) || PathPrefix(`/_matrix/federation/v2/invite`))"
4547
entrypoints: [federation]
4648
tls: true
4749
service: matrix-proxy
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2026 MA 01 - Wien Digital
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.cloudflight.pgk.proxy.invitation.adapter.inbound
18+
19+
import org.springframework.http.HttpStatus
20+
import org.springframework.http.MediaType
21+
import org.springframework.http.ResponseEntity
22+
import org.springframework.web.bind.annotation.PathVariable
23+
import org.springframework.web.bind.annotation.PutMapping
24+
import org.springframework.web.bind.annotation.RequestMapping
25+
import org.springframework.web.bind.annotation.RestController
26+
27+
/**
28+
* Disables the legacy federation invite endpoint (`/_matrix/federation/v1/invite`). Invitations
29+
* are only accepted via `/_matrix/federation/v2/invite` (see [InvitationController]); the v1
30+
* route is permanently retired and always answers with 403 so no federated invite can reach
31+
* Synapse through it.
32+
*/
33+
@RestController
34+
@RequestMapping("/_matrix/federation/v1")
35+
class LegacyInviteController {
36+
37+
@PutMapping("/invite/{roomId}/{eventId}")
38+
fun disabledInvitation(
39+
@PathVariable roomId: String,
40+
@PathVariable eventId: String,
41+
): ResponseEntity<String> =
42+
ResponseEntity
43+
.status(HttpStatus.FORBIDDEN)
44+
.contentType(MediaType.APPLICATION_JSON)
45+
.body("""{"errcode": "M_FORBIDDEN", "error": "Legacy federation invite endpoint (v1) is disabled."}""")
46+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2026 MA 01 - Wien Digital
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.cloudflight.pgk.proxy.invitation.adapter.inbound
18+
19+
import org.junit.jupiter.api.BeforeEach
20+
import org.junit.jupiter.api.DisplayName
21+
import org.junit.jupiter.api.Test
22+
import org.springframework.test.web.servlet.MockMvc
23+
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put
24+
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
25+
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
26+
import org.springframework.test.web.servlet.setup.MockMvcBuilders
27+
28+
class LegacyInviteControllerTest {
29+
30+
private lateinit var mockMvc: MockMvc
31+
32+
@BeforeEach
33+
fun setUp() {
34+
mockMvc = MockMvcBuilders
35+
.standaloneSetup(LegacyInviteController())
36+
.build()
37+
}
38+
39+
@Test
40+
@DisplayName("returns 403 for the legacy v1 federation invite endpoint")
41+
fun returns403ForLegacyV1Invite() {
42+
mockMvc.perform(put("/_matrix/federation/v1/invite/room1/evt1")
43+
.contentType("application/json")
44+
.content("""{"origin":"matrix.partner.example","event":{"type":"m.room.member"}}"""))
45+
.andExpect(status().isForbidden)
46+
.andExpect(content().json(
47+
"""{"errcode": "M_FORBIDDEN", "error": "Legacy federation invite endpoint (v1) is disabled."}"""
48+
))
49+
}
50+
}

0 commit comments

Comments
 (0)