From 7788e3550f6a5c8a75967ce3248285d730810505 Mon Sep 17 00:00:00 2001 From: aydnOktay Date: Wed, 5 Aug 2026 20:33:30 +0300 Subject: [PATCH] fix(webhook): tolerate malformed int config without crashing adapter init Unguarded int() on port, rate_limit, max_body_bytes, and script_timeout_seconds ValueErrors during WebhookAdapter construction and takes the platform down. Add _coerce_int with defaults, mirroring Teams/Raft config int guards. Co-authored-by: Cursor --- gateway/platforms/webhook.py | 34 +++++++++++---- .../gateway/test_webhook_config_int_guard.py | 42 +++++++++++++++++++ 2 files changed, 69 insertions(+), 7 deletions(-) create mode 100644 tests/gateway/test_webhook_config_int_guard.py diff --git a/gateway/platforms/webhook.py b/gateway/platforms/webhook.py index da5eddbe94b79..000a52f341880 100644 --- a/gateway/platforms/webhook.py +++ b/gateway/platforms/webhook.py @@ -128,9 +128,20 @@ def _is_webhook_silence_response(content: Any) -> bool: # ``platforms.webhook.extra.host``. DEFAULT_HOST = None DEFAULT_PORT = 8644 +DEFAULT_RATE_LIMIT = 30 # requests per minute +DEFAULT_MAX_BODY_BYTES = 1_048_576 # 1MB _INSECURE_NO_AUTH = "INSECURE_NO_AUTH" _DYNAMIC_ROUTES_FILENAME = "webhook_subscriptions.json" _RATE_WINDOW_SECONDS = 60.0 + + +def _coerce_int(value: object, *, default: int) -> int: + """Parse config ints; malformed values must not crash adapter init.""" + try: + return int(value) # type: ignore[arg-type] + except (TypeError, ValueError): + return default + # Hostnames/IP literals that only serve connections originating on the same # machine. Anything else is treated as a public bind for safety-rail purposes. _LOOPBACK_HOSTS = frozenset({ @@ -190,7 +201,11 @@ def __init__(self, config: PlatformConfig): # also means "bind all families" rather than an invalid "" host. _cfg_host = config.extra.get("host", DEFAULT_HOST) self._host: Optional[str] = _cfg_host or None - self._port: int = int(config.extra.get("port", DEFAULT_PORT)) + # Malformed port / rate / size ints must not ValueError during + # construction and take the webhook platform down (Teams/Raft posture). + self._port: int = _coerce_int( + config.extra.get("port", DEFAULT_PORT), default=DEFAULT_PORT + ) self._global_secret: str = config.extra.get("secret", "") self._static_routes: Dict[str, dict] = config.extra.get("routes", {}) self._dynamic_routes: Dict[str, dict] = {} @@ -225,17 +240,22 @@ def __init__(self, config: PlatformConfig): # Rate limiting: per-route timestamps in a fixed window. self._rate_counts: Dict[str, Deque[float]] = {} - self._rate_limit: int = int(config.extra.get("rate_limit", 30)) # per minute + self._rate_limit: int = _coerce_int( + config.extra.get("rate_limit", DEFAULT_RATE_LIMIT), + default=DEFAULT_RATE_LIMIT, + ) # Body size limit (auth-before-body pattern) - self._max_body_bytes: int = int( - config.extra.get("max_body_bytes", 1_048_576) - ) # 1MB - self._script_timeout_seconds: int = int( + self._max_body_bytes: int = _coerce_int( + config.extra.get("max_body_bytes", DEFAULT_MAX_BODY_BYTES), + default=DEFAULT_MAX_BODY_BYTES, + ) + self._script_timeout_seconds: int = _coerce_int( config.extra.get( "script_timeout_seconds", DEFAULT_SCRIPT_TIMEOUT_SECONDS, - ) + ), + default=DEFAULT_SCRIPT_TIMEOUT_SECONDS, ) self._route_processor = WebhookRouteProcessor( script_timeout_seconds=self._script_timeout_seconds diff --git a/tests/gateway/test_webhook_config_int_guard.py b/tests/gateway/test_webhook_config_int_guard.py new file mode 100644 index 0000000000000..ca4de959c1b65 --- /dev/null +++ b/tests/gateway/test_webhook_config_int_guard.py @@ -0,0 +1,42 @@ +"""Webhook adapter must tolerate malformed int config fields.""" + +from __future__ import annotations + +from gateway.config import PlatformConfig +from gateway.platforms.webhook import ( + DEFAULT_MAX_BODY_BYTES, + DEFAULT_PORT, + DEFAULT_RATE_LIMIT, + WebhookAdapter, +) + + +def _make_config(**extra): + data = { + "host": "127.0.0.1", + "port": DEFAULT_PORT, + "routes": {}, + "secret": "test-secret", + } + data.update(extra) + return PlatformConfig(enabled=True, extra=data) + + +def test_malformed_port_falls_back_to_default(): + adapter = WebhookAdapter(_make_config(port="not-a-port")) + assert adapter._port == DEFAULT_PORT + + +def test_malformed_rate_limit_falls_back_to_default(): + adapter = WebhookAdapter(_make_config(rate_limit="nope")) + assert adapter._rate_limit == DEFAULT_RATE_LIMIT + + +def test_malformed_max_body_bytes_falls_back_to_default(): + adapter = WebhookAdapter(_make_config(max_body_bytes="oops")) + assert adapter._max_body_bytes == DEFAULT_MAX_BODY_BYTES + + +def test_valid_port_is_honored(): + adapter = WebhookAdapter(_make_config(port=9001)) + assert adapter._port == 9001