fix(matrix,mattermost): invite auth check + API path traversal guard

Two platform-security hardenings:

- Matrix: _on_invite now checks the inviter against the existing
  allow-list (_allowed_user_ids / GATEWAY_ALLOW_ALL_USERS) before
  auto-joining. Without this any federated Matrix user could invite
  the bot into arbitrary rooms, exposing its presence and metadata.
  The message and reaction paths already enforce this allow-list; the
  invite path bypassed it.

- Mattermost: _api_get / _api_post / _api_put reject any path
  containing '..'. WebSocket-event values (channel_id, post_id,
  file_id) are interpolated directly into API paths, so a malicious or
  compromised server could craft traversal payloads to make the bot
  issue authenticated requests to arbitrary endpoints with its bearer
  token.

The configurable-E2EE-passphrase change from the original PR is dropped:
the matrix adapter was rewritten onto mautrix and the passphrase-protected
key-export file no longer exists.
This commit is contained in:
aaronagent 2026-06-28 19:18:32 -07:00 committed by Teknium
parent 9cf9d3a28f
commit d836b2bac4
2 changed files with 28 additions and 0 deletions

View file

@ -2892,6 +2892,25 @@ class MatrixAdapter(BasePlatformAdapter):
is_direct = bool(getattr(content, "is_direct", False))
inviter = str(getattr(event, "sender", ""))
# Only auto-join when the inviter is authorized. Without this, any
# federated Matrix user could invite the bot into arbitrary rooms,
# exposing its presence and metadata. Mirrors the allow-list gate
# used on the message/reaction paths.
allow_all = os.getenv("GATEWAY_ALLOW_ALL_USERS", "").lower() in {
"true",
"1",
"yes",
}
if not allow_all and not (
self._allowed_user_ids and inviter in self._allowed_user_ids
):
logger.warning(
"Matrix: rejecting invite to %s from unauthorized user %s",
room_id,
inviter,
)
return
logger.info(
"Matrix: invited to %s — joining (is_direct=%s)",
room_id,

View file

@ -117,6 +117,9 @@ class MattermostAdapter(BasePlatformAdapter):
async def _api_get(self, path: str) -> Dict[str, Any]:
"""GET /api/v4/{path}."""
import aiohttp
if ".." in path:
logger.error("MM API path traversal blocked: %s", path)
return {}
url = f"{self._base_url}/api/v4/{path.lstrip('/')}"
try:
async with self._session.get(url, headers=self._headers(), timeout=aiohttp.ClientTimeout(total=30)) as resp:
@ -134,6 +137,9 @@ class MattermostAdapter(BasePlatformAdapter):
) -> Dict[str, Any]:
"""POST /api/v4/{path} with JSON body."""
import aiohttp
if ".." in path:
logger.error("MM API path traversal blocked: %s", path)
return {}
url = f"{self._base_url}/api/v4/{path.lstrip('/')}"
self._last_post_status = None
self._last_post_error = ""
@ -213,6 +219,9 @@ class MattermostAdapter(BasePlatformAdapter):
) -> Dict[str, Any]:
"""PUT /api/v4/{path} with JSON body."""
import aiohttp
if ".." in path:
logger.error("MM API path traversal blocked: %s", path)
return {}
url = f"{self._base_url}/api/v4/{path.lstrip('/')}"
try:
async with self._session.put(