Files
hermes-agent/tests/gateway/test_slack_download_ssrf.py
T
Teknium 39975613b1 test: prune wave 2 + speed fixes — 28,106 → 19,757 test functions, suite wall 315s → 294s
Second, deeper pass over tools/gateway/hermes_cli plus first pass over
the trees wave 1 missed (acp, acp_adapter, skills, computer_use, docker,
dashboard, conformance, monitoring, secret_sources, hermes_state,
providers). Same rubric as wave 1 (AGENTS.md test policy); security,
alternation/caching invariants, issue-number regressions, and E2E kept.

Real test-quality fixes found and rooted out along the way:
- tests/tools/test_command_guards.py made real auxiliary-LLM HTTPS calls
  (DEFAULT_CONFIG smart-approval leaked in) — pinned approval
  mode=manual via autouse fixture: 17.4s → 0.4s.
- test_model_switch_custom_providers.py / test_user_providers_model_switch.py
  silently probed live provider catalogs (~2s/test) — stubbed
  cached_provider_model_ids/provider_model_ids/fetch_api_models.
- test_telegram_noise_filter.py: 15-platform copy-paste matrix over
  shared gateway.run logic → 3 representative platforms (55s → 3.9s).
- test_gateway_shutdown.py: stop()'s 5s interrupt-deadline loop spun on
  MagicMock agents — interrupt.side_effect now clears _running_agents
  (22s → 1.0s).
- test_gateway_inactivity_timeout.py poll-harness timings shrunk 3-5x
  (24s → 1.1s); test_mcp_stability.py backoff/SIGTERM-grace sleeps
  patched (15.4s → 2.5s); test_async_delegation.py negative-drain wait
  5s → 0.5s.
- test_telegram_init_deadline.py: loop-block margin restored to 1.0s
  with rationale comment — the watchdog-dump assertion needs the loop
  blocked well past deadline+grace under parallel load (flaked once in
  the 40-worker verification run at a 0.2s margin).

Verification: full hermetic suite via scripts/run_tests.sh —
2,438 files, 21,718 tests passed, 0 failed, 293.9s wall.
Suite totals vs original baseline: 46,820 → 19,757 test functions
(−57.8%), wall 583.5s → 293.9s (−50%), subprocess CPU 13,564s → 11,623s.
2026-07-29 13:39:40 -07:00

151 lines
4.9 KiB
Python

"""SSRF regression tests for inbound Slack file downloads.
``_download_slack_file`` / ``_download_slack_file_bytes`` attach the bot
token and follow redirects, so they must validate the destination (CWE-918)
exactly like the already-guarded outbound ``send_image`` path: a pre-flight
``is_safe_url`` check plus a per-redirect guard.
"""
import asyncio
import sys
from types import SimpleNamespace
from unittest.mock import MagicMock
import pytest
from gateway.platforms.base import _ssrf_redirect_guard
def _ensure_slack_mock():
"""Install mock slack modules so SlackAdapter can be imported."""
if "slack_bolt" not in sys.modules:
for name in (
"slack_bolt",
"slack_bolt.adapter",
"slack_bolt.adapter.socket_mode",
"slack_bolt.adapter.socket_mode.async_handler",
"slack_bolt.async_app",
"slack_sdk",
"slack_sdk.web",
"slack_sdk.web.async_client",
"slack_sdk.errors",
):
sys.modules.setdefault(name, MagicMock())
if "aiohttp" not in sys.modules:
sys.modules.setdefault("aiohttp", MagicMock())
_ensure_slack_mock()
from plugins.platforms.slack.adapter import SlackAdapter # noqa: E402
def _fake_adapter():
self = SlackAdapter.__new__(SlackAdapter)
self.config = SimpleNamespace(token="xoxb-test-token")
self._team_clients = {}
return self
class _NetworkTouched(RuntimeError):
pass
class _RecordingClient:
"""Captures AsyncClient kwargs; refuses to perform real I/O."""
last_kwargs = None
def __init__(self, **kwargs):
type(self).last_kwargs = kwargs
async def __aenter__(self):
return self
async def __aexit__(self, *exc):
return False
async def get(self, *args, **kwargs):
raise _NetworkTouched("network access attempted")
@pytest.mark.parametrize(
"method_name",
["_download_slack_file", "_download_slack_file_bytes"],
)
def test_unsafe_url_blocked_before_network(monkeypatch, method_name):
import tools.url_safety as url_safety
calls = {"checked": []}
def fake_is_safe_url(url, *a, **k):
calls["checked"].append(url)
return False
monkeypatch.setattr(url_safety, "is_safe_url", fake_is_safe_url)
# If the guard is bypassed, the fake client raises _NetworkTouched; a
# correct implementation raises ValueError *before* touching httpx.
monkeypatch.setattr("httpx.AsyncClient", _RecordingClient)
self = _fake_adapter()
method = getattr(self, method_name)
args = ("http://169.254.169.254/latest/meta-data/", ".jpg") \
if method_name == "_download_slack_file" \
else ("http://169.254.169.254/latest/meta-data/",)
with pytest.raises(ValueError):
asyncio.run(method(*args))
assert calls["checked"], "download must call is_safe_url before fetching"
@pytest.mark.parametrize(
"method_name",
["_download_slack_file", "_download_slack_file_bytes"],
)
def test_redirect_guard_is_wired(monkeypatch, method_name):
import tools.url_safety as url_safety
monkeypatch.setattr(url_safety, "is_safe_url", lambda *a, **k: True)
monkeypatch.setattr("httpx.AsyncClient", _RecordingClient)
self = _fake_adapter()
method = getattr(self, method_name)
args = ("https://files.slack.com/x.jpg", ".jpg") \
if method_name == "_download_slack_file" \
else ("https://files.slack.com/x.jpg",)
# The fake client raises when .get() is called; we only care that the
# client was constructed with the redirect guard hook.
with pytest.raises(_NetworkTouched):
asyncio.run(method(*args))
kwargs = _RecordingClient.last_kwargs
assert kwargs is not None
hooks = kwargs.get("event_hooks", {})
assert _ssrf_redirect_guard in hooks.get("response", []), (
"AsyncClient must register _ssrf_redirect_guard to block "
"redirect-based SSRF"
)
# ---------------------------------------------------------------------------
# Slack-CDN allowlist (follow-up hardening on top of #44026)
#
# ``url_private`` / ``url_private_download`` legitimately only ever point at
# the Slack CDN. Because the download attaches the bot token as a Bearer
# header, a forged file object (malicious workspace app / compromised event
# stream) pointing at ANY public host would exfiltrate the token — a hole the
# generic private-IP SSRF check cannot close. The adapter therefore refuses
# every non-Slack-CDN https URL up front.
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# Connect-time DNS pinning (composition with #57860): a Slack-CDN hostname
# whose DNS answer flips from public at preflight to a metadata IP at connect
# time must be blocked before any TCP connect is attempted.
# ---------------------------------------------------------------------------