Blind SSRF via Unauthenticated Socket.IO in Ground Station
CVE-2026-53983: Unauthenticated blind SSRF in Ground Station <0.6.0 lets attackers reach internal services and cloud metadata endpoints with no credentials required.
Overview
CVE-2026-53983 is an unauthenticated blind Server-Side Request Forgery (SSRF) vulnerability affecting Ground Station prior to version 0.6.0. The flaw sits at the intersection of two independently dangerous design decisions: a Socket.IO endpoint that enforces no authentication whatsoever, and a URL ingestion pipeline that applies no scheme, host, or network-range validation before passing attacker-supplied values to Python’s requests.get. The result is a primitive that lets any anonymous network client coerce the ground-station process into issuing arbitrary outbound HTTP requests — including requests to cloud instance metadata services such as http://169.254.169.254.
What makes this vulnerability operationally significant beyond a standard SSRF is its persistence model. The attacker-supplied URL is written to the application database and replayed automatically every 24 hours by the scheduled orbital sync cycle. The attacker does not need to maintain a connection or issue repeat commands; a single two-event exchange is sufficient to establish a durable, recurring exfiltration primitive that survives application restarts. This changes the threat model from an ephemeral probe to a persistent foothold in the network fabric of whatever environment hosts the application.
Ground Station is satellite ground-control software likely deployed in research, academic, and small-to-medium aerospace operator contexts. Any deployment exposing port 7000 to an untrusted network — including cloud VMs with overly permissive security groups — is directly affected.
Technical Analysis
The vulnerability chains three root causes that individually would each be a finding in their own right.
Root Cause 1 — Missing authentication on the Socket.IO server. The Socket.IO server on port 7000 is configured with a wildcard CORS policy and no authentication middleware. Any client that can reach the port is treated as a fully trusted peer with access to all registered event handlers.
Root Cause 2 — Unsanitised URL persistence via data_submission. The data_submission event handler accepts a JSON payload containing a submit-orbital-sources action. The supplied URL is written directly to the database with no validation:
# backend/tlesync/source_adapters.py (VULNERABLE — pre-0.6.0)
@sio.on("data_submission")
def handle_data_submission(sid, data):
action = data.get("action")
if action == "submit-orbital-sources":
url = data.get("url") # ← fully attacker-controlled
db.session.add(OrbitalSource(url=url))
db.session.commit()
sio.emit("submission_ack", {"status": "ok"})
# ...
def _fetch_http_3le(source):
resp = requests.get(source.url, timeout=10) # ← no scheme/host check
return resp.text
def _fetch_http_omm(source):
resp = requests.get(source.url, timeout=10) # ← same pattern
return resp.text
No scheme allowlist (http, https only), no rejection of RFC 1918 ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), no rejection of loopback (127.0.0.1), and no rejection of link-local (169.254.169.254) are present anywhere in the pipeline.
Root Cause 3 — Out-of-band oracle via orbital_sync_state. Although the raw HTTP response body is not reflected, the sync task emits HTTP status codes and error messages to all connected Socket.IO clients via the orbital_sync_state event. An attacker monitoring this event stream can distinguish 200 OK from 401 Unauthorized, 403 Forbidden, 404 Not Found, and connection errors — a serviceable oracle for mapping internal services.
Attack sequence:
# Step 1 — Connect anonymously
wscat -c ws://target:7000/socket.io/?EIO=4&transport=websocket
# Step 2 — Persist a malicious orbital source pointing at cloud metadata
emit("data_submission", {
"action": "submit-orbital-sources",
"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
})
# Step 3 — Trigger an immediate sync (also unauthenticated)
emit("background_task:start", {"task": "orbital_sync"})
# Step 4 — Observe the oracle
listen("orbital_sync_state") # HTTP status + error text from metadata endpoint
Because the URL persists in the database, the sync re-fires every 24 hours with no further attacker interaction required.
Impact
An attacker with network access to port 7000 can pivot to any HTTP-speaking service reachable from the ground-station host. In cloud environments, the most immediate consequence is the ability to query the Instance Metadata Service (IMDS) at 169.254.169.254. On AWS, even without IMDSv2 enforcement, successful probing of the IAM credentials endpoint can reveal temporary access keys associated with the attached instance role, escalating SSRF to full cloud account compromise. On GCP and Azure, equivalent metadata paths expose OAuth tokens and subscription data.
Within the internal network, the status-code oracle enables port scanning and service fingerprinting of otherwise unreachable hosts. Internal APIs, admin panels, and unauthenticated microservices that rely on network isolation as their sole access control are all exposed. The CVSS 8.6 score (Network/Low/None/None — High Confidentiality, Low Integrity, Low Availability impact) reflects the ease of exploitation but understates the compounding risk of the persistence mechanism, which is not captured in a single-event CVSS calculation.
How to Fix It
The patch in commit 2ecde82 addresses all three root causes. Operators should upgrade to Ground Station 0.6.0 immediately.
1. Enforce authentication on Socket.IO connections.
# FIXED: reject unauthenticated connections at the connect event
@sio.on("connect")
def on_connect(sid, environ, auth):
token = (auth or {}).get("token")
if not validate_token(token):
raise ConnectionRefusedError("authentication required")
2. Validate URLs before persistence — scheme allowlist and network-range rejection.
import ipaddress
from urllib.parse import urlparse
ALLOWED_SCHEMES = {"http", "https"}
FORBIDDEN_NETWORKS = [
ipaddress.ip_network("127.0.0.0/8"),
ipaddress.ip_network("10.0.0.0/8"),
ipaddress.ip_network("172.16.0.0/12"),
ipaddress.ip_network("192.168.0.0/16"),
ipaddress.ip_network("169.254.0.0/16"), # link-local / cloud IMDS
ipaddress.ip_network("::1/128"),
ipaddress.ip_network("fc00::/7"),
]
def validate_orbital_url(url: str) -> str:
parsed = urlparse(url)
if parsed.scheme not in ALLOWED_SCHEMES:
raise ValueError(f"Scheme '{parsed.scheme}' is not permitted")
try:
addr = ipaddress.ip_address(parsed.hostname)
for net in FORBIDDEN_NETWORKS:
if addr in net:
raise ValueError(f"Address {addr} targets a forbidden network range")
except ValueError as exc:
# hostname is a domain name — resolve and re-check (DNS rebinding prevention)
import socket
for _, _, _, _, sockaddr in socket.getaddrinfo(parsed.hostname, None):
ip = ipaddress.ip_address(sockaddr[0])
for net in FORBIDDEN_NETWORKS:
if ip in net:
raise ValueError(f"Resolved address {ip} is in a forbidden range") from exc
return url
3. Restrict the CORS policy to known trusted origins rather than the wildcard default.
Our Take
This vulnerability is a textbook illustration of how “internal-only” assumptions collapse the moment a single network boundary is breached or misconfigured. The SSRF itself is unremarkable — requests.get(user_input) without validation has been a well-understood anti-pattern for over a decade. What is notable is the combination of zero authentication on a real-time event bus and a persistence mechanism that transforms a one-shot probe into a recurring exfiltration job. Neither the SSRF nor the missing authentication is subtle; both would be caught by a competent code review or a basic DAST scan. Their coexistence suggests that security was not part of the development process at all.
For enterprises deploying open-source components in operational or cloud environments, this case reinforces that network isolation is not a substitute for application-layer authentication. Any service that can initiate outbound requests and accepts input from the network must validate that input as rigorously as a public-facing API.
Detection with SAST
This vulnerability class maps to CWE-918 (Server-Side Request Forgery) and CWE-306 (Missing Authentication for Critical Function). SAST detection focuses on two patterns:
- Taint tracking from Socket.IO event payloads to HTTP client sinks. Any data path where a value extracted from a
sio.onhandler reachesrequests.get,requests.post,urllib.request.urlopen,httpx.get, or equivalent without an intervening sanitiser should be flagged as a critical taint flow. - Authentication bypass detection. SAST rules should model the Socket.IO connection lifecycle and flag
@sio.on("connect")handlers that do not raiseConnectionRefusedErroror an equivalent rejection on failed authentication checks, particularly when combined with wildcard CORS configuration in the server instantiation call.
Offensive360’s engine additionally flags ipaddress or URL-parsing logic that handles the scheme but omits RFC 1918 and link-local range checks, a common partial-fix pattern that leaves cloud metadata endpoints reachable.
References
Detect this vulnerability class in your codebase
Offensive360 SAST scans your source code for CVE-2026-53983-class vulnerabilities and thousands of other patterns — across 60+ languages.