feat: harden notification stack and switch logging selectors to icon grid
Notifications: - Add shared http_base, redact, and SSRF hardening modules - Refactor dispatcher, queue, receiver and per-provider clients (telegram, discord, email, matrix, ntfy, slack, webhook) to use the shared base, with bounded queue and redacted error logs - Tests for ssrf, redact, http_base, queue bounds, dispatcher aggregation, telegram media partition, email and matrix clients Frontend: - Settings: log level / log format selectors now use IconGridSelect with per-option icons and i18n descriptions - Minor providers page and entity-cache store updates Tooling: - Document code-review-graph MCP usage in CLAUDE.md - Ignore .code-review-graph/, register .mcp.json
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
"""Matrix client validation: room_id format and quoting."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import aiohttp
|
||||
import pytest
|
||||
from aioresponses import aioresponses
|
||||
|
||||
from notify_bridge_core.notifications.matrix.client import MatrixClient
|
||||
|
||||
|
||||
HOMESERVER = "https://matrix.example.com"
|
||||
TOKEN = "secret-bearer-token-1234567890"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rejects_path_injection_room_id() -> None:
|
||||
async with aiohttp.ClientSession() as sess:
|
||||
client = MatrixClient(sess, HOMESERVER, TOKEN)
|
||||
result = await client.send_message("!abc:host/../../etc/passwd", "hi")
|
||||
assert result["success"] is False
|
||||
assert "room_id" in result["error"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rejects_empty_room_id() -> None:
|
||||
async with aiohttp.ClientSession() as sess:
|
||||
client = MatrixClient(sess, HOMESERVER, TOKEN)
|
||||
result = await client.send_message("", "hi")
|
||||
assert result["success"] is False
|
||||
assert "room_id" in result["error"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rejects_unicode_control_chars_in_room_id() -> None:
|
||||
async with aiohttp.ClientSession() as sess:
|
||||
client = MatrixClient(sess, HOMESERVER, TOKEN)
|
||||
result = await client.send_message("!abc\x00:host", "hi")
|
||||
assert result["success"] is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_url_encodes_room_id_special_chars() -> None:
|
||||
"""``!`` and ``:`` must reach the server URL-encoded."""
|
||||
captured: list[str] = []
|
||||
|
||||
with aioresponses() as mocked:
|
||||
# Match any PUT under the rooms path; capture the URL we got.
|
||||
mocked.put(
|
||||
"https://matrix.example.com/_matrix/client/v3/rooms/%21abc%3Ahost.example/send/m.room.message",
|
||||
status=200, body='{}', repeat=True,
|
||||
)
|
||||
# aioresponses doesn't expose URL templates well, so use a regex mock.
|
||||
import re
|
||||
mocked.put(
|
||||
re.compile(r"https://matrix\.example\.com/_matrix/client/v3/rooms/[^/]+/send/m\.room\.message/.*"),
|
||||
status=200, body='{}', repeat=True,
|
||||
)
|
||||
|
||||
async with aiohttp.ClientSession() as sess:
|
||||
client = MatrixClient(sess, HOMESERVER, TOKEN)
|
||||
result = await client.send_message("!abc:host.example", "hi")
|
||||
|
||||
assert result["success"] is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_redacts_bearer_in_error() -> None:
|
||||
"""A 4xx response body must not echo the Authorization Bearer back to caller."""
|
||||
import re
|
||||
with aioresponses() as mocked:
|
||||
mocked.put(
|
||||
re.compile(r".*"),
|
||||
status=403,
|
||||
body='{"errcode": "M_FORBIDDEN", "Authorization": "Bearer ' + TOKEN + '"}',
|
||||
repeat=True,
|
||||
)
|
||||
|
||||
async with aiohttp.ClientSession() as sess:
|
||||
client = MatrixClient(sess, HOMESERVER, TOKEN)
|
||||
result = await client.send_message("!abc:host.example", "hi")
|
||||
|
||||
assert result["success"] is False
|
||||
assert TOKEN not in result["error"]
|
||||
Reference in New Issue
Block a user