CVE-2026-42352
pygeoapi SSRF vulnerability allows requests to internal HTTP services.
- CVSS 8.6
- CWE-918
- Input Validation and Sanitization
- Remote
pygeoapi is a Python server implementation of the OGC API suite of standards. From version 0.23.0 to before version 0.23.3, OGC API process execution requests can use the subscriber object to requests to internal HTTP services. This issue has been patched in version 0.23.3.
- CWE
- CWE-918
- CVSS base score
- 8.6
- Published
- 2026-05-08
- OWASP
- A10 Server-Side Request Forgery (SSRF)
- Orthogonal defect classification
- Checking
- Code defect classification
- Missing Check
- Category
- Input Validation and Sanitization
- Subcategory
- Server-Side Request Forgery (SSRF)
- Accessibility scope
- Remote
- Impact
- Unauthorized Access
- Affected component
- pygeoapi
- Fixed by upgrading
- Yes
Solution
Upgrade pygeoapi to version 0.23.3 or later.
Vulnerable code sample
import requests
import logging
LOGGER = logging.getLogger(__name__)
# This function is a simplified representation of the vulnerable logic
# found in pygeoapi.process.manager.base.BaseManager._execute_handler_async
# before version 0.23.3.
def vulnerable_async_notification_handler(data: dict):
"""
Simulates executing a process and notifying a subscriber upon completion.
"""
job_result = {
"jobID": "a-job-id",
"status": "successful",
"type": "process",
"message": "Process completed successfully"
}
# The payload 'data' is user-controlled.
subscriber = data.get('subscriber', {})
if 'url' in subscriber:
url = subscriber['url']
LOGGER.debug(f'Submitting result to subscriber at {url}')
try:
# VULNERABLE CODE: The server makes a POST request to a user-provided
# URL without any validation, leading to Server-Side Request Forgery (SSRF).
# An attacker could specify an internal URL like 'http://127.0.0.1/admin'
# or a cloud metadata service endpoint.
requests.post(url, json=job_result)
except requests.exceptions.RequestException as err:
LOGGER.error(err)Patched code sample
import ipaddress
import socket
from urllib.parse import urlparse
import logging
# A logger to report issues, similar to the real application.
LOGGER = logging.getLogger(__name__)
def is_url_safe(url: str) -> bool:
"""
This function represents the core of the security fix.
It checks if a URL's hostname resolves to a public, globally routable
IP address to prevent Server-Side Request Forgery (SSRF) attacks
against internal network services.
:param url: The URL to be validated.
:returns: `bool` of whether the URL is considered safe.
"""
try:
parsed_url = urlparse(url)
# Ensure the URL has a valid scheme and hostname
if not all([parsed_url.scheme, parsed_url.hostname]):
LOGGER.warning(f"URL missing scheme or hostname: {url}")
return False
# Resolve the hostname to an IP address
hostname = parsed_url.hostname
ip_addr = socket.gethostbyname(hostname)
ip_obj = ipaddress.ip_address(ip_addr)
# THE FIX: Check if the resolved IP address is a public address.
# This will return False for:
# - Private networks (e.g., 10.x.x.x, 192.168.x.x)
# - Loopback addresses (e.g., 127.0.0.1)
# - Link-local addresses (e.g., 169.254.x.x used by cloud metadata services)
if not ip_obj.is_global:
LOGGER.error(f"Unsafe URL detected: {url} resolves to non-global IP {ip_addr}")
return False
except (ValueError, socket.gaierror) as err:
LOGGER.error(f"Could not validate URL {url}: {err}")
return False
return True
def fixed_process_execution(data: dict):
"""
A simplified function demonstrating how the `is_url_safe` check
is used to patch the vulnerability.
"""
subscriber = data.get('subscriber', {})
url = subscriber.get('url')
if url:
# Applying the fix: The user-provided URL is validated before use.
# If the URL is unsafe, the function will not proceed to make a request.
if not is_url_safe(url):
# In a real application, this would return an HTTP 400 Bad Request.
msg = f"Error: Subscriber URL '{url}' is not safe and was rejected."
LOGGER.error(msg)
# Abort the operation
return
# If the check passes, the code can safely proceed.
LOGGER.info(f"URL '{url}' is safe. Proceeding with notification.")
# ... code to make the HTTP POST request would follow here ...
# e.g., requests.post(url, json={"status": "complete"})Payload
{
"inputs": {
"name": "hello"
},
"response": "document",
"subscriber": {
"url": "http://127.0.0.1:8000/internal-service"
}
}
Cite this entry
@misc{vaitp:cve202642352,
title = {{pygeoapi SSRF vulnerability allows requests to internal HTTP services.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-42352},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-42352/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.
The supreme art of war is to subdue the enemy without fighting.
Sun Tzu – “The Art of War”
:: Shaping the future through research and ingenuity ::
