Skip to content

fix(security): add SSRF guard for A2A peer and Gensui connect endpoints#18

Open
wstlima wants to merge 1 commit into
AlphaHorizon-AI:mainfrom
wstlima:fix/ssrf-a2a-gensui-connect
Open

fix(security): add SSRF guard for A2A peer and Gensui connect endpoints#18
wstlima wants to merge 1 commit into
AlphaHorizon-AI:mainfrom
wstlima:fix/ssrf-a2a-gensui-connect

Conversation

@wstlima

@wstlima wstlima commented Jul 10, 2026

Copy link
Copy Markdown

Fixes #9

Summary

CodeQL flagged 2 full-ssrf and 7 partial-ssrf findings. Investigated all 9:

  • Findings in shogun/api/system.py (Ollama/LMStudio model provider proxy) and shogun/services/channel_service.py (Telegram bot API — only the path is user-controlled, not the host) are working as designed. Connecting to a local/self-hosted inference server is the actual feature; blocking private IPs there would break the documented use case. Left those alone.

  • The two that are genuine unrestricted SSRF, with no legitimate reason to reach internal network ranges:

    • shogun/integrations/a2a_client.py send()/ping()peer_url comes straight from InvitePeerRequest.peer_url (no visible auth dependency on that router) and is used verbatim to build the outbound request URL. A2A peers are remote Shogun instances reached over the public internet — no valid case for a peer resolving to 127.0.0.0/8, 169.254.169.254, or an RFC 1918 address.
    • shogun/api/gensui_config.py /connect and /testreq.server_url is fully user-controlled and hits {server_url}/api/gensui/health unchecked. Gensui commonly runs on the operator's own LAN, so this endpoint needs to allow private/loopback ranges — but not the cloud metadata endpoint or link-local space.

Fix

Added shogun/services/ssrf_guard.py: resolves the hostname via socket.getaddrinfo and rejects the target if any resolved address is private/loopback/link-local/reserved/multicast.

  • assert_safe_url() defaults to strict (blocks everything above) — used for a2a_client.py
  • allow_private=True permits RFC 1918 and loopback while still blocking link-local (covers 169.254.169.254) and the well-known metadata literal addresses — used for gensui_config.py

This is a resolve-time check, not a complete SSRF defense: it doesn't protect against DNS rebinding after the check or redirects to a blocked target. Documented as a known limitation in the module docstring.

Test plan

  • 8 representative cases tested (public host, metadata endpoint in both modes, private/loopback in both modes, bad scheme) — all pass as expected
  • 127.0.0.1:8787 (the documented local Gensui use case) still works with allow_private=True

CodeQL flagged 2 full-ssrf and 7 partial-ssrf findings. Investigated all
9: the ones in shogun/api/system.py (Ollama/LMStudio model provider
proxy) and shogun/services/channel_service.py (Telegram bot API, only
the path is user-controlled, not the host) are working as designed —
connecting to a local/self-hosted inference server is the actual
feature, and blocking private IPs there would break the documented use
case. Left those alone.

The two that are genuine unrestricted SSRF, with no legitimate reason
to reach internal network ranges:

- shogun/integrations/a2a_client.py `send()` / `ping()` — peer_url
  comes straight from InvitePeerRequest.peer_url (shogun/api/a2a.py,
  no visible auth dependency on that router) and is used verbatim to
  build the outbound request URL. A2A peers are remote Shogun
  instances reached over the public internet — there's no valid case
  for an A2A peer resolving to 127.0.0.0/8, 169.254.169.254, or an
  RFC 1918 address.

- shogun/api/gensui_config.py `/connect` and `/test` — req.server_url
  is fully attacker/user-controlled and hits `{server_url}/api/gensui/
  health` unchecked. Gensui commonly runs on the operator's own LAN
  (this POC itself proxies http://127.0.0.1:8787), so this endpoint
  needs to allow private/loopback ranges — but not the cloud metadata
  endpoint or link-local space.

Added shogun/services/ssrf_guard.py: resolves the hostname via
socket.getaddrinfo and rejects the target if any resolved address is
private/loopback/link-local/reserved/multicast. `assert_safe_url()`
defaults to strict (blocks everything above — used for a2a_client.py);
`allow_private=True` permits RFC 1918 and loopback while still
blocking link-local (which covers 169.254.169.254 generically) and the
well-known metadata literal addresses explicitly — used for
gensui_config.py.

This is a resolve-time check, not a complete SSRF defense: it does not
protect against DNS rebinding after the check or against redirects to
a blocked target (httpx follows redirects by default in some call
sites here). Sufficient to close the most common/obvious vector;
documented as a known limitation in the module docstring.

Verified with 8 representative cases (public host, metadata endpoint
in both modes, private/loopback in both modes, bad scheme) — all pass
as expected. See shogun/services/ssrf_guard.py for the test snippet
used.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SSRF: A2A peer invitation and Gensui connect endpoints accept unrestricted user-supplied URLs

1 participant