Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 16 additions & 6 deletions doris_mcp_server/utils/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

import json
import logging
import math
import multiprocessing
import os
import secrets
Expand Down Expand Up @@ -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")
Expand Down
3 changes: 2 additions & 1 deletion doris_mcp_server/utils/doris_http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import asyncio
import ipaddress
import math
import socket
from collections.abc import Mapping
from dataclasses import dataclass
Expand Down Expand Up @@ -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)

Expand Down
55 changes: 55 additions & 0 deletions test/security/test_doris_http_ssrf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down