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

Open
opened 2026-06-07 15:05:19 -05:00 by GiteaMirror · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/reconurge/flowsint/pull/164
Author: @Mubashirrrr
Created: 6/3/2026
Status: 🔄 Open

Base: mainHead: fix/ssrf-numeric-ip-bypass


📝 Commits (1)

  • 1e426cf fix(ssrf): block numeric-encoded IPv4 loopback/private addresses

📊 Changes

2 files changed (+57 additions, -7 deletions)

View changed files

📝 flowsint-core/src/flowsint_core/templates/loader/yaml_loader.py (+27 -7)
📝 flowsint-core/tests/templates/test_loader.py (+30 -0)

📄 Description

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


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/reconurge/flowsint/pull/164 **Author:** [@Mubashirrrr](https://github.com/Mubashirrrr) **Created:** 6/3/2026 **Status:** 🔄 Open **Base:** `main` ← **Head:** `fix/ssrf-numeric-ip-bypass` --- ### 📝 Commits (1) - [`1e426cf`](https://github.com/reconurge/flowsint/commit/1e426cfde09384aaab24f78b782066b60579316b) fix(ssrf): block numeric-encoded IPv4 loopback/private addresses ### 📊 Changes **2 files changed** (+57 additions, -7 deletions) <details> <summary>View changed files</summary> 📝 `flowsint-core/src/flowsint_core/templates/loader/yaml_loader.py` (+27 -7) 📝 `flowsint-core/tests/templates/test_loader.py` (+30 -0) </details> ### 📄 Description ## Bug (security — SSRF bypass) `flowsint-core/src/flowsint_core/templates/loader/yaml_loader.py` protects outbound enricher requests against SSRF via `validate_url_safe` → `is_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 ```python 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. `0x08080808` → `8.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_bypass` — **fail 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](https://claude.com/claude-code) --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
GiteaMirror added the pull-request label 2026-06-07 15:05:19 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/flowsint#2633