fixes Yakifo/amqtt#187 : explicitly declaring auth plugins as an empty list (entry points configuration style) or not including a BaseAuthPlugin in loaded plugins, still allowed clients to connect to broker. security risk as explicit omission (former) or inadvertent omission (latter) might allow anonymous connections

pull/244/head
Andrew Mirsky 2025-06-30 14:08:01 -04:00
rodzic 4b99a17c8e
commit ee7250c720
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: A98E67635CDF2C39
8 zmienionych plików z 87 dodań i 10 usunięć

Wyświetl plik

@ -697,7 +697,7 @@ class Broker:
"""
returns = await self.plugins_manager.map_plugin_auth(session=session)
results = [ result for _, result in returns.items() if result is not None]
results = [ result for _, result in returns.items() if result is not None] if returns else []
if len(results) < 1:
self.logger.debug("Authentication failed: no plugin responded with a boolean")
return False

Wyświetl plik

@ -119,9 +119,9 @@ class PluginManager(Generic[C]):
auth_filter_list = []
topic_filter_list = []
if self.app_context.config and "auth" in self.app_context.config:
auth_filter_list = self.app_context.config["auth"].get("plugins", [])
auth_filter_list = self.app_context.config["auth"].get("plugins", None)
if self.app_context.config and "topic-check" in self.app_context.config:
topic_filter_list = self.app_context.config["topic-check"].get("plugins", [])
topic_filter_list = self.app_context.config["topic-check"].get("plugins", None)
ep: EntryPoints | list[EntryPoint] = []
if hasattr(entry_points(), "select"):
@ -133,10 +133,12 @@ class PluginManager(Generic[C]):
ep_plugin = self._load_ep_plugin(item)
if ep_plugin is not None:
self._plugins.append(ep_plugin.object)
if ((not auth_filter_list or ep_plugin.name in auth_filter_list)
# maintain legacy behavior that if there is no list, use all auth plugins
if ((auth_filter_list is None or ep_plugin.name in auth_filter_list)
and hasattr(ep_plugin.object, "authenticate")):
self._auth_plugins.append(ep_plugin.object)
if ((not topic_filter_list or ep_plugin.name in topic_filter_list)
# maintain legacy behavior that if there is no list, use all topic plugins
if ((topic_filter_list is None or ep_plugin.name in topic_filter_list)
and hasattr(ep_plugin.object, "topic_filtering")):
self._topic_plugins.append(ep_plugin.object)
self.logger.debug(f" Plugin {item.name} ready")

Wyświetl plik

@ -8,6 +8,8 @@ import urllib.request
import pytest
from amqtt.broker import Broker
from amqtt.contexts import BaseContext
from amqtt.plugins.base import BasePlugin
log = logging.getLogger(__name__)
@ -22,7 +24,7 @@ test_config = {
"sys_interval": 0,
"auth": {
"allow-anonymous": True,
},
}
}
@ -49,12 +51,15 @@ test_config_acl: dict[str, int | dict[str, Any]] = {
@pytest.fixture
def mock_plugin_manager():
with unittest.mock.patch("amqtt.broker.PluginManager") as plugin_manager:
with (unittest.mock.patch("amqtt.broker.PluginManager") as plugin_manager):
plugin_manager_instance = plugin_manager.return_value
# disable topic filtering when using the mock manager
plugin_manager_instance.is_topic_filtering_enabled.return_value = False
# allow any connection when using the mock manager
plugin_manager_instance.map_plugin_auth = unittest.mock.AsyncMock(return_value={ BasePlugin(BaseContext()): True })
yield plugin_manager

Wyświetl plik

@ -83,6 +83,7 @@ listeners:
type: tcp
bind: 0.0.0.0:1883
plugins:
- amqtt.plugins.authentication.AnonymousAuthPlugin
- tests.plugins.mocks.TestAllowTopicPlugin:
"""
@ -93,6 +94,7 @@ listeners:
type: tcp
bind: 0.0.0.0:1883
plugins:
- amqtt.plugins.authentication.AnonymousAuthPlugin
- tests.plugins.mocks.TestBlockTopicPlugin:
"""

Wyświetl plik

@ -31,7 +31,7 @@ class EventTestPlugin(BaseAuthPlugin, BaseTopicPlugin):
async def authenticate(self, *, session: Session) -> bool | None:
self.test_auth_flag = True
return None
return True
async def topic_filtering(
self, *, session: Session | None = None, topic: str | None = None, action: Action | None = None

Wyświetl plik

@ -145,6 +145,7 @@ async def test_all_plugin_events():
},
'sys_interval': 1,
'plugins':{
'amqtt.plugins.authentication.AnonymousAuthPlugin': {},
'tests.plugins.test_plugins.AllEventsPlugin': {}
}
}

Wyświetl plik

@ -80,7 +80,7 @@ async def test_broker_sys_plugin_deprecated_config() -> None:
case 'tests.mock_plugins':
return [
EntryPoint(name='broker_sys', group='tests.mock_plugins', value='amqtt.plugins.sys.broker:BrokerSysPlugin'),
EntryPoint(name='auth_anonymous', group='test.mock_plugins', value='amqtt.plugins.sys.auth:AuthAnonymousPlugin'),
EntryPoint(name='auth_anonymous', group='test.mock_plugins', value='amqtt.plugins.authentication:AnonymousAuthPlugin'),
]
case _:
return list()

Wyświetl plik

@ -76,7 +76,8 @@ async def test_start_stop(broker, mock_plugin_manager):
@pytest.mark.asyncio
async def test_client_connect(broker, mock_plugin_manager):
client = MQTTClient()
client = MQTTClient(config={'auto_reconnect':False})
ret = await client.connect("mqtt://127.0.0.1/")
assert ret == 0
assert client.session is not None
@ -739,3 +740,69 @@ async def test_broker_socket_open_close(broker):
s.send(static_connect_packet)
await asyncio.sleep(0.1)
s.close()
legacy_config_empty_auth_plugin_list = {
"listeners": {
"default": {"type": "tcp", "bind": "127.0.0.1:1883", "max_connections": 10},
},
'sys_interval': 0,
'auth':{
'plugins':[] # explicitly declare no auth plugins
}
}
class_path_config_no_auth = {
"listeners": {
"default": {"type": "tcp", "bind": "127.0.0.1:1883", "max_connections": 10},
},
'plugins':{
'tests.plugins.test_plugins.AllEventsPlugin': {}
}
}
@pytest.mark.parametrize("test_config", [
legacy_config_empty_auth_plugin_list,
class_path_config_no_auth,
])
@pytest.mark.asyncio
async def test_broker_without_auth_plugin(test_config):
broker = Broker(config=test_config)
await broker.start()
await asyncio.sleep(2)
# make sure all expected events get triggered
with pytest.raises(ConnectError):
mqtt_client = MQTTClient(config={'auto_reconnect': False})
await mqtt_client.connect()
await broker.shutdown()
legacy_config_with_absent_auth_plugin_filter = {
"listeners": {
"default": {"type": "tcp", "bind": "127.0.0.1:1883", "max_connections": 10},
},
'sys_interval': 0,
'auth':{
'allow-anonymous': True
}
}
@pytest.mark.asyncio
async def test_broker_with_absent_auth_plugin_filter():
# maintain legacy behavior that if a config is missing the 'auth' > 'plugins' filter, all plugins are active
broker = Broker(config=legacy_config_with_absent_auth_plugin_filter)
await broker.start()
await asyncio.sleep(2)
mqtt_client = MQTTClient(config={'auto_reconnect': False})
await mqtt_client.connect()
await broker.shutdown()