diff --git a/gateway/platforms/webhook.py b/gateway/platforms/webhook.py index 5acd2c87a5..fc886965a0 100644 --- a/gateway/platforms/webhook.py +++ b/gateway/platforms/webhook.py @@ -386,8 +386,8 @@ class WebhookAdapter(BasePlatformAdapter): return web.json_response({"status": "ok", "platform": "webhook"}) def _dynamic_route_allowed(self, name: str, route: dict) -> bool: - """An empty effective secret would make _handle_webhook skip HMAC validation → reject such - dynamic routes; INSECURE_NO_AUTH is loopback-only.""" + """Reject dynamic routes whose effective secret is missing, blank, or not a string + (_is_usable_secret); INSECURE_NO_AUTH is loopback-only.""" effective_secret = route.get("secret", self._global_secret) if not _is_usable_secret(effective_secret): logger.warning("[webhook] Dynamic route '%s' skipped: 'secret' is missing, blank, or not a string. " diff --git a/tests/gateway/test_webhook_adapter.py b/tests/gateway/test_webhook_adapter.py index 54cd343294..db0d1898f6 100644 --- a/tests/gateway/test_webhook_adapter.py +++ b/tests/gateway/test_webhook_adapter.py @@ -1252,11 +1252,13 @@ def test_route_profile_validation_fails_closed(): class TestBlankRouteSecretFailsClosed: - @pytest.mark.parametrize("blank", [" ", "\t", "\n", 123]) - def test_connect_rejects_a_whitespace_only_route_secret(self, blank): - adapter = _make_adapter(routes={"hook": {"secret": blank}}) + @pytest.mark.parametrize("secret", [" ", "\t", "\n", 123]) + def test_connect_rejects_an_unusable_route_secret(self, secret): + adapter = _make_adapter(routes={"hook": {"secret": secret}}) with pytest.raises(ValueError, match="missing, blank, or not a string"): asyncio.run(adapter.connect()) + # The hot-reload path applies the same guard to agent-created dynamic routes. + assert adapter._dynamic_route_allowed("hook", {"secret": secret}) is False @pytest.mark.parametrize(