fix(s52): scrub every URL-bearing exception path in WebhookNotifier

This commit is contained in:
Kjell Tore Guttormsen 2026-07-17 03:15:57 +02:00
commit c48e2102d0
3 changed files with 38 additions and 8 deletions

View file

@ -108,7 +108,8 @@ def test_webhook_posts_correct_payload() -> None:
def test_webhook_default_post_is_urllib_post() -> None:
"""inspect.signature proves the stdlib default WITHOUT opening a socket — ``_urllib_post`` is
the single socket path and is never called by the offline suite."""
the single socket path; the offline suite never lets it reach a socket (the only in-suite
call uses a scheme-less URL that fails at parse time)."""
sig = inspect.signature(WebhookNotifier.__init__)
assert sig.parameters["post"].default is _urllib_post

View file

@ -12,6 +12,7 @@ Seams pinned here:
from __future__ import annotations
import ast
import traceback
from pathlib import Path
import pytest
@ -109,18 +110,22 @@ def test_no_socket_call_outside_the_post_seam() -> None:
def test_webhook_error_never_leaks_url() -> None:
"""LOAD-BEARING (secret discipline): a Slack/Teams webhook URL embeds the receiver secret, so
the wrapped ``NotifyError`` must NOT contain it (STRICTER than ``ingest.py:314``, which may
include its url). Positive control: the url IS delivered to a successful transport so the
negative assertion has teeth. Detach (put the url in the error message) RED."""
include its url). The injected ``OSError`` EMBEDS the url in its message, so the traceback
assertion guards the severed cause-chain: restore ``from exc`` the URL-bearing cause renders
via ``traceback.format_exception`` RED. Positive control: the url IS delivered to a
successful transport so the negative assertion has teeth. Detach (put the url in the error
message, or restore ``from exc``) RED."""
url = "https://hooks.example.test/T000/B000/secret-token"
def failing_post(post_url: str, body: str) -> None:
raise OSError("connection refused")
raise OSError(f"connection refused for {url}")
notifier = WebhookNotifier(url, allow_egress=True, post=failing_post)
with pytest.raises(NotifyError) as excinfo:
notifier(_verdict())
assert url not in str(excinfo.value)
assert url not in repr(excinfo.value)
assert url not in "".join(traceback.format_exception(excinfo.value))
# Positive control: the same url reaches a working transport verbatim.
posts: list[tuple[str, str]] = []
@ -132,6 +137,23 @@ def test_webhook_error_never_leaks_url() -> None:
assert posts[0][0] == url
def test_webhook_malformed_url_never_leaks_through_real_transport() -> None:
"""LOAD-BEARING (secret discipline, real transport): a scheme-less webhook URL makes the
DEFAULT ``_urllib_post`` raise a bare ``ValueError`` carrying the URL verbatim (``unknown url
type: 'hooks…secret'``) the raise happens at URL parsing, BEFORE any socket, so the suite
stays socket-free. Direct construction bypasses ``NotifierConfig``'s scheme gate, so the catch
in ``WebhookNotifier.__call__`` is the actual safety net. Detach points: narrow the catch back
to ``(URLError, OSError)`` the ValueError escapes unscrubbed RED; restore ``from exc``
the URL renders via ``__cause__`` in ``traceback.format_exception`` RED."""
url = "hooks.example.test/T000/B000/secret-token"
notifier = WebhookNotifier(url, allow_egress=True) # default _urllib_post, no injection
with pytest.raises(NotifyError) as excinfo:
notifier(_verdict())
assert "secret-token" not in str(excinfo.value)
assert "secret-token" not in repr(excinfo.value)
assert "secret-token" not in "".join(traceback.format_exception(excinfo.value))
def test_notify_registered_maf_free() -> None:
"""Meta: notify.py is registered in the MAF-free guard list, so ``test_okf_is_maf_free``
actually scans it otherwise the MAF-free claim would be green-but-dead (never checked)."""