fix(security): require source CIDR allowlisting for public msgraph webhook binds

This commit is contained in:
Dusk1e 2026-05-25 18:40:36 +03:00 committed by Teknium
parent 986abb3cf7
commit 43abc51f66
4 changed files with 84 additions and 11 deletions

View file

@ -11,6 +11,7 @@ from gateway.platforms.msgraph_webhook import AIOHTTP_AVAILABLE, MSGraphWebhookA
def _make_adapter(**extra_overrides) -> MSGraphWebhookAdapter:
extra = {
"host": "127.0.0.1",
"client_state": "expected-client-state",
"accepted_resources": ["communications/onlineMeetings"],
}
@ -80,6 +81,27 @@ class TestMSGraphValidationHandshake:
# is_connected is a @property on the base adapter, not a method.
assert adapter.is_connected is False
@pytest.mark.anyio
async def test_connect_requires_source_allowlist_on_public_bind(self):
if not AIOHTTP_AVAILABLE:
pytest.skip("aiohttp not installed")
adapter = _make_adapter(host="0.0.0.0", port=0, allowed_source_cidrs=[])
connected = await adapter.connect()
assert connected is False
assert adapter.is_connected is False
@pytest.mark.anyio
async def test_connect_allows_loopback_without_source_allowlist(self):
if not AIOHTTP_AVAILABLE:
pytest.skip("aiohttp not installed")
adapter = _make_adapter(host="127.0.0.1", port=0, allowed_source_cidrs=[])
try:
connected = await adapter.connect()
assert connected is True
assert adapter.is_connected is True
finally:
await adapter.disconnect()
@pytest.mark.anyio
async def test_validation_token_echo_on_get(self):
adapter = _make_adapter()
@ -381,9 +403,9 @@ class TestMSGraphNotifications:
class TestMSGraphSourceIPAllowlist:
@pytest.mark.anyio
async def test_disabled_by_default_allows_all(self):
"""Empty allowlist preserves pre-existing behavior (dev tunnels, localhost)."""
adapter = _make_adapter() # no allowed_source_cidrs set
async def test_public_bind_without_allowlist_fails_closed(self):
"""Public binds must not accept requests until a source allowlist is configured."""
adapter = _make_adapter(host="0.0.0.0", allowed_source_cidrs=[])
payload = {
"value": [
{
@ -396,6 +418,24 @@ class TestMSGraphSourceIPAllowlist:
resp = await adapter._handle_notification(
_FakeRequest(json_payload=payload, remote="203.0.113.99")
)
assert resp.status == 403
@pytest.mark.anyio
async def test_loopback_bind_without_allowlist_still_accepts_local_requests(self):
"""Loopback-only listeners may rely on local proxying/tunnels instead of CIDRs."""
adapter = _make_adapter(host="127.0.0.1", allowed_source_cidrs=[])
payload = {
"value": [
{
"id": "notif-ip-local",
"resource": "communications/onlineMeetings/m",
"clientState": "expected-client-state",
}
]
}
resp = await adapter._handle_notification(
_FakeRequest(json_payload=payload, remote="127.0.0.1")
)
assert resp.status == 202
@pytest.mark.anyio
@ -441,6 +481,13 @@ class TestMSGraphSourceIPAllowlist:
)
assert resp.status == 403
@pytest.mark.anyio
async def test_health_endpoint_also_respects_allowlist(self):
"""The readiness endpoint should not leak counters to arbitrary sources."""
adapter = _make_adapter(allowed_source_cidrs=["10.0.0.0/8"])
resp = await adapter._handle_health(_FakeRequest(remote="203.0.113.99"))
assert resp.status == 403
@pytest.mark.anyio
async def test_invalid_cidr_entries_are_ignored_at_init(self):
"""Malformed CIDR strings should log a warning and be ignored, not crash."""