From 7d4be7341b79aa7f8e80df8e8edda25fb76fde3f Mon Sep 17 00:00:00 2001 From: Khaled Salhab Date: Sun, 31 May 2026 15:49:20 +0300 Subject: [PATCH 1/2] chore: release v1.8.0 Bumps the package version and moves the security-fix entries into a versioned 1.8.0 section. No code changes from main. Highlights: - base_url / mcp_url validation now rejects http://, userinfo, query, and fragment by default (breaking for http://localhost; gated by allow_insecure=True with InsecureTransportWarning). - HyperpingAPIError.response_body recursively redacted at construction. - MCP transport no longer stores the first 500 bytes of server text on error paths. - _endpoint_breakers map capped at 1024 with LRU eviction. See CHANGELOG.md for the full list and upgrade notes. --- CHANGELOG.md | 42 ++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ea5ab2..394cb80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,48 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +## [1.8.0] - 2026-05-31 + +This is a security-focused release. It closes several credential-leak and validation gaps surfaced by an independent audit. One change is breaking for local-development workflows; see Upgrade Notes below. + +### Security + +- `validate_base_url` is now applied at every constructor that accepts a `base_url` or `mcp_url`: both the sync and async REST clients, both MCP transports, and the high-level MCP clients. The validator rejects non-`https` URLs by default, rejects URLs that carry userinfo (`user:pass@host` or bare `user@host`), and rejects URLs with a query string or fragment. The `Authorization: Bearer` header therefore cannot reach an attacker-controlled host through a misconfigured base URL. +- `HyperpingAPIError.response_body` is now recursively redacted at construction time. Sensitive keys (authorization, tokens, cookies, set-cookie, request headers, request body, emails, webhooks) are replaced with `[REDACTED]`. Free-form string values are scrubbed for `Bearer ` and `sk_` shapes. The recursion is bounded to prevent `RecursionError` from pathological payloads. Applies to all subclasses, including `HyperpingRateLimitError`. +- `HyperpingAPIError` formatted messages now strip C0 control bytes (preserving `\t` and `\n`) and cap at 256 characters, so a server-supplied error string carrying ANSI escapes or a 1 KB blob cannot poison terminal output or downstream log pipelines. +- The MCP transport no longer captures the first 500 bytes of server response as `response_body["raw"]` on rate-limit (429), generic HTTP error, or JSON-parse failure paths. Subscriber emails or webhook URLs that the server may echo cannot leak through that channel. +- `_utils.parse_list` no longer logs the full `pydantic.ValidationError` string on per-item parse failures (Pydantic v2 includes the offending input by default). It now logs only the exception class and field locations. +- The `breaker_key_fn` callback is now used through an LRU-bounded map. A custom callback that returns unbounded unique strings can no longer leak memory in long-running processes: the per-endpoint breaker map is capped at 1024 entries and evicts oldest on overflow. Applies to both sync and async clients. + +### Added + +- `allow_insecure: bool = False` keyword argument on every constructor that accepts a `base_url` or `mcp_url`. When set to `True`, `http://` URLs are permitted and an `InsecureTransportWarning` is emitted. Provided for local-development workflows; not recommended for production. +- `InsecureTransportWarning` warning class exported from `hyperping`. + +### Changed + +- `_parse_retry_after` documents that it intentionally supports only the delta-seconds form of the `Retry-After` header. HTTP-date values fall through to exponential backoff rather than raising. Trade-off is documented in the docstring; no behavioral change for callers passing the integer form. + +### Upgrade Notes + +If your code instantiates `HyperpingClient` (or any MCP client) against a `http://localhost` URL for local development or against a mock server, the constructor will now raise `ValueError`. Pass `allow_insecure=True` to opt in, and expect an `InsecureTransportWarning` at runtime: + +```python +from hyperping import HyperpingClient + +client = HyperpingClient( + api_key="sk_test_xxx", + base_url="http://localhost:8000", + allow_insecure=True, +) +``` + +Production callers using `https://api.hyperping.io` are unaffected. + +URLs that previously carried embedded credentials (`https://user:pass@api.example.com`) or a trailing query string / fragment will now also raise at construction time. Refactor to pass credentials through `api_key` and to keep query strings out of the base URL. + ## [1.7.0] - 2026-05-21 ### Added diff --git a/pyproject.toml b/pyproject.toml index 865905a..47d1ff4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "hyperping" -version = "1.7.0" +version = "1.8.0" description = "Python SDK for the Hyperping uptime monitoring and incident management API" readme = {file = "README.md", content-type = "text/markdown"} license = {text = "MIT"} From a91e778caa642b2f35c22f3c868a5fd2c0d8593e Mon Sep 17 00:00:00 2001 From: Khaled Salhab Date: Sun, 31 May 2026 15:51:38 +0300 Subject: [PATCH 2/2] fix(tests): scan whole CHANGELOG for v1.7.0 MCP guardrails The previous test (test_changelog_top_section_documents_mcp_rate_limit_work) asserted that 'ensure_initialized', '### Added', '### Fixed', and 'rate limit' appeared in the TOP CHANGELOG section. That worked while the v1.7.0 work was in Unreleased, but breaks the moment a subsequent release rolls v1.7.0 down the file (v1.8.0 release prep here). The test's actual intent is 'this work must remain documented somewhere'. Renamed and rewritten to scan the entire CHANGELOG for the markers that matter (ensure_initialized, rate limit). The structural heading assertions were release-specific and not load-bearing for the guarantee. --- tests/unit/test_mcp_client.py | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/tests/unit/test_mcp_client.py b/tests/unit/test_mcp_client.py index f36e14b..b55d608 100644 --- a/tests/unit/test_mcp_client.py +++ b/tests/unit/test_mcp_client.py @@ -321,27 +321,16 @@ def test_readme_contains_mcp_rate_limits_section(): ) -def test_changelog_top_section_documents_mcp_rate_limit_work(): - """CHANGELOG's most recent section (either [Unreleased] or a released - version) must document the MCP rate-limit work shipped in this change. +def test_changelog_documents_mcp_rate_limit_work(): + """CHANGELOG must continue to document the MCP rate-limit work shipped in + v1.7.0 (ensure_initialized, JSON-RPC -32000 classification, README guidance). + Scans the entire file so subsequent releases that roll the top section over + do not silently drop the historical entry. """ - import re - changelog = (_repo_root() / "CHANGELOG.md").read_text(encoding="utf-8") - # Match the first ## heading (Unreleased or a released version) and slice - # up to the next ## heading. - match = re.search( - r"^## \[(?:Unreleased|\d+\.\d+\.\d+)\][^\n]*\n(.*?)(?=^## \[)", - changelog, - re.MULTILINE | re.DOTALL, - ) - assert match, "CHANGELOG is missing a top '## [Unreleased]' or '## [X.Y.Z]' heading" - section = match.group(1) - assert "### Added" in section, "Top CHANGELOG section is missing '### Added'" - assert "### Fixed" in section, "Top CHANGELOG section is missing '### Fixed'" - assert "ensure_initialized" in section, ( - "Top CHANGELOG section must mention ensure_initialized()" + assert "ensure_initialized" in changelog, ( + "CHANGELOG must mention ensure_initialized() somewhere" ) - assert "rate limit" in section.lower(), ( - "Top CHANGELOG section must mention rate-limit handling" + assert "rate limit" in changelog.lower(), ( + "CHANGELOG must mention rate-limit handling somewhere" )