VYPR
Moderate severityNVD Advisory· Published Oct 10, 2023· Updated Feb 13, 2025

matrix-synapse vulnerable to denial of service due to malicious server ACL events

CVE-2023-45129

Description

Synapse is an open-source Matrix homeserver written and maintained by the Matrix.org Foundation. Prior to version 1.94.0, a malicious server ACL event can impact performance temporarily or permanently leading to a persistent denial of service. Homeservers running on a closed federation (which presumably do not need to use server ACLs) are not affected. Server administrators are advised to upgrade to Synapse 1.94.0 or later. As a workaround, rooms with malicious server ACL events can be purged and blocked using the admin API.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Malicious server ACL events can cause a persistent denial of service in Synapse prior to 1.94.0 by degrading performance indefinitely.

Vulnerability

Overview CVE-2023-45129 is a denial-of-service vulnerability in Synapse, the open-source Matrix homeserver, affecting versions prior to 1.94.0. The issue lies in how Synapse processes m.room.server_acl events — a malicious ACL event can trigger costly, repeated computation, degrading server performance either temporarily or permanently, leading to a persistent denial of service. Homeservers running on a closed federation that do not use server ACLs are not affected [1][2].

Attack

Vector and Exploitation An attacker with the ability to send a server ACL event in a room (e.g., a room moderator or admin) can craft an event with carefully constructed rules (e.g., complex deny/allow lists) that triggers inefficient processing every time the ACL is evaluated for incoming federation requests. Since the evaluation occurs repeatedly without caching, the resource drain can be sustained, impacting the targeted server's ability to function normally. The fix, introduced in commit f84da3c, adds a cache around server ACL checking and refactors the code to avoid redundant operations, preventing the performance degradation [4].

Impact

Successful exploitation can result in a denial of service, making the homeserver unresponsive or severely degraded for all users on that server. The impact is persistent because the malicious ACL event remains in the room state until removed, continuously consuming server resources. There is no data breach or remote code execution; the vulnerability exclusively affects availability.

Mitigation

Server administrators should upgrade to Synapse 1.94.0 or later to completely remediate the issue. As a workaround, rooms containing malicious server ACL events can be purged and blocked via the admin API [2][3]. No evidence of active exploitation in the wild has been reported as of the publication date.

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
matrix-synapsePyPI
< 1.94.01.94.0

Affected products

2

Patches

1
f84da3c32ec7

Add a cache around server ACL checking (#16360)

https://github.com/matrix-org/synapsePatrick ClokeSep 26, 2023via ghsa
11 files changed · +235 85
  • changelog.d/16360.misc+1 0 added
    @@ -0,0 +1 @@
    +Cache server ACL checking.
    
  • rust/src/acl/mod.rs+102 0 added
    @@ -0,0 +1,102 @@
    +// Copyright 2023 The Matrix.org Foundation C.I.C.
    +//
    +// 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.
    +
    +//! An implementation of Matrix server ACL rules.
    +
    +use std::net::Ipv4Addr;
    +use std::str::FromStr;
    +
    +use anyhow::Error;
    +use pyo3::prelude::*;
    +use regex::Regex;
    +
    +use crate::push::utils::{glob_to_regex, GlobMatchType};
    +
    +/// Called when registering modules with python.
    +pub fn register_module(py: Python<'_>, m: &PyModule) -> PyResult<()> {
    +    let child_module = PyModule::new(py, "acl")?;
    +    child_module.add_class::<ServerAclEvaluator>()?;
    +
    +    m.add_submodule(child_module)?;
    +
    +    // We need to manually add the module to sys.modules to make `from
    +    // synapse.synapse_rust import acl` work.
    +    py.import("sys")?
    +        .getattr("modules")?
    +        .set_item("synapse.synapse_rust.acl", child_module)?;
    +
    +    Ok(())
    +}
    +
    +#[derive(Debug, Clone)]
    +#[pyclass(frozen)]
    +pub struct ServerAclEvaluator {
    +    allow_ip_literals: bool,
    +    allow: Vec<Regex>,
    +    deny: Vec<Regex>,
    +}
    +
    +#[pymethods]
    +impl ServerAclEvaluator {
    +    #[new]
    +    pub fn py_new(
    +        allow_ip_literals: bool,
    +        allow: Vec<&str>,
    +        deny: Vec<&str>,
    +    ) -> Result<Self, Error> {
    +        let allow = allow
    +            .iter()
    +            .map(|s| glob_to_regex(s, GlobMatchType::Whole))
    +            .collect::<Result<_, _>>()?;
    +        let deny = deny
    +            .iter()
    +            .map(|s| glob_to_regex(s, GlobMatchType::Whole))
    +            .collect::<Result<_, _>>()?;
    +
    +        Ok(ServerAclEvaluator {
    +            allow_ip_literals,
    +            allow,
    +            deny,
    +        })
    +    }
    +
    +    pub fn server_matches_acl_event(&self, server_name: &str) -> bool {
    +        // first of all, check if literal IPs are blocked, and if so, whether the
    +        // server name is a literal IP
    +        if !self.allow_ip_literals {
    +            // check for ipv6 literals. These start with '['.
    +            if server_name.starts_with('[') {
    +                return false;
    +            }
    +
    +            // check for ipv4 literals. We can just lift the routine from std::net.
    +            if Ipv4Addr::from_str(server_name).is_ok() {
    +                return false;
    +            }
    +        }
    +
    +        // next, check the deny list
    +        if self.deny.iter().any(|e| e.is_match(server_name)) {
    +            return false;
    +        }
    +
    +        // then the allow list.
    +        if self.allow.iter().any(|e| e.is_match(server_name)) {
    +            return true;
    +        }
    +
    +        // everything else should be rejected.
    +        false
    +    }
    +}
    
  • rust/src/lib.rs+2 0 modified
    @@ -2,6 +2,7 @@ use lazy_static::lazy_static;
     use pyo3::prelude::*;
     use pyo3_log::ResetHandle;
     
    +pub mod acl;
     pub mod push;
     
     lazy_static! {
    @@ -38,6 +39,7 @@ fn synapse_rust(py: Python<'_>, m: &PyModule) -> PyResult<()> {
         m.add_function(wrap_pyfunction!(get_rust_file_digest, m)?)?;
         m.add_function(wrap_pyfunction!(reset_logging_config, m)?)?;
     
    +    acl::register_module(py, m)?;
         push::register_module(py, m)?;
     
         Ok(())
    
  • stubs/synapse/synapse_rust/acl.pyi+21 0 added
    @@ -0,0 +1,21 @@
    +# Copyright 2023 The Matrix.org Foundation C.I.C.
    +#
    +# 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.
    +
    +from typing import List
    +
    +class ServerAclEvaluator:
    +    def __init__(
    +        self, allow_ip_literals: bool, allow: List[str], deny: List[str]
    +    ) -> None: ...
    +    def server_matches_acl_event(self, server_name: str) -> bool: ...
    
  • synapse/events/validator.py+5 2 modified
    @@ -39,9 +39,9 @@
         CANONICALJSON_MIN_INT,
         validate_canonicaljson,
     )
    -from synapse.federation.federation_server import server_matches_acl_event
     from synapse.http.servlet import validate_json_object
     from synapse.rest.models import RequestBodyModel
    +from synapse.storage.controllers.state import server_acl_evaluator_from_event
     from synapse.types import EventID, JsonDict, RoomID, StrCollection, UserID
     
     
    @@ -106,7 +106,10 @@ def validate_new(self, event: EventBase, config: HomeServerConfig) -> None:
                 self._validate_retention(event)
     
             elif event.type == EventTypes.ServerACL:
    -            if not server_matches_acl_event(config.server.server_name, event):
    +            server_acl_evaluator = server_acl_evaluator_from_event(event)
    +            if not server_acl_evaluator.server_matches_acl_event(
    +                config.server.server_name
    +            ):
                     raise SynapseError(
                         400, "Can't create an ACL event that denies the local server"
                     )
    
  • synapse/federation/federation_server.py+6 70 modified
    @@ -29,10 +29,8 @@
         Union,
     )
     
    -from matrix_common.regex import glob_to_regex
     from prometheus_client import Counter, Gauge, Histogram
     
    -from twisted.internet.abstract import isIPAddress
     from twisted.python import failure
     
     from synapse.api.constants import (
    @@ -1324,75 +1322,13 @@ async def check_server_matches_acl(self, server_name: str, room_id: str) -> None
             Raises:
                 AuthError if the server does not match the ACL
             """
    -        acl_event = await self._storage_controllers.state.get_current_state_event(
    -            room_id, EventTypes.ServerACL, ""
    +        server_acl_evaluator = (
    +            await self._storage_controllers.state.get_server_acl_for_room(room_id)
             )
    -        if not acl_event or server_matches_acl_event(server_name, acl_event):
    -            return
    -
    -        raise AuthError(code=403, msg="Server is banned from room")
    -
    -
    -def server_matches_acl_event(server_name: str, acl_event: EventBase) -> bool:
    -    """Check if the given server is allowed by the ACL event
    -
    -    Args:
    -        server_name: name of server, without any port part
    -        acl_event: m.room.server_acl event
    -
    -    Returns:
    -        True if this server is allowed by the ACLs
    -    """
    -    logger.debug("Checking %s against acl %s", server_name, acl_event.content)
    -
    -    # first of all, check if literal IPs are blocked, and if so, whether the
    -    # server name is a literal IP
    -    allow_ip_literals = acl_event.content.get("allow_ip_literals", True)
    -    if not isinstance(allow_ip_literals, bool):
    -        logger.warning("Ignoring non-bool allow_ip_literals flag")
    -        allow_ip_literals = True
    -    if not allow_ip_literals:
    -        # check for ipv6 literals. These start with '['.
    -        if server_name[0] == "[":
    -            return False
    -
    -        # check for ipv4 literals. We can just lift the routine from twisted.
    -        if isIPAddress(server_name):
    -            return False
    -
    -    # next,  check the deny list
    -    deny = acl_event.content.get("deny", [])
    -    if not isinstance(deny, (list, tuple)):
    -        logger.warning("Ignoring non-list deny ACL %s", deny)
    -        deny = []
    -    for e in deny:
    -        if _acl_entry_matches(server_name, e):
    -            # logger.info("%s matched deny rule %s", server_name, e)
    -            return False
    -
    -    # then the allow list.
    -    allow = acl_event.content.get("allow", [])
    -    if not isinstance(allow, (list, tuple)):
    -        logger.warning("Ignoring non-list allow ACL %s", allow)
    -        allow = []
    -    for e in allow:
    -        if _acl_entry_matches(server_name, e):
    -            # logger.info("%s matched allow rule %s", server_name, e)
    -            return True
    -
    -    # everything else should be rejected.
    -    # logger.info("%s fell through", server_name)
    -    return False
    -
    -
    -def _acl_entry_matches(server_name: str, acl_entry: Any) -> bool:
    -    if not isinstance(acl_entry, str):
    -        logger.warning(
    -            "Ignoring non-str ACL entry '%s' (is %s)", acl_entry, type(acl_entry)
    -        )
    -        return False
    -    regex = glob_to_regex(acl_entry)
    -    return bool(regex.match(server_name))
    +        if server_acl_evaluator and not server_acl_evaluator.server_matches_acl_event(
    +            server_name
    +        ):
    +            raise AuthError(code=403, msg="Server is banned from room")
     
     
     class FederationHandlerRegistry:
    
  • synapse/handlers/federation_event.py+6 0 modified
    @@ -2342,6 +2342,12 @@ async def _notify_persisted_event(
                 # TODO retrieve the previous state, and exclude join -> join transitions
                 self._notifier.notify_user_joined_room(event.event_id, event.room_id)
     
    +        # If this is a server ACL event, clear the cache in the storage controller.
    +        if event.type == EventTypes.ServerACL:
    +            self._state_storage_controller.get_server_acl_for_room.invalidate(
    +                (event.room_id,)
    +            )
    +
         def _sanity_check_event(self, ev: EventBase) -> None:
             """
             Do some early sanity checks of a received event
    
  • synapse/handlers/message.py+5 0 modified
    @@ -1730,6 +1730,11 @@ async def persist_and_notify_client_events(
                             event.event_id, event.room_id
                         )
     
    +            if event.type == EventTypes.ServerACL:
    +                self._storage_controllers.state.get_server_acl_for_room.invalidate(
    +                    (event.room_id,)
    +                )
    +
                 await self._maybe_kick_guest_users(event, context)
     
                 if event.type == EventTypes.CanonicalAlias:
    
  • synapse/replication/tcp/client.py+6 0 modified
    @@ -205,6 +205,12 @@ async def on_rdata(
                         self.notifier.notify_user_joined_room(
                             row.data.event_id, row.data.room_id
                         )
    +
    +                # If this is a server ACL event, clear the cache in the storage controller.
    +                if row.data.type == EventTypes.ServerACL:
    +                    self._state_storage_controller.get_server_acl_for_room.invalidate(
    +                        (row.data.room_id,)
    +                    )
             elif stream_name == UnPartialStatedRoomStream.NAME:
                 for row in rows:
                     assert isinstance(row, UnPartialStatedRoomStreamRow)
    
  • synapse/storage/controllers/state.py+59 0 modified
    @@ -37,6 +37,7 @@
         PartialCurrentStateTracker,
         PartialStateEventsTracker,
     )
    +from synapse.synapse_rust.acl import ServerAclEvaluator
     from synapse.types import MutableStateMap, StateMap, get_domain_from_id
     from synapse.types.state import StateFilter
     from synapse.util.async_helpers import Linearizer
    @@ -501,6 +502,31 @@ async def get_canonical_alias_for_room(self, room_id: str) -> Optional[str]:
     
             return event.content.get("alias")
     
    +    @cached()
    +    async def get_server_acl_for_room(
    +        self, room_id: str
    +    ) -> Optional[ServerAclEvaluator]:
    +        """Get the server ACL evaluator for room, if any
    +
    +        This does up-front parsing of the content to ignore bad data and pre-compile
    +        regular expressions.
    +
    +        Args:
    +            room_id: The room ID
    +
    +        Returns:
    +            The server ACL evaluator, if any
    +        """
    +
    +        acl_event = await self.get_current_state_event(
    +            room_id, EventTypes.ServerACL, ""
    +        )
    +
    +        if not acl_event:
    +            return None
    +
    +        return server_acl_evaluator_from_event(acl_event)
    +
         @trace
         @tag_args
         async def get_current_state_deltas(
    @@ -760,3 +786,36 @@ async def _get_joined_hosts(
                     cache.state_group = object()
     
             return frozenset(cache.hosts_to_joined_users)
    +
    +
    +def server_acl_evaluator_from_event(acl_event: EventBase) -> "ServerAclEvaluator":
    +    """
    +    Create a ServerAclEvaluator from a m.room.server_acl event's content.
    +
    +    This does up-front parsing of the content to ignore bad data. It then creates
    +    the ServerAclEvaluator which will pre-compile regular expressions from the globs.
    +    """
    +
    +    # first of all, parse if literal IPs are blocked.
    +    allow_ip_literals = acl_event.content.get("allow_ip_literals", True)
    +    if not isinstance(allow_ip_literals, bool):
    +        logger.warning("Ignoring non-bool allow_ip_literals flag")
    +        allow_ip_literals = True
    +
    +    # next, parse the deny list by ignoring any non-strings.
    +    deny = acl_event.content.get("deny", [])
    +    if not isinstance(deny, (list, tuple)):
    +        logger.warning("Ignoring non-list deny ACL %s", deny)
    +        deny = []
    +    else:
    +        deny = [s for s in deny if isinstance(s, str)]
    +
    +    # then the allow list.
    +    allow = acl_event.content.get("allow", [])
    +    if not isinstance(allow, (list, tuple)):
    +        logger.warning("Ignoring non-list allow ACL %s", allow)
    +        allow = []
    +    else:
    +        allow = [s for s in allow if isinstance(s, str)]
    +
    +    return ServerAclEvaluator(allow_ip_literals, allow, deny)
    
  • tests/federation/test_federation_server.py+22 13 modified
    @@ -22,10 +22,10 @@
     from synapse.api.room_versions import KNOWN_ROOM_VERSIONS
     from synapse.config.server import DEFAULT_ROOM_VERSION
     from synapse.events import EventBase, make_event_from_dict
    -from synapse.federation.federation_server import server_matches_acl_event
     from synapse.rest import admin
     from synapse.rest.client import login, room
     from synapse.server import HomeServer
    +from synapse.storage.controllers.state import server_acl_evaluator_from_event
     from synapse.types import JsonDict
     from synapse.util import Clock
     
    @@ -67,37 +67,46 @@ def test_blocked_server(self) -> None:
             e = _create_acl_event({"allow": ["*"], "deny": ["evil.com"]})
             logging.info("ACL event: %s", e.content)
     
    -        self.assertFalse(server_matches_acl_event("evil.com", e))
    -        self.assertFalse(server_matches_acl_event("EVIL.COM", e))
    +        server_acl_evalutor = server_acl_evaluator_from_event(e)
     
    -        self.assertTrue(server_matches_acl_event("evil.com.au", e))
    -        self.assertTrue(server_matches_acl_event("honestly.not.evil.com", e))
    +        self.assertFalse(server_acl_evalutor.server_matches_acl_event("evil.com"))
    +        self.assertFalse(server_acl_evalutor.server_matches_acl_event("EVIL.COM"))
    +
    +        self.assertTrue(server_acl_evalutor.server_matches_acl_event("evil.com.au"))
    +        self.assertTrue(
    +            server_acl_evalutor.server_matches_acl_event("honestly.not.evil.com")
    +        )
     
         def test_block_ip_literals(self) -> None:
             e = _create_acl_event({"allow_ip_literals": False, "allow": ["*"]})
             logging.info("ACL event: %s", e.content)
     
    -        self.assertFalse(server_matches_acl_event("1.2.3.4", e))
    -        self.assertTrue(server_matches_acl_event("1a.2.3.4", e))
    -        self.assertFalse(server_matches_acl_event("[1:2::]", e))
    -        self.assertTrue(server_matches_acl_event("1:2:3:4", e))
    +        server_acl_evalutor = server_acl_evaluator_from_event(e)
    +
    +        self.assertFalse(server_acl_evalutor.server_matches_acl_event("1.2.3.4"))
    +        self.assertTrue(server_acl_evalutor.server_matches_acl_event("1a.2.3.4"))
    +        self.assertFalse(server_acl_evalutor.server_matches_acl_event("[1:2::]"))
    +        self.assertTrue(server_acl_evalutor.server_matches_acl_event("1:2:3:4"))
     
         def test_wildcard_matching(self) -> None:
             e = _create_acl_event({"allow": ["good*.com"]})
    +
    +        server_acl_evalutor = server_acl_evaluator_from_event(e)
    +
             self.assertTrue(
    -            server_matches_acl_event("good.com", e),
    +            server_acl_evalutor.server_matches_acl_event("good.com"),
                 "* matches 0 characters",
             )
             self.assertTrue(
    -            server_matches_acl_event("GOOD.COM", e),
    +            server_acl_evalutor.server_matches_acl_event("GOOD.COM"),
                 "pattern is case-insensitive",
             )
             self.assertTrue(
    -            server_matches_acl_event("good.aa.com", e),
    +            server_acl_evalutor.server_matches_acl_event("good.aa.com"),
                 "* matches several characters, including '.'",
             )
             self.assertFalse(
    -            server_matches_acl_event("ishgood.com", e),
    +            server_acl_evalutor.server_matches_acl_event("ishgood.com"),
                 "pattern does not allow prefixes",
             )
     
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

14

News mentions

0

No linked articles in our index yet.