Skip to content
Open
Changes from 17 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
6adc165
MSC4155: Invite filtering
Johennes Jun 13, 2024
43aef70
Fix typo
Johennes Jun 13, 2024
b8b226a
Update proposal with feedback from pull request
Johennes May 27, 2025
dcde420
Update proposals/4155-invite-filtering.md
Johennes May 30, 2025
04169f8
Update proposals/4155-invite-filtering.md
Johennes May 30, 2025
8c81fd5
Update proposals/4155-invite-filtering.md
Johennes May 30, 2025
d132a13
Update proposals/4155-invite-filtering.md
Johennes May 30, 2025
2345a5b
Update proposals/4155-invite-filtering.md
Johennes May 30, 2025
a558adb
Update proposals/4155-invite-filtering.md
Johennes May 30, 2025
a47dbce
Update proposals/4155-invite-filtering.md
Johennes May 30, 2025
e9478f0
Update proposals/4155-invite-filtering.md
Johennes Jun 2, 2025
29324e9
Drop capability and enforce the config on the server only
Johennes Jun 4, 2025
2f17aa7
Add missing client-side review option as a potential issue
Johennes Jun 4, 2025
3b248c5
Update proposals/4155-invite-filtering.md
Johennes Jun 5, 2025
230ed37
Update proposals/4155-invite-filtering.md
Johennes Jun 5, 2025
f916f98
Update proposals/4155-invite-filtering.md
Johennes Jun 5, 2025
44ecf7e
Update proposals/4155-invite-filtering.md
Johennes Jun 5, 2025
88fe9cc
Fix list indentation
Johennes Jun 26, 2025
be64f07
Extend alternatives
Johennes Jun 26, 2025
e526862
Update proposals/4155-invite-filtering.md
Johennes Jun 26, 2025
52c8bd5
Clarify interaction with m.ignored_user_list
Johennes Jun 30, 2025
0eee397
Fix typo
Johennes Jun 30, 2025
21521f0
Spell out what the globs should operate on
Johennes Jun 30, 2025
643c5bc
Avoid 'eponymous'
Johennes Jul 8, 2025
5c3cab8
Avoid over-specifying null / missing behaviour
Johennes Jul 8, 2025
16dd841
Update proposals/4155-invite-filtering.md
Johennes Jul 8, 2025
e735ee8
Add expanding m.ignored_user_list as an alternative
Johennes Jul 8, 2025
f65b9b2
Ignore ports when applying server globs
Johennes Jul 9, 2025
8c30b56
Add enabled property
Johennes Sep 9, 2025
eb06849
Add potential issue about unanticipated side effects of changes
Johennes Sep 9, 2025
f17d2ee
Clarify how to valide array properties
Johennes Oct 31, 2025
195199e
Use normative MUST
Johennes Nov 5, 2025
8d87122
Clarify that ignored invites can be unignored
Johennes Nov 5, 2025
64128c5
Remove dead link
Johennes Nov 5, 2025
e36e71d
Use normative wording
Johennes Nov 5, 2025
6fada45
Fix comma error / wordsmith
Johennes Nov 5, 2025
8ed8f33
Fix typo
Johennes Nov 5, 2025
31bbbb6
Add unstable_features flag
Johennes Nov 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions proposals/4155-invite-filtering.md
Copy link
Copy Markdown
Member

@turt2live turt2live Jun 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implementation requirements:

  • Client supporting the feature, and using it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since I believe all the gematik implementations will be closed source, I'll reference #3860 (comment) as an example for how cases like this were handled in the past. Thanks @clokep for digging it up.

Copy link
Copy Markdown
Contributor

@Half-Shot Half-Shot Mar 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

element-hq/synapse#18288 is a serverside implementation, albeit with #4155 (comment) "corrected"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

element-hq/element-web#29603 exposes the serverside settings in the client, but does no filtering of itself. @Johennes does this evoke any worries from you if the invite filtering is done server-side?

Copy link
Copy Markdown
Contributor Author

@Johennes Johennes Mar 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I this is fine and consistent with the proposal which allows but doesn't enforce the filtering on either the client or the server. Now that I think of it, we might need a capability so that the client knows when the server does not filter in which case the client needs to filter itself.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since I believe all the gematik implementations will be closed source,

Since it sounds like the biggest concern is whether a reasonable UI can be implemented, maybe a screenshot would suffice?

The gematik implementations are based on the initial version of this proposal where we had a base setting of either "allow all" or "block all" and exceptions for users and servers applied on top of it. It also didn't include any globbing.

FWIW, this is how my own health insurance has implemented it:

Main screen with radio buttons for the base setting at the top ("Alle" = "(Allow) everyone" / "Niemand" = "(Allow) noone"). Below that are two buttons to add exceptions for users ("Benutzer") and servers.

1

For adding user exceptions, they pop up a bottom sheet with a few options ("Teilnehmende hinzufügen"). The first two are for obtaining the MXID by searching gematik-specific directories. The third option scans the MXID from a QR code. And the last one lets you input it manually.

2 4

For server exceptions, the only option is to enter the server name manually.

5

Once entered, the exceptions show in a list at the bottom of the main screen.

6

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @Johennes, that's extremely helpful. I'm concerned, however, that it only (appears to) offer controls for blocked_users and blocked_servers (i.e, it does not expose allowed_users, ignored_users, allowed_servers or ignored_servers), and nor does it expose globbing.

It is the interplay between these settings that I think makes this proposal complicated to implement in a client, and my concern remains that we don't yet have a viable client implementation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[...] it only (appears to) offer controls for blocked_users and blocked_servers (i.e, it does not expose allowed_users, ignored_users, allowed_servers or ignored_servers), and nor does it expose globbing.

Yes, the gematik implementations are based on the initial version of this MSC which used the following scheme:

{
  "type": "m.invite_permission_config",
  "content": {
    "default": "allow | block",
    "user_exceptions": {
      "@someone:example.org": {},
      ...
    },
    "server_exceptions": {
      "example.org": {},
      ...
    }
  }
}

I think the only apparent UX problem this has is that when you flip default, your previous exceptions suddenly apply the other way around.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose a client can help manage that UX problem by clearing user_exceptions and server_exceptions at the same time as changing default? That doesn't feel too bad to me, tbh.

(I'm sorry you've been sent so far around the houses on this, @Johennes. For now I'm going to focus on landing something in the spec, and then we can come back here and see if we can figure out a plausible way forward.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries at all. 👍

Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
# MSC4155: Invite filtering

Matrix supports ignoring users via the eponymous [module] and the `m.ignored_user_list` account data
event. This is a nuclear option though and will suppress both invites and room events from the ignored
users. Additionally, the `m.ignored_user_list` event only allows for block-list configurations that ignore
specific users but doesn't have a mechanism to ignore entire servers. These shortcomings make the module
insufficient for use cases such as tightly locked down applications where ignoring needs to be the default
behaviour.

This proposal generalises the ignoring users [module] to allow filtering invites specifically. The scheme
outlined below was conceptually borrowed from the [gematik specification].


## Proposal

To allow users to configure which other users are allowed to invite them into rooms, a new account data
event `m.invite_permission_config` is introduced.

```json5
{
"type": "m.invite_permission_config",
"content": {
// User-level settings
"allowed_users": [ "@john:goodguys.org", ... ],
"ignored_users": [ ... ],
"blocked_users": [ ... ],
// Server-level settings
"allowed_servers": [ ... ],
"ignored_servers": [ ... ],
"blocked_servers": [ "*" ] // A feature of all the fields at this level, globs
}
}
```

All properties in `content` are optional arrays. A missing or `null` property MUST be treated like an
empty array. The array elements are [glob expressions] to be matched against user IDs and server names,
respectively.

When evaluating an invite against the contents of `m.invite_permission_config`, implementations MUST
apply the following order:

1. Verify the invite against each `*_users` setting:
1. If it matches `allowed_users`, stop processing and allow.
2. If it matches `ignored_users`, stop processing and ignore.
3. If it matches `blocked_users`, stop processing and block.
2. Verify the invite against each `*_servers` setting:
1. If it matches `allowed_servers`, stop processing and allow.
2. If it matches `ignored_servers`, stop processing and ignore.
3. If it matches `blocked_servers`, stop processing and block.
3. Otherwise, allow.
Copy link
Copy Markdown
Contributor

@Gnuxie Gnuxie Jun 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The allow semantic here is in the reverse order to server ACL, and the implication is that an allow rules overrule ban rules. This will cause problems later if an allow policy rule recommendation is created, because it will have different semantics based on context.

To be explicit, for it to be consistent with server ACL, the allow rule would have to be applied first and processing can only continue if there is a match.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it feels beneficial to follow the Server ACL order, with a slight change to the very last rule. Instead of "Otherwise, deny", we "Otherwise, allow" for backwards compatibility.

I think this means processing the rules as such:

  1. Verify the invite against each *_users setting:
    1. If it matches blocked_users, stop processing and block.
    2. If it matches ignored_users, stop processing and ignore.
    3. If it matches allowed_users, stop processing and allow.
  2. Verify the invite against each *_servers setting:
    1. If it matches blocked_servers, stop processing and block.
    2. If it matches ignored_servers, stop processing and ignore.
    3. If it matches allowed_servers, stop processing and allow.
  3. Otherwise, allow.

I don't think we want to check each in batches (all blocks, then all ignores, then all allows) specifically to ensure the user is able to allow-list certain senders from otherwise banned servers. For example: blocked_servers: [matrix.org], allowed_users: [@abuse:matrix.org].

Later, when we have more rules, we'd append to just before the "Otherwise, allow" rule.

Copy link
Copy Markdown
Contributor

@Gnuxie Gnuxie Jul 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider the following issue1.

  1. @alice:example.com is added to allowed_users.
  2. example.com becomes hostile.
  3. example.com is added to blocked_servers.
  4. @alice:example.com sends malicious invitations.

When example.com is added to blocked_servers, the entry for each user from that server in allowed_users becomes stale. There could probably be other scenarios where entries can become stale too.

A way to fix this could be to instead provide a *_users object for eachserver.

{
  "example.com": { "allowed_users": "@alice:example.com" }
}

You would then also need to be able to state whether all users from the server can send invitations, or specific users. So you would also need to introduce a "match all" rule for *_users.

Footnotes

  1. You can't avoid this problem by inverting the behavior described in the comment above (ie requiring that both checks for servers and users pass) because then you would end up in a situation where the allowed_servers entry for the user being added can become stale as the allowed_users entries are changed. To be explicit, adding @alice:example.com to allowed_users would require adding example.com to allowed_servers first.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I notice this thread hasn't been addressed. For my part, I'm not sold on the importance of the ordering here matching that in server ACLs, but we need to agree it one way or the other.

(Further, this has been in production on matrix.org for months, much to my irritation, so it's a bit late to start changing it now :/)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider the following issue1.

1. `@alice:example.com` is added to `allowed_users`.
2. `example.com` becomes hostile.
3. `example.com` is added to `blocked_servers`.
4. `@alice:example.com` sends malicious invitations.

When example.com is added to blocked_servers, the entry for each user from that server in allowed_users becomes stale.

That looks like an expected consequence of the decision to let user-level rules override server-level rules (#4155 (comment)). It is also called out under "Potential issues". No changes are needed here IMHO.


The semantics of "ignore" and "block" follow [MSC4283] which means ignoring hides the invite with no
feedback to the invite whereas blocking rejects (or refuses, in the case of servers) the invite.

When blocking an inviter, the server must respond to the following endpoints with an error:

- `PUT /_matrix/federation/(v1|v2)/invite/{roomId}/{eventId}`
- `POST /_matrix/client/v3/rooms/{roomId}/invite`
- `POST /_matrix/client/v3/createRoom` (checking the `invite` list)
- `PUT /_matrix/client/v3/rooms/{roomId}/state/m.room.member/{stateKey}` (for invite membership)

The error SHOULD be `M_INVITE_BLOCKED` with a HTTP 403 status code.

When ignoring an invite, these endpoints MUST handle an invite normally as if accepted. However, the server
MUST not include the invite down client synchronization endpoints such as `GET /_matrix/client/v3/sync` or
MSC4186's sliding sync endpoint. In addition, these invites MUST be ignored when calculating push notifications.

Otherwise, all other endpoints (such as `GET /_matrix/client/v3/rooms/{roomId}/state`) should work as before.

Servers are not expected to process these rules for appservice users when calculating events to send down
`PUT /_matrix/app/v1/transactions`. Appservices are not expected to be run directly by client users, and
should be able to handle their own spam prevention.

The semantics and order of evaluation enable a number of use cases, for instance:

```json5
// Invites from everyone
{
"type": "m.invite_permission_config",
"content": { }
}

// No invites at all
{
"type": "m.invite_permission_config",
"content": {
"blocked_servers": [ "*" ]
}
}

// Only invites from goodguys.org
{
"type": "m.invite_permission_config",
"content": {
"allowed_servers": [ "goodguys.org" ],
"blocked_servers": [ "*" ]
}
}

// Invites from everyone except badguys.org
{
"type": "m.invite_permission_config",
"content": {
"blocked_servers": [ "badguys.org" ]
}
}

// Only invites from goodguys.org except for @notactuallyguy:goodguys.org
{
"type": "m.invite_permission_config",
"content": {
"blocked_users": [ "@notactuallyguy:goodguys.org" ],
"allowed_servers": [ "goodguys.org" ]
"blocked_servers": [ "*" ]
}
}

// No invites from badguys.org unless it's @goodguy:badguys.org
{
"type": "m.invite_permission_config",
"content": {
"allowed_users": [ "@goodguy:badguys.org" ],
"blocked_servers": [ "badguys.org" ]
}
}

// Only invites from goodguys.org and don't provide feedback to reallybadguys.org
{
"type": "m.invite_permission_config",
"content": {
"allowed_servers": [ "goodguys.org" ],
"ignored_servers": [ "reallybadguys.org" ],
"blocked_servers": [ "*" ]
}
}
```

Servers MUST enforce `m.invite_permission_config` against incoming new invites. Additionally, Servers
SHOULD apply the config against existing pending invites as well.


## Potential issues

Enforcing the permission configuration exclusively on the server means users have no way to review
processed invites. This is desirable in most cases as a spam protection measure. It does mean, however,
that if the user has accidentally blocked a good actor and is informed about it through a different
communication channel, they'll have to update their permission configuration and request a re-invite.


## Alternatives

A comprehensive comparison of existing invite filtering proposals may be found in [MSC4192]. The
present proposal is functionally inferior to some of the alternatives outline there. It does, for
instance, not cover the change history of the permission config or sharing the config among different
users. The proposal is, however, a straightforward and easy to implement extension of the existing
`m.ignored_user_list` mechanism. See also [this comment] for further details. This proposal is additionally
extensible for further types of blocking in the future. For example, a future MSC could create definitions
and behaviours to block/ignore/allow invites from contacts, of a particular type (DM, space, etc),
to a particular room, or even with given keywords.


## Security considerations

None.


## Unstable prefix

Until this proposal is accepted into the spec, implementations should refer to `m.invite_permission_config`
and `m.invite_permission_config_enforced` as `org.matrix.msc4155.invite_permission_config` and
`org.matrix.msc4155.invite_permission_config_enforced`, respectively. Note that the [gematik specification],
which predates this MSC, uses an event type of `de.gematik.tim.account.permissionconfig.v1` and
a different event schema.

The error `M_INVITE_BLOCKED` should be `ORG.MATRIX.MSC4155.M_INVITE_BLOCKED` until this proposal is accepted into the spec.
## Dependencies

This proposal loosely depends on [MSC4283] for the semantics of "ignore" and "block".


[gematik specification]: https://github.com/gematik/api-ti-messenger/blob/9b9f21b87949e778de85dbbc19e25f53495871e2/src/schema/permissionConfig.json
[glob expressions]: https://spec.matrix.org/v1.14/appendices/#glob-style-matching
[MSC4192]: https://github.com/matrix-org/matrix-spec-proposals/pull/4192
[MSC4283]: https://github.com/matrix-org/matrix-spec-proposals/pull/4283
[module]: https://spec.matrix.org/v1.10/client-server-api/#ignoring-users
[this comment]: https://github.com/matrix-org/matrix-spec-proposals/pull/4192#discussion_r2025188127