flawopen.com/AI Security/wazuh-mcp-server-webshell-check-ssrf-ghsa-pw2j-pj4h-f5vg
Wazuh MCP SSRF in blueteam_check_webshell (GHSA-pw2j-pj4h-f5vg)
A hostname bypassed Wazuh-MCP-Server’s literal-IP check before a forensic tool fetched it. The GHSA assigns High severity and CWE-918; no CVE is assigned. Fixed in commit 2bbfe12.[1], [2], [3]
A receptionist checks that a visitor’s badge is not already on a blacklist, then lets the visitor choose which office to enter. Checking only the printed badge misses a visitor whose name points to a restricted office. The fix checks where the name resolves and makes the connection use that checked destination.[1], [3]
Core Concepts & Subsystem Terms
Server-side request forgery (SSRF)- A server makes a network request to a destination influenced by its caller. If destination checks are incomplete, the server may reach networks the caller cannot reach directly.[4], [1]
DNS resolution and rebinding- A hostname is translated to one or more IP addresses. If validation checks only the hostname text, or the HTTP client resolves it again later, the address used for the connection may not be the address that was checked.[1], [3]
Destination pinning- The validated IP address is bound to the outgoing connection while the hostname remains available for HTTP host and TLS verification. This closes the validation-to-connection gap for that request.[3]
Framework mappings
- CWE reference for the weakness described in this guide: CWE-918
- D3-OTF Outbound Traffic Filtering — This guide's outbound network restriction limits which destinations the service or host can reach.
Step-by-Step Attack Flow
An incident-derived URL reaches the checker
The affected blueteam_check_webshell tool accepts a URL so an analyst can inspect a suspected webshell address from forensic data. The advisory scopes this finding to that tool, not every Wazuh-MCP-Server capability.[1]
The HTTP client resolves and follows the destination
The validated URL was later passed to curl, which performed its own DNS lookup. The advisory also documents automatic redirect following without re-validating each destination. This creates SSRF exposure when the service can reach an internal address and the tool is callable with an attacker-influenced URL. The actual impact depends on network reachability and tool access.[1], [3]
The patch validates and pins resolved addresses
Commit 2bbfe12 resolves both IPv4 and IPv6 records, rejects non-public addresses unless the hostname is explicitly configured in ALLOWED_INTERNAL_DOMAINS, and pins the selected address for the request. The patched fetch path does not automatically follow redirects. The GHSA assigns High severity and CWE-918, reports no CVE, and lists no package version range.[1], [3]
Source Code: Flaw vs. Secure Implementation
The vulnerable excerpt reproduces the relevant WebshellCheckInput.validate_url logic from commit 6bd1fa8; surrounding imports and class fields are omitted. It rejects literal non-public IPs, but a hostname raises ValueError, is treated as a domain, and passes without DNS resolution. The hardened excerpt reproduces _pinned_curl_args from fix commit 2bbfe12. The patched validator also calls _host_pins; the fetch loop supplies these --resolve arguments to curl and validates each redirect destination before the next hop. The patch allows internal destinations only for explicitly configured domains. These are focused excerpts, not a drop-in replacement for the full URL-fetching path.[2], [3]
Source excerpt: mcp_server/tools/webshell_check.py — WebshellCheckInput.validate_url (6bd1fa8) and _pinned_curl_args (2bbfe12)
@field_validator("url")
@classmethod
def validate_url(cls, v: str) -> str:
v = v.strip()
if not v.startswith(("http://", "https://")):
raise ValueError(f"URL must start with http:// or https://, got: {v!r}")
# Reject private/reserved IPs in URL host
from urllib.parse import urlparse
import ipaddress
host = urlparse(v).hostname
if host:
try:
ip = ipaddress.ip_address(host)
except ValueError:
ip = None # not an IP - domain name, allowed
if ip is not None and (ip.is_private or ip.is_loopback or ip.is_reserved or ip.is_link_local):
raise ValueError(f"URL host is a private/reserved IP: {host}. This tool only accepts public hosts.")
return v
def _pinned_curl_args(url: str) -> tuple[list[str], str | None]:
"""Build curl --resolve args for an already-validated destination."""
import ipaddress
u = urlparse(url)
host = u.hostname
if not host:
return [], "URL has no host."
try:
ipaddress.ip_address(host)
return [], None # Literal IP: no DNS lookup to rebind.
except ValueError:
pass
port = u.port or (443 if u.scheme == "https" else 80)
ips, err = _host_pins(host, _allowed_internal_domains())
if err:
return [], err
args: list[str] = []
for ip in ips:
addr = f"[{ip}]" if ":" in ip else ip
args += ["--resolve", f"{host}:{port}:{addr}"]
return args, None
Engineering & System Hardening Checklist
- ✓Identify deployments containing the affected
blueteam_check_webshellimplementation; the advisory gives commits, not a package version range.[1], [2] - ✓Update to commit
2bbfe12or a later release that includes the fix, and verify the deployed source actually contains it.[3] - ✓For each outbound URL fetch, resolve every A and AAAA result and reject the request if any address is non-public, unless a narrowly scoped internal-domain exception is explicitly required.[3]
- ✓Bind the validated address to the actual connection to prevent a second DNS lookup from changing the destination after validation.[3]
- ✓Disable automatic redirects or validate and pin every redirect hop; do not assume the original URL check protects a later destination.[1], [3]
- ✓Restrict access to the MCP tool, limit the server's outbound network reach, and review whether untrusted users or agent content can supply its URL argument.[1]
- ✓Add regression tests for hostname resolution, mixed public and non-public DNS answers, IPv4 and IPv6, configured internal-domain exceptions, redirects, and connection pinning without making requests to real internal services.[3]
Sources
- Wazuh-MCP-Server security advisory GHSA-pw2j-pj4h-f5vg (no CVE assigned)
- Vulnerable source at commit 6bd1fa8 — WebshellCheckInput.validate_url
- Wazuh-MCP-Server fix commit 2bbfe12
- MITRE CWE-918: Server-Side Request Forgery
- The Next Web: Google, JPMorgan and others fixed separate MCP SSRF findings
- Ars Technica: Vulnerability in agents from Google and others exposes structural flaw in MCP