[SECURITY] HTTP tools: add SSRF protection #133

Closed
opened 2026-03-31 01:40:15 +00:00 by Timmy · 1 comment
Owner

From Audit #131 — Severity: HIGH

uni-wizard/tools/network_tools.py http_get() and http_post() accept arbitrary URLs with zero validation. Can be used to probe internal services, cloud metadata endpoints, or localhost.

Fix

Port the SSRF protection from hermes-agent/tools/url_safety.py (already merged in PR #59). Before any HTTP request, validate the resolved IP:

import ipaddress
from urllib.parse import urlparse

BLOCKED_NETS = [
    ipaddress.ip_network("127.0.0.0/8"),
    ipaddress.ip_network("10.0.0.0/8"),
    ipaddress.ip_network("172.16.0.0/12"),
    ipaddress.ip_network("192.168.0.0/16"),
    ipaddress.ip_network("169.254.0.0/16"),  # link-local / cloud metadata
    ipaddress.ip_network("100.64.0.0/10"),   # CGNAT / Tailscale
]

def is_safe_url(url: str) -> bool:
    host = urlparse(url).hostname
    try:
        ip = ipaddress.ip_address(socket.gethostbyname(host))
        return not any(ip in net for net in BLOCKED_NETS)
    except:
        return False

Add validation at the top of http_get() and http_post().

Acceptance Criteria

  • Private IPs blocked (127.x, 10.x, 172.16-31.x, 192.168.x)
  • Cloud metadata blocked (169.254.169.254)
  • CGNAT/Tailscale range blocked (100.64.0.0/10)
  • Clear error message on blocked URLs
  • Test: http_get("http://169.254.169.254/") returns blocked error
## From Audit #131 — Severity: HIGH `uni-wizard/tools/network_tools.py` `http_get()` and `http_post()` accept arbitrary URLs with zero validation. Can be used to probe internal services, cloud metadata endpoints, or localhost. ## Fix Port the SSRF protection from `hermes-agent/tools/url_safety.py` (already merged in PR #59). Before any HTTP request, validate the resolved IP: ```python import ipaddress from urllib.parse import urlparse BLOCKED_NETS = [ ipaddress.ip_network("127.0.0.0/8"), ipaddress.ip_network("10.0.0.0/8"), ipaddress.ip_network("172.16.0.0/12"), ipaddress.ip_network("192.168.0.0/16"), ipaddress.ip_network("169.254.0.0/16"), # link-local / cloud metadata ipaddress.ip_network("100.64.0.0/10"), # CGNAT / Tailscale ] def is_safe_url(url: str) -> bool: host = urlparse(url).hostname try: ip = ipaddress.ip_address(socket.gethostbyname(host)) return not any(ip in net for net in BLOCKED_NETS) except: return False ``` Add validation at the top of `http_get()` and `http_post()`. ## Acceptance Criteria - [ ] Private IPs blocked (127.x, 10.x, 172.16-31.x, 192.168.x) - [ ] Cloud metadata blocked (169.254.169.254) - [ ] CGNAT/Tailscale range blocked (100.64.0.0/10) - [ ] Clear error message on blocked URLs - [ ] Test: `http_get("http://169.254.169.254/")` returns blocked error
allegro was assigned by Timmy 2026-03-31 01:40:15 +00:00
Member

🏷️ Automated Triage Check

Timestamp: 2026-03-31T03:30:04.165101
Agent: Allegro Heartbeat

This issue has been identified as needing triage:

Checklist

  • Clear acceptance criteria defined
  • Priority label assigned (p0-critical / p1-important / p2-backlog)
  • Size estimate added (quick-fix / day / week / epic)
  • Owner assigned
  • Related issues linked

Context

  • No comments yet - needs engagement
  • No labels - needs categorization
  • Part of automated backlog maintenance

Automated triage from Allegro 15-minute heartbeat

## 🏷️ Automated Triage Check **Timestamp:** 2026-03-31T03:30:04.165101 **Agent:** Allegro Heartbeat This issue has been identified as needing triage: ### Checklist - [ ] Clear acceptance criteria defined - [ ] Priority label assigned (p0-critical / p1-important / p2-backlog) - [ ] Size estimate added (quick-fix / day / week / epic) - [ ] Owner assigned - [ ] Related issues linked ### Context - No comments yet - needs engagement - No labels - needs categorization - Part of automated backlog maintenance --- *Automated triage from Allegro 15-minute heartbeat*
Sign in to join this conversation.
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Timmy_Foundation/timmy-home#133