From 28d8e0d64ebc4297a962e4ea44b1aadfcb7ef4fa Mon Sep 17 00:00:00 2001 From: sol713 <133726765+sol713@users.noreply.github.com> Date: Fri, 9 Oct 2026 13:25:43 +0800 Subject: [PATCH] fix(http): reject non-finite Doris HTTP timeouts Reject NaN and infinite connect/read/total timeout configuration, and use safe defaults at the HTTP client boundary when configuration validation is bypassed. Add configuration, client, and isolated loopback HTTP regressions showing that NaN cannot disable the total request deadline. Generated-by: OpenAI Codex (GPT-6) --- CHANGELOG.md | 3 ++ doris_mcp_server/utils/config.py | 22 ++++++--- doris_mcp_server/utils/doris_http_client.py | 3 +- test/security/test_doris_http_ssrf.py | 55 +++++++++++++++++++++ 4 files changed, 76 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b5363d..06aa1e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,9 @@ under **Unreleased** until a new version is selected and published. ### Fixed +- Rejected non-finite Doris HTTP timeout configuration and restored safe + timeout defaults at the client boundary, preventing NaN values from + disabling the total request deadline. - Preserved fail-closed distribution provenance across version parsing, brandless FE/BE component fallback, and certification evidence validation, preventing unknown or mismatched brands from inheriting Apache Doris diff --git a/doris_mcp_server/utils/config.py b/doris_mcp_server/utils/config.py index 7646365..a37b0dd 100644 --- a/doris_mcp_server/utils/config.py +++ b/doris_mcp_server/utils/config.py @@ -22,6 +22,7 @@ import json import logging +import math import multiprocessing import os import secrets @@ -2252,14 +2253,23 @@ def validate(self) -> list[str]: if not (1 <= self.database.be_webserver_port <= 65535): errors.append("Doris BE HTTP port must be in the range 1-65535") - if self.database.http_connect_timeout_seconds <= 0: - errors.append("Doris HTTP connect timeout must be greater than 0") + if ( + not math.isfinite(self.database.http_connect_timeout_seconds) + or self.database.http_connect_timeout_seconds <= 0 + ): + errors.append("Doris HTTP connect timeout must be finite and greater than 0") - if self.database.http_read_timeout_seconds <= 0: - errors.append("Doris HTTP read timeout must be greater than 0") + if ( + not math.isfinite(self.database.http_read_timeout_seconds) + or self.database.http_read_timeout_seconds <= 0 + ): + errors.append("Doris HTTP read timeout must be finite and greater than 0") - if self.database.http_total_timeout_seconds <= 0: - errors.append("Doris HTTP total timeout must be greater than 0") + if ( + not math.isfinite(self.database.http_total_timeout_seconds) + or self.database.http_total_timeout_seconds <= 0 + ): + errors.append("Doris HTTP total timeout must be finite and greater than 0") if self.database.http_max_response_bytes <= 0: errors.append("Doris HTTP response byte limit must be greater than 0") diff --git a/doris_mcp_server/utils/doris_http_client.py b/doris_mcp_server/utils/doris_http_client.py index 41c0a26..2e89021 100644 --- a/doris_mcp_server/utils/doris_http_client.py +++ b/doris_mcp_server/utils/doris_http_client.py @@ -20,6 +20,7 @@ import asyncio import ipaddress +import math import socket from collections.abc import Mapping from dataclasses import dataclass @@ -168,7 +169,7 @@ def _bounded_float(value: Any, default: float) -> float: parsed = float(value) except (TypeError, ValueError): return default - if parsed <= 0: + if not math.isfinite(parsed) or parsed <= 0: return default return min(parsed, MAX_TIMEOUT_SECONDS) diff --git a/test/security/test_doris_http_ssrf.py b/test/security/test_doris_http_ssrf.py index 20977b0..7824570 100644 --- a/test/security/test_doris_http_ssrf.py +++ b/test/security/test_doris_http_ssrf.py @@ -300,6 +300,61 @@ async def test_read_and_total_timeouts_are_enforced() -> None: ) +@pytest.mark.parametrize( + ("setting", "label"), + [ + ("DORIS_HTTP_CONNECT_TIMEOUT_SECONDS", "connect"), + ("DORIS_HTTP_READ_TIMEOUT_SECONDS", "read"), + ("DORIS_HTTP_TOTAL_TIMEOUT_SECONDS", "total"), + ], +) +@pytest.mark.parametrize("value", ["nan", "inf", "-inf", "1e309"]) +def test_http_timeout_configuration_rejects_non_finite_values( + monkeypatch: pytest.MonkeyPatch, + setting: str, + label: str, + value: str, +) -> None: + monkeypatch.setenv(setting, value) + config = DorisConfig.from_env() + assert any( + error.startswith(f"Doris HTTP {label} timeout") + for error in config.validate() + ) + + +@pytest.mark.parametrize("value", [float("nan"), float("inf"), float("-inf")]) +def test_http_client_uses_safe_defaults_for_non_finite_timeouts(value: float) -> None: + client = _http_client( + "127.0.0.1", + 8030, + connect_timeout=value, + read_timeout=value, + total_timeout=value, + ) + assert client.connect_timeout_seconds == doris_http_client_module.DEFAULT_CONNECT_TIMEOUT_SECONDS + assert client.read_timeout_seconds == doris_http_client_module.DEFAULT_READ_TIMEOUT_SECONDS + assert client.total_timeout_seconds == doris_http_client_module.DEFAULT_TOTAL_TIMEOUT_SECONDS + + +async def test_nan_total_timeout_cannot_disable_the_http_deadline( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr(doris_http_client_module, "DEFAULT_TOTAL_TIMEOUT_SECONDS", 0.05) + async with _http_server(body=b"ok", delay_seconds=0.2) as port: + client = _http_client("127.0.0.1", port, total_timeout=float("nan")) + with pytest.raises(DorisHTTPRequestError, match="timed out"): + await asyncio.wait_for( + client.get( + role="fe", + host="127.0.0.1", + port=port, + path="/metrics", + ), + timeout=1, + ) + + async def test_monitoring_does_not_discover_be_http_nodes_from_sql() -> None: manager = _manager( _database_config(