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.
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.)
🔄 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)
- [`3a3da3c`](https://github.com/reconurge/flowsint/commit/3a3da3cf0e2f4813be199c8d87a48b9f10fbf0ba) 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>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
📋 Pull Request Information
Original PR: https://github.com/reconurge/flowsint/pull/164
Author: @Mubashirrrr
Created: 6/3/2026
Status: 🔄 Open
Base:
main← Head:fix/ssrf-numeric-ip-bypass📝 Commits (1)
3a3da3cfix(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.pyprotects outbound enricher requests against SSRF viavalidate_url_safe→is_ip_blocked.is_ip_blockedcanonicalizes the host withipaddress.ip_address, which accepts only the dotted-quad IPv4 form.However,
httpx(used byTemplateEnricher) and the OS resolver also accept legacy numeric IPv4 encodings that all resolve to127.0.0.1:21307064330x7f000001017700000001127.1ipaddress.ip_addressrejects all of these, sois_ip_blockedreturnedFalseand the URL was reported ALLOWED — bypassing the loopback/private/metadata blocklist. A template URL likehttp://2130706433/therefore reaches internal services.validate_url_safeis reached on the real request path:template_enricher.pyrenders a template URL and callsvalidate_url_safe(url)immediately before thehttpxrequest.Reproduction
End-to-end confirmation against a loopback listener:
httpx.get("http://2130706433:<port>/")andhttpx.get("http://0x7f000001:<port>/")both connect to127.0.0.1and return the internal response, whilevalidate_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 tois_ip_blockedplus a small_normalize_iphelper.Regression tests
Added to the existing
TestSSRFProtectionclass intests/templates/test_loader.py:test_is_ip_blocked_numeric_loopback_encodingsandtest_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 intests/templates/test_template_enricher.pyexist onmainindependently 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.