mirror of
https://github.com/safishamsi/graphify.git
synced 2026-09-22 21:45:58 +00:00
security: SSRF private IP blocking and Neo4j Cypher injection fix
Two targeted security hardening changes: 1. SSRF: validate_url() now resolves hostnames and blocks private/reserved IP ranges (127.x, 10.x, 169.254.x, etc.) and cloud metadata endpoints. Prevents SSRF attacks in cloud environments where a URL like http://169.254.169.254/latest/meta-data/ could leak instance credentials. 2. Neo4j Cypher injection: Node labels (file_type) are now sanitized to alphanumeric + underscore before interpolation into Cypher queries. Previously, attacker-controlled file_type metadata could inject arbitrary Cypher in team/shared graph scenarios. Both changes are zero-impact on normal UX - no new config, no new flags, no behavioral changes for legitimate use.
This commit is contained in:
+6
-1
@@ -832,6 +832,11 @@ def push_to_neo4j(
|
||||
def _safe_rel(relation: str) -> str:
|
||||
return re.sub(r"[^A-Z0-9_]", "_", relation.upper().replace(" ", "_").replace("-", "_")) or "RELATED_TO"
|
||||
|
||||
def _safe_label(label: str) -> str:
|
||||
"""Sanitize a Neo4j node label to prevent Cypher injection."""
|
||||
sanitized = re.sub(r"[^A-Za-z0-9_]", "", label)
|
||||
return sanitized if sanitized else "Entity"
|
||||
|
||||
driver = GraphDatabase.driver(uri, auth=(user, password))
|
||||
nodes_pushed = 0
|
||||
edges_pushed = 0
|
||||
@@ -843,7 +848,7 @@ def push_to_neo4j(
|
||||
cid = node_community.get(node_id)
|
||||
if cid is not None:
|
||||
props["community"] = cid
|
||||
ftype = data.get("file_type", "Entity").capitalize()
|
||||
ftype = _safe_label(data.get("file_type", "Entity").capitalize())
|
||||
session.run(
|
||||
f"MERGE (n:{ftype} {{id: $id}}) SET n += $props",
|
||||
id=node_id,
|
||||
|
||||
+34
-2
@@ -8,20 +8,28 @@ import urllib.parse
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
import ipaddress
|
||||
import socket
|
||||
|
||||
_ALLOWED_SCHEMES = {"http", "https"}
|
||||
_MAX_FETCH_BYTES = 52_428_800 # 50 MB hard cap for binary downloads
|
||||
_MAX_TEXT_BYTES = 10_485_760 # 10 MB hard cap for HTML / text
|
||||
|
||||
# AWS metadata, link-local, and common cloud metadata endpoints
|
||||
_BLOCKED_HOSTS = {"metadata.google.internal", "metadata.google.com"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# URL validation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def validate_url(url: str) -> str:
|
||||
"""Raise ValueError if *url* is not http or https.
|
||||
"""Raise ValueError if *url* is not http or https, or targets a private/internal IP.
|
||||
|
||||
Blocks file://, ftp://, data:, and any other scheme that could be used
|
||||
for SSRF or local file access.
|
||||
for SSRF or local file access. Also blocks requests to private/reserved
|
||||
IP ranges (127.x, 10.x, 169.254.x, etc.) and cloud metadata endpoints
|
||||
to prevent SSRF in cloud environments.
|
||||
"""
|
||||
parsed = urllib.parse.urlparse(url)
|
||||
if parsed.scheme.lower() not in _ALLOWED_SCHEMES:
|
||||
@@ -29,6 +37,30 @@ def validate_url(url: str) -> str:
|
||||
f"Blocked URL scheme '{parsed.scheme}' - only http and https are allowed. "
|
||||
f"Got: {url!r}"
|
||||
)
|
||||
|
||||
hostname = parsed.hostname
|
||||
if hostname:
|
||||
# Block known cloud metadata hostnames
|
||||
if hostname.lower() in _BLOCKED_HOSTS:
|
||||
raise ValueError(
|
||||
f"Blocked cloud metadata endpoint '{hostname}'. "
|
||||
f"Got: {url!r}"
|
||||
)
|
||||
|
||||
# Resolve hostname and block private/reserved IP ranges
|
||||
try:
|
||||
infos = socket.getaddrinfo(hostname, None, socket.AF_UNSPEC, socket.SOCK_STREAM)
|
||||
for info in infos:
|
||||
addr = info[4][0]
|
||||
ip = ipaddress.ip_address(addr)
|
||||
if ip.is_private or ip.is_reserved or ip.is_loopback or ip.is_link_local:
|
||||
raise ValueError(
|
||||
f"Blocked private/internal IP {addr} (resolved from '{hostname}'). "
|
||||
f"Got: {url!r}"
|
||||
)
|
||||
except socket.gaierror:
|
||||
pass # DNS failure will surface later during fetch
|
||||
|
||||
return url
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user