Skip to content

fix(ssrf): block numeric-encoded IPv4 loopback/private addresses#164

Open
Mubashirrrr wants to merge 1 commit into
reconurge:mainfrom
Mubashirrrr:fix/ssrf-numeric-ip-bypass
Open

fix(ssrf): block numeric-encoded IPv4 loopback/private addresses#164
Mubashirrrr wants to merge 1 commit into
reconurge:mainfrom
Mubashirrrr:fix/ssrf-numeric-ip-bypass

Conversation

@Mubashirrrr

Copy link
Copy Markdown
Contributor

Bug (security — SSRF bypass)

flowsint-core/src/flowsint_core/templates/loader/yaml_loader.py protects outbound enricher requests against SSRF via validate_url_safeis_ip_blocked. is_ip_blocked canonicalizes the host with ipaddress.ip_address, which accepts only the dotted-quad IPv4 form.

However, httpx (used by TemplateEnricher) and the OS resolver also accept legacy numeric IPv4 encodings that all resolve to 127.0.0.1:

  • decimal: 2130706433
  • hex: 0x7f000001
  • octal: 017700000001
  • short form: 127.1

ipaddress.ip_address rejects all of these, so is_ip_blocked returned False and the URL was reported ALLOWED — bypassing the loopback/private/metadata blocklist. A template URL like http://2130706433/ therefore reaches internal services.

validate_url_safe is reached on the real request path: template_enricher.py renders a template URL and calls validate_url_safe(url) immediately before the httpx request.

Reproduction

from flowsint_core.templates.loader.yaml_loader import validate_url_safe, is_ip_blocked

is_ip_blocked("2130706433")          # before: False   (after: True)
validate_url_safe("http://2130706433/")  # before: returns (ALLOWED); after: raises SSRFError

End-to-end confirmation against a loopback listener: httpx.get("http://2130706433:<port>/") and httpx.get("http://0x7f000001:<port>/") both connect to 127.0.0.1 and return the internal response, while validate_url_safe (pre-fix) raised nothing.

Fix

Normalize the host literal through socket.inet_aton (which mirrors the C resolver's acceptance of decimal/hex/octal/short IPv4 forms) before the blocklist check. All numeric encodings of a blocked address are now caught; numeric encodings of public addresses (e.g. 0x080808088.8.8.8) remain allowed. Change is confined to is_ip_blocked plus a small _normalize_ip helper.

Regression tests

Added to the existing TestSSRFProtection class in tests/templates/test_loader.py:

  • test_is_ip_blocked_numeric_loopback_encodings and test_validate_url_safe_numeric_ip_bypassfail before this change (numeric forms reported as not-blocked / ALLOWED), pass after.
  • test_is_ip_blocked_numeric_public_still_allowed — guards against false positives on a public numeric address.

tests/templates/test_loader.py: 43 passed on Python 3.12. (Pre-existing, unrelated failures/errors in tests/templates/test_template_enricher.py exist on main independently of this change and are untouched by it.)

🤖 Generated with Claude Code

The SSRF guard (is_ip_blocked / validate_url_safe) only canonicalized
hostnames via ipaddress.ip_address, which accepts the dotted-quad form
exclusively. HTTP clients (httpx) and the OS resolver also accept the
decimal ("2130706433"), hexadecimal ("0x7f000001"), octal
("017700000001") and short ("127.1") forms of an IPv4 address, all of
which resolve to 127.0.0.1. Those forms were reported as ALLOWED, so a
template URL such as http://2130706433/ bypassed the loopback/private
blocklist entirely and reached internal services — defeating the SSRF
protection.

Normalize the host literal through socket.inet_aton (which mirrors the C
resolver's acceptance of legacy numeric IPv4 forms) before the blocklist
check, so all numeric encodings of a blocked address are caught while
numeric encodings of public addresses remain allowed.

Adds regression tests for decimal/hex/octal/short loopback encodings (at
is_ip_blocked and validate_url_safe level) plus a public numeric address
that must still pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

1 participant