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.
5690 vulnerabilities reference this CWE, most recent first.
GHSA-VF62-MR8Q-5X6J
Vulnerability from github – Published: 2026-01-22 18:30 – Updated: 2026-01-27 21:31Server-Side Request Forgery (SSRF) vulnerability in SmartDataSoft Electrician - Electrical Service WordPress electrician allows Server Side Request Forgery.This issue affects Electrician - Electrical Service WordPress: from n/a through <= 5.6.
{
"affected": [],
"aliases": [
"CVE-2026-22358"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2026-01-22T17:16:31Z",
"severity": "MODERATE"
},
"details": "Server-Side Request Forgery (SSRF) vulnerability in SmartDataSoft Electrician - Electrical Service WordPress electrician allows Server Side Request Forgery.This issue affects Electrician - Electrical Service WordPress: from n/a through \u003c= 5.6.",
"id": "GHSA-vf62-mr8q-5x6j",
"modified": "2026-01-27T21:31:43Z",
"published": "2026-01-22T18:30:39Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-22358"
},
{
"type": "WEB",
"url": "https://patchstack.com/database/Wordpress/Theme/electrician/vulnerability/wordpress-electrician-electrical-service-wordpress-theme-5-6-server-side-request-forgery-ssrf-vulnerability?_s_id=cve"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:L/I:L/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-VF62-Q3J8-CJ9G
Vulnerability from github – Published: 2026-07-19 00:30 – Updated: 2026-07-19 00:30A weakness has been identified in Sipeed PicoClaw up to 0.2.9. Impacted is the function isPrivateOrRestrictedIP of the file pkg/tools/integration/web.go of the component web_fetch. This manipulation causes server-side request forgery. The attack can be initiated remotely. The exploit has been made available to the public and could be used for attacks. Patch name: 2efbe5d560e7ed9bc5209c203dc4aa6ecdbc7405. To fix this issue, it is recommended to deploy a patch.
{
"affected": [],
"aliases": [
"CVE-2026-16196"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2026-07-18T23:17:01Z",
"severity": "LOW"
},
"details": "A weakness has been identified in Sipeed PicoClaw up to 0.2.9. Impacted is the function isPrivateOrRestrictedIP of the file pkg/tools/integration/web.go of the component web_fetch. This manipulation causes server-side request forgery. The attack can be initiated remotely. The exploit has been made available to the public and could be used for attacks. Patch name: 2efbe5d560e7ed9bc5209c203dc4aa6ecdbc7405. To fix this issue, it is recommended to deploy a patch.",
"id": "GHSA-vf62-q3j8-cj9g",
"modified": "2026-07-19T00:30:23Z",
"published": "2026-07-19T00:30:23Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-16196"
},
{
"type": "WEB",
"url": "https://github.com/sipeed/picoclaw/issues/3077"
},
{
"type": "WEB",
"url": "https://github.com/sipeed/picoclaw/pull/3085"
},
{
"type": "WEB",
"url": "https://github.com/sipeed/picoclaw/commit/2efbe5d560e7ed9bc5209c203dc4aa6ecdbc7405"
},
{
"type": "WEB",
"url": "https://github.com/sipeed/picoclaw"
},
{
"type": "WEB",
"url": "https://vuldb.com/cve/CVE-2026-16196"
},
{
"type": "WEB",
"url": "https://vuldb.com/submit/852963"
},
{
"type": "WEB",
"url": "https://vuldb.com/vuln/380012"
},
{
"type": "WEB",
"url": "https://vuldb.com/vuln/380012/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-VF76-XM6X-R4HV
Vulnerability from github – Published: 2026-09-09 15:35 – Updated: 2026-09-09 15:35Snipe-IT versions before 8.7.0 fail to HTML-escape the employee_num field in the acceptance PDF generator, allowing attackers with users.edit permission to inject img tags into TCPDF's writeHTML() function. Attackers can craft a malicious employee_num value containing an img tag with an arbitrary HTTP(S) URL to trigger server-side requests to internal services, cloud metadata endpoints, or external targets when a victim signs an asset acceptance.
{
"affected": [],
"aliases": [
"CVE-2026-86771"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2026-09-09T14:17:27Z",
"severity": "HIGH"
},
"details": "Snipe-IT versions before 8.7.0 fail to HTML-escape the employee_num field in the acceptance PDF generator, allowing attackers with users.edit permission to inject img tags into TCPDF\u0027s writeHTML() function. Attackers can craft a malicious employee_num value containing an img tag with an arbitrary HTTP(S) URL to trigger server-side requests to internal services, cloud metadata endpoints, or external targets when a victim signs an asset acceptance.",
"id": "GHSA-vf76-xm6x-r4hv",
"modified": "2026-09-09T15:35:14Z",
"published": "2026-09-09T15:35:14Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/grokability/snipe-it/security/advisories/GHSA-73xg-j94v-gjcf"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-86771"
},
{
"type": "WEB",
"url": "https://www.vulncheck.com/advisories/snipe-it-before-8.7.0-server-side-request-forgery-via-employee-num"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:N/A:L",
"type": "CVSS_V3"
},
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:H/VI:N/VA:L/SC:H/SI:N/SA:L/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-VFJ6-275Q-4PVM
Vulnerability from github – Published: 2019-10-25 13:55 – Updated: 2024-09-20 21:42Impact
send_email in graphite-web/webapp/graphite/composer/views.py in Graphite through 1.1.5 is vulnerable to SSRF. The vulnerable SSRF endpoint can be used by an attacker to have the Graphite web server request any resource. The response to this SSRF request is encoded into an image file and then sent to an e-mail address that can be supplied by the attacker. Thus, an attacker can exfiltrate any information. Email will be send through SMTP server configured in Graphite, by default it's 'localhost'
Patches
Problem was patched in Graphite-web 1.1.6. Also patches was released for graphite-web 1.0.x and 0.9.x, and we'll discuss releases of non-supported branches later.
Workarounds
You can manually remove function send_email from file webapp/graphite/composer/views.py. This function are not in use and will not affect your Graphite installation.
References
For more information check this graphite-web Github issue #2008
For more information
If you have any questions or comments about this advisory: * Add comment in issue #2008
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "graphite-web"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.1.6"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2017-18638"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2020-06-03T23:34:36Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "### Impact\nsend_email in graphite-web/webapp/graphite/composer/views.py in Graphite through 1.1.5 is vulnerable to SSRF. The vulnerable SSRF endpoint can be used by an attacker to have the Graphite web server request any resource. The response to this SSRF request is encoded into an image file and then sent to an e-mail address that can be supplied by the attacker. Thus, an attacker can exfiltrate any information. Email will be send through SMTP server configured in Graphite, by default it\u0027s \u0027localhost\u0027\n\n### Patches\nProblem was patched in Graphite-web 1.1.6. Also patches was released for graphite-web [1.0.x](https://github.com/graphite-project/graphite-web/pull/2501) and [0.9.x](https://github.com/graphite-project/graphite-web/pull/2500), and we\u0027ll discuss releases of non-supported branches later.\n\n### Workarounds\nYou can manually remove function `send_email` from file `webapp/graphite/composer/views.py`. This function are not in use and will not affect your Graphite installation.\n\n### References\nFor more information check this graphite-web Github issue #2008 \n\n### For more information\nIf you have any questions or comments about this advisory:\n* Add comment in [issue #2008](https://github.com/graphite-project/graphite-web/issues/2008)",
"id": "GHSA-vfj6-275q-4pvm",
"modified": "2024-09-20T21:42:27Z",
"published": "2019-10-25T13:55:20Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/graphite-project/graphite-web/security/advisories/GHSA-vfj6-275q-4pvm"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2017-18638"
},
{
"type": "WEB",
"url": "https://github.com/graphite-project/graphite-web/issues/2008"
},
{
"type": "WEB",
"url": "https://github.com/graphite-project/graphite-web/pull/2499"
},
{
"type": "WEB",
"url": "https://github.com/graphite-project/graphite-web/commit/71726a0e41a5263f49b973a7b856505a5b931c1f"
},
{
"type": "WEB",
"url": "https://blog.orange.tw/2017/07/how-i-chained-4-vulnerabilities-on.html#second-bug-internal-graphite-ssrf"
},
{
"type": "PACKAGE",
"url": "https://github.com/graphite-project/graphite-web"
},
{
"type": "WEB",
"url": "https://github.com/pypa/advisory-database/tree/main/vulns/graphite-web/PYSEC-2019-151.yaml"
},
{
"type": "WEB",
"url": "https://lists.debian.org/debian-lts-announce/2019/10/msg00030.html"
},
{
"type": "WEB",
"url": "https://www.youtube.com/watch?v=ds4Gp4xoaeA"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
},
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "graphite.composer.views.send_email vulnerable to SSRF"
}
GHSA-VFP7-JCG3-JHW8
Vulnerability from github – Published: 2026-09-08 18:32 – Updated: 2026-09-08 18:32Server-side request forgery (ssrf) in Microsoft Office SharePoint allows an authorized attacker to disclose information over a network.
{
"affected": [],
"aliases": [
"CVE-2026-69683"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2026-09-08T18:19:40Z",
"severity": "MODERATE"
},
"details": "Server-side request forgery (ssrf) in Microsoft Office SharePoint allows an authorized attacker to disclose information over a network.",
"id": "GHSA-vfp7-jcg3-jhw8",
"modified": "2026-09-08T18:32:53Z",
"published": "2026-09-08T18:32:52Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-69683"
},
{
"type": "WEB",
"url": "https://msrc.microsoft.com/update-guide/vulnerability/CVE-2026-69683"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-VFX9-PMH4-CQPQ
Vulnerability from github – Published: 2026-05-26 18:31 – Updated: 2026-05-26 18:31A vulnerability in the Google Cloud Apigee SetIntegrationRequest policy allowed remote attackers to perform Server-Side Request Forgery (SSRF) and exfiltrate service account access tokens.
For successful exploitation, an administrator must initially establish an insecure configuration of the API proxy.
{
"affected": [],
"aliases": [
"CVE-2026-2264"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2026-05-26T17:16:30Z",
"severity": "CRITICAL"
},
"details": "A vulnerability in the Google Cloud Apigee\u00a0SetIntegrationRequest\u00a0policy allowed remote attackers to perform Server-Side Request Forgery (SSRF) and exfiltrate service account access tokens.\n\nFor successful exploitation, an administrator must initially establish an insecure configuration of the API proxy.",
"id": "GHSA-vfx9-pmh4-cqpq",
"modified": "2026-05-26T18:31:45Z",
"published": "2026-05-26T18:31:45Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-2264"
},
{
"type": "WEB",
"url": "https://docs.cloud.google.com/apigee/docs/security-bulletins/security-bulletins#gcp-2026-034"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/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:Amber",
"type": "CVSS_V4"
}
]
}
GHSA-VG48-J87H-HC85
Vulnerability from github – Published: 2026-03-10 18:31 – Updated: 2026-03-10 18:31Server-side request forgery (ssrf) in Azure IoT Explorer allows an unauthorized attacker to perform spoofing over a network.
{
"affected": [],
"aliases": [
"CVE-2026-26121"
],
"database_specific": {
"cwe_ids": [
"CWE-20",
"CWE-918"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2026-03-10T18:18:41Z",
"severity": "HIGH"
},
"details": "Server-side request forgery (ssrf) in Azure IoT Explorer allows an unauthorized attacker to perform spoofing over a network.",
"id": "GHSA-vg48-j87h-hc85",
"modified": "2026-03-10T18:31:21Z",
"published": "2026-03-10T18:31:21Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-26121"
},
{
"type": "WEB",
"url": "https://msrc.microsoft.com/update-guide/vulnerability/CVE-2026-26121"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-VG6P-V9VM-6FGJ
Vulnerability from github – Published: 2026-08-25 14:43 – Updated: 2026-08-25 14:43The web_crawl tool performs its SSRF check only on the initial URL: it resolves the hostname once with socket.gethostbyname and rejects private/loopback/link-local results. It then passes the URL to a fetcher that uses httpx.Client(follow_redirects=True) - or urllib.request.urlopen when httpx is absent, which also follows redirects - and re-resolves the hostname at connect time, with no further validation. This validate-here/fetch-there gap is bypassable two independent ways: HTTP redirects and DNS rebinding.
Affected code: src/praisonai-agents/praisonaiagents/tools/web_crawl_tools.py - Single-shot validation (lines 229-238): if os.environ.get("ALLOW_LOCAL_CRAWL") != "true": ip_str = socket.gethostbyname(hostname) # resolved ONCE, at validation time ip = ipaddress.ip_address(ip_str) if ip.is_loopback or ip.is_private or ip.is_link_local or ip.is_multicast or ip.is_unspecified: continue # rejected url_list.append(u) - Vulnerable fetch (_crawl_with_httpx, lines 142 / 149): follows redirects, re-resolves DNS, no re-check: with httpx.Client(follow_redirects=True, timeout=30.0) as client: response = client.get(url) # fallback: urllib.request.urlopen(url, timeout=30) (also follows redirects by default) - web_crawl / crawl_web are registered tools (tools/init.py:156-157); httpx is the default fallback provider (dispatch at web_crawl_tools.py:269).
The two bypasses: 1) Redirect: validation approves an attacker domain resolving to a public IP; attacker server replies 302 Location: http://169.254.169.254/... (or any internal host); the fetcher follows it unchecked. 2) DNS rebinding (TOCTOU): validator's gethostbyname and fetcher's connect-time resolution are independent; a low-TTL attacker domain answers public to the validator and private/loopback to fetch.
Impact: An agent with web_crawl - driven by direct input or indirect prompt injection - can be made to read internal-only HTTP services and cloud instance-metadata endpoints (e.g. IAM credentials), with the response body returned in the tool output. Scope is Changed because the request pivots into the internal network.
Proof of concept: A PoC drives the real web_crawl() (httpx absent -> genuine urllib fallback). It runs a loopback "internal metadata" service and a loopback attacker redirector, substituting DNS only to stand in for "attacker owns a public domain" / offline routing - the redirect-following and connect-time re-resolution are the repo's own behavior. Observed: CONTROL: web_crawl("http://127.0.0.1:.../meta-data/") -> blocked (validator works) PoC 1A (redirect): attacker.example approved (public); 302 -> loopback metadata -> result.content leaks {"AccessKeyId":"ASIA_FAKE_STOLEN_CREDENTIAL_..."} PoC 1B (rebinding): gethostbyname(rebind.example)->public (allowed); connect->127.0.0.1 -> same secret leaked The control proves the validator blocks a direct loopback request, so the bypasses are genuine.
Remediation: Resolve the hostname once, validate that IP, and connect to that exact validated IP (pin it) rather than re-resolving. Disable redirect following (follow_redirects=False; for urllib use a redirect handler that re-validates), or re-validate every redirect hop's resolved IP. Apply the deny check to both the validator and the actual socket target. file_tools.py:364 already uses follow_redirects=False and is the correct pattern to propagate.
Distinct from prior advisories: The accepted SSRF advisories concern host-string parsing in different code — alternate loopback encodings in spider_tools (GHSA-5c6w-wwfq-7qqm) and the CLI @url feature (GHSA-5cxw-77wg-jrf3). This is in the web_crawl tool, which neither advisory names, and the mechanisms (redirect-following and DNS rebinding) differ categorically from host-string encoding; the spider_tools _host_is_blocked hardening does not apply to this tool.
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "praisonaiagents"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.6.58"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-55524"
],
"database_specific": {
"cwe_ids": [
"CWE-367",
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2026-08-25T14:43:19Z",
"nvd_published_at": "2026-08-05T20:17:10Z",
"severity": "HIGH"
},
"details": "The web_crawl tool performs its SSRF check only on the initial URL: it resolves the hostname once\nwith socket.gethostbyname and rejects private/loopback/link-local results. It then passes the URL to\na fetcher that uses httpx.Client(follow_redirects=True) - or urllib.request.urlopen when httpx is\nabsent, which also follows redirects - and re-resolves the hostname at connect time, with no further\nvalidation. This validate-here/fetch-there gap is bypassable two independent ways: HTTP redirects and\nDNS rebinding.\n\nAffected code: src/praisonai-agents/praisonaiagents/tools/web_crawl_tools.py\n- Single-shot validation (lines 229-238):\n if os.environ.get(\"ALLOW_LOCAL_CRAWL\") != \"true\":\n ip_str = socket.gethostbyname(hostname) # resolved ONCE, at validation time\n ip = ipaddress.ip_address(ip_str)\n if ip.is_loopback or ip.is_private or ip.is_link_local or ip.is_multicast or ip.is_unspecified:\n continue # rejected\n url_list.append(u)\n- Vulnerable fetch (_crawl_with_httpx, lines 142 / 149): follows redirects, re-resolves DNS, no re-check:\n with httpx.Client(follow_redirects=True, timeout=30.0) as client: response = client.get(url)\n # fallback: urllib.request.urlopen(url, timeout=30) (also follows redirects by default)\n- web_crawl / crawl_web are registered tools (tools/__init__.py:156-157); httpx is the default fallback\n provider (dispatch at web_crawl_tools.py:269).\n\nThe two bypasses:\n1) Redirect: validation approves an attacker domain resolving to a public IP; attacker server replies\n 302 Location: http://169.254.169.254/... (or any internal host); the fetcher follows it unchecked.\n2) DNS rebinding (TOCTOU): validator\u0027s gethostbyname and fetcher\u0027s connect-time resolution are\n independent; a low-TTL attacker domain answers public to the validator and private/loopback to fetch.\n\nImpact:\nAn agent with web_crawl - driven by direct input or indirect prompt injection - can be made to read\ninternal-only HTTP services and cloud instance-metadata endpoints (e.g. IAM credentials), with the\nresponse body returned in the tool output. Scope is Changed because the request pivots into the\ninternal network.\n\nProof of concept:\nA PoC drives the real web_crawl() (httpx absent -\u003e genuine urllib fallback). It runs a loopback\n\"internal metadata\" service and a loopback attacker redirector, substituting DNS only to stand in\nfor \"attacker owns a public domain\" / offline routing - the redirect-following and connect-time\nre-resolution are the repo\u0027s own behavior. Observed:\n CONTROL: web_crawl(\"http://127.0.0.1:.../meta-data/\") -\u003e blocked (validator works)\n PoC 1A (redirect): attacker.example approved (public); 302 -\u003e loopback metadata\n -\u003e result.content leaks {\"AccessKeyId\":\"ASIA_FAKE_STOLEN_CREDENTIAL_...\"}\n PoC 1B (rebinding): gethostbyname(rebind.example)-\u003epublic (allowed); connect-\u003e127.0.0.1\n -\u003e same secret leaked\nThe control proves the validator blocks a direct loopback request, so the bypasses are genuine.\n\nRemediation:\nResolve the hostname once, validate that IP, and connect to that exact validated IP (pin it) rather\nthan re-resolving. Disable redirect following (follow_redirects=False; for urllib use a redirect\nhandler that re-validates), or re-validate every redirect hop\u0027s resolved IP. Apply the deny check to\nboth the validator and the actual socket target. file_tools.py:364 already uses follow_redirects=False\nand is the correct pattern to propagate.\n\nDistinct from prior advisories:\nThe accepted SSRF advisories concern host-string parsing in different code \u2014 alternate loopback\nencodings in spider_tools (GHSA-5c6w-wwfq-7qqm) and the CLI @url feature (GHSA-5cxw-77wg-jrf3). This\nis in the web_crawl tool, which neither advisory names, and the mechanisms (redirect-following and DNS\nrebinding) differ categorically from host-string encoding; the spider_tools _host_is_blocked hardening\ndoes not apply to this tool.",
"id": "GHSA-vg6p-v9vm-6fgj",
"modified": "2026-08-25T14:43:19Z",
"published": "2026-08-25T14:43:19Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-vg6p-v9vm-6fgj"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-55524"
},
{
"type": "WEB",
"url": "https://github.com/MervinPraison/PraisonAI/commit/2f9677abb2ea68eab864ee8b6a828fd0141612e1"
},
{
"type": "PACKAGE",
"url": "https://github.com/MervinPraison/PraisonAI"
},
{
"type": "WEB",
"url": "https://github.com/MervinPraison/PraisonAI/releases/tag/v4.6.58"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:L/A:N",
"type": "CVSS_V3"
}
],
"summary": "praisonaiagents vulnerable to SSRF in web_crawl tool via redirect-following and DNS rebinding (validate-then-fetch gap)"
}
GHSA-VG6V-J97M-H5XQ
Vulnerability from github – Published: 2026-07-28 14:59 – Updated: 2026-07-28 14:59Hi Novu team,
Reporting an SSRF blocklist gap in the shared validateUrlSsrf guard. A complete self-contained reproduction is inlined below — copy the four files into a directory and run docker compose up, plus a single-file probe that runs against Node directly. Locally validated against HEAD 291817c.
Summary
Novu's shared SSRF guard validateUrlSsrf(url) is used before server-side requests to user-configured URLs. The guard resolves hostnames and blocks a regex list of private/reserved IP ranges, but it does not block 100.64.0.0/10 shared address space. As a result, Novu features protected by this guard can still send server-side requests to destinations such as 100.100.100.200 (Alibaba Cloud metadata service) and any other service reachable in 100.64.0.0/10.
Affected code
Guard:
libs/application-generic/src/utils/ssrf-url-validation.tsisPrivateIp(...)regex list at lines 9-28- DNS resolution and address validation at lines 55-72
Product call-sites:
- Workflow HTTP request step:
apps/worker/src/app/workflow/usecases/send-message/execute-http-request-step.usecase.ts— callsvalidateUrlSsrf(url)at line 149, then usesHttpClientServiceto send the request. - Webhook filter condition:
libs/application-generic/src/usecases/conditions-filter/conditions-filter.usecase.ts— callsvalidateUrlSsrf(child.webhookUrl)at line 265, then sendsaxios.post(child.webhookUrl, ...)at line 277.
HTTP client:
libs/application-generic/src/services/http-client/http-client.service.ts— usesgot(gotOptions)at lines 120 and 142 after the preflight validation.
Root cause
The SSRF guard uses a hand-written regex deny-list:
/^0\.0\.0\.0$/i,
/^127\./,
/^10\./,
/^172\.(1[6-9]|2[0-9]|3[01])\./,
/^192\.168\./,
/^169\.254\./,
/^::ffff:127\./i,
/^::ffff:10\./i,
/^::ffff:172\.(1[6-9]|2[0-9]|3[01])\./i,
/^::ffff:192\.168\./i,
/^::ffff:169\.254\./i,
/^::1$/,
/^fc00:/i,
/^fe80:/i,
This list omits 100.64.0.0/10, also called shared address space or CGNAT. These addresses are not RFC1918 private addresses, but they are also not normal public-internet destinations. Cloud and infrastructure providers commonly use special-use address ranges for metadata and internal services; Alibaba Cloud metadata is available at 100.100.100.200.
Reproduction — Part 1: unit-level probe (no Docker required)
Save the following file and run with node novu_ssrf_guard_probe.js. The script replicates validateUrlSsrf from libs/application-generic/src/utils/ssrf-url-validation.ts verbatim (the isPrivateIp regex list is copied as-is) and tests several URL categories.
novu_ssrf_guard_probe.js
const dns = require('dns/promises');
function isPrivateIp(ip) {
const privateRanges = [
/^0\.0\.0\.0$/i,
/^127\./,
/^10\./,
/^172\.(1[6-9]|2[0-9]|3[01])\./,
/^192\.168\./,
/^169\.254\./,
/^::ffff:127\./i,
/^::ffff:10\./i,
/^::ffff:172\.(1[6-9]|2[0-9]|3[01])\./i,
/^::ffff:192\.168\./i,
/^::ffff:169\.254\./i,
/^::1$/,
/^fc00:/i,
/^fe80:/i,
];
return privateRanges.some((range) => range.test(ip));
}
async function validateUrlSsrf(url) {
let parsed;
try {
parsed = new URL(url);
} catch {
return 'Invalid URL format.';
}
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
return `URL scheme "${parsed.protocol}" is not allowed.`;
}
const hostname = parsed.hostname.toLowerCase();
const blockedHostnames = ['localhost', 'metadata.google.internal'];
if (blockedHostnames.includes(hostname)) {
return `Requests to "${hostname}" are not allowed.`;
}
let addresses;
try {
addresses = await dns.lookup(hostname, { all: true });
} catch {
return `Unable to resolve hostname "${hostname}".`;
}
for (const { address } of addresses) {
if (isPrivateIp(address)) {
return `Requests to private or reserved IP addresses are not allowed (resolved: ${address}).`;
}
}
return null;
}
async function main() {
for (const url of [
'http://127.0.0.1/',
'http://0.0.0.0/',
'http://0.0.0.1/',
'http://169.254.169.254/',
'http://100.64.0.1/',
'http://100.100.100.200/',
'http://224.0.0.1/',
'http://[fd00::1]/',
'http://[64:ff9b::7f00:1]/',
'http://[::ffff:100.64.0.1]/',
'http://8.8.8.8/',
]) {
console.log(JSON.stringify({ url, verdict: (await validateUrlSsrf(url)) ?? 'ALLOW' }));
}
}
main().catch((e) => { console.error(e); process.exitCode = 1; });
Expected output (relevant lines)
{"url":"http://127.0.0.1/","verdict":"Requests to private or reserved IP addresses are not allowed (resolved: 127.0.0.1)."}
{"url":"http://169.254.169.254/","verdict":"Requests to private or reserved IP addresses are not allowed (resolved: 169.254.169.254)."}
{"url":"http://100.64.0.1/","verdict":"ALLOW"}
{"url":"http://100.100.100.200/","verdict":"ALLOW"}
{"url":"http://8.8.8.8/","verdict":"ALLOW"}
The 2nd and 3rd ALLOW rows are the bypass — both are non-public destinations the guard should refuse.
Reproduction — Part 2: end-to-end Docker CGNAT proof
Save the three files below into a directory, then:
docker compose up --abort-on-container-exit --exit-code-from novu-client
This mirrors the product sequence in execute-http-request-step.usecase.ts: resolve hostname → validate with validateUrlSsrf → send HTTP request. The "target" container is bound to a CGNAT address (100.64.0.20) on a custom subnet, simulando a cloud-internal service reachable on the CGNAT range.
docker-compose.yml
services:
cgnat-target:
image: python:3.12-alpine
command: python -u /srv/target.py
volumes:
- ./target.py:/srv/target.py:ro
networks:
novu-cgnat:
ipv4_address: 100.64.0.20
novu-client:
image: node:22-alpine
command: node /srv/client.js
volumes:
- ./client.js:/srv/client.js:ro
depends_on:
- cgnat-target
networks:
novu-cgnat:
ipv4_address: 100.64.0.10
networks:
novu-cgnat:
ipam:
config:
- subnet: 100.64.0.0/24
target.py
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
def do_POST(self):
print(f"[target] {self.client_address[0]} POST {self.path}", flush=True)
self.send_response(200)
self.send_header("content-type", "application/json")
self.end_headers()
self.wfile.write(b'{"marker":"NOVU_CGNAT_SSRF_OK"}\n')
def log_message(self, fmt, *args): return
HTTPServer(("100.64.0.20", 8080), Handler).serve_forever()
client.js
const dns = require('dns/promises');
function isPrivateIp(ip) {
const privateRanges = [
/^0\.0\.0\.0$/i, /^127\./, /^10\./,
/^172\.(1[6-9]|2[0-9]|3[01])\./, /^192\.168\./, /^169\.254\./,
/^::ffff:127\./i, /^::ffff:10\./i,
/^::ffff:172\.(1[6-9]|2[0-9]|3[01])\./i,
/^::ffff:192\.168\./i, /^::ffff:169\.254\./i,
/^::1$/, /^fc00:/i, /^fe80:/i,
];
return privateRanges.some((range) => range.test(ip));
}
async function validateUrlSsrf(url) {
const parsed = new URL(url);
if (!['http:', 'https:'].includes(parsed.protocol)) return 'bad scheme';
if (['localhost', 'metadata.google.internal'].includes(parsed.hostname.toLowerCase())) {
return 'blocked hostname';
}
const addresses = await dns.lookup(parsed.hostname, { all: true });
for (const { address } of addresses) {
if (isPrivateIp(address)) return `blocked ${address}`;
}
return null;
}
async function waitForTarget(url) {
for (let attempt = 0; attempt < 20; attempt += 1) {
try {
const r = await fetch(url, { method: 'POST' });
await r.text();
return;
} catch (_e) {
await new Promise((resolve) => setTimeout(resolve, 250));
}
}
}
async function main() {
const url = 'http://cgnat-target:8080/workflow-http-step';
const addresses = await dns.lookup('cgnat-target', { all: true });
const validation = await validateUrlSsrf(url);
console.log(JSON.stringify({ url, addresses, validation: validation ?? 'ALLOW' }));
if (validation) { process.exitCode = 2; return; }
await waitForTarget(url);
const response = await fetch(url, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ source: 'novu-http-request-step' }),
});
const body = await response.text();
console.log(JSON.stringify({ status: response.status, body }));
}
main().catch((e) => { console.error(e); process.exitCode = 1; });
Expected output
novu-client-1 | {"url":"http://cgnat-target:8080/workflow-http-step","addresses":[{"address":"100.64.0.20","family":4}],"validation":"ALLOW"}
cgnat-target-1 | [target] 100.64.0.10 POST /workflow-http-step
novu-client-1 | {"status":200,"body":"{\"marker\":\"NOVU_CGNAT_SSRF_OK\"}\n"}
The chain is:
- Resolve hostname
cgnat-target→100.64.0.20(a CGNAT address). - Run Novu's
validateUrlSsrfagainst the URL — returnsALLOWbecause100.64.0.0/10is missing fromisPrivateIp. - Send the actual server-side HTTP POST → reaches the CGNAT-bound target → response with marker
NOVU_CGNAT_SSRF_OKis received.
Impact
Any Novu feature that allows a user to configure an outbound HTTP URL and relies on validateUrlSsrf may still reach 100.64.0.0/10. Impact is highest for:
- Alibaba Cloud deployments, where
http://100.100.100.200/latest/meta-data/may expose instance metadata. - Self-hosted deployments where
100.64.0.0/10routes to private infrastructure, service meshes, VPNs, carrier-grade NAT, or provider-side internal services. - Multi-tenant deployments where one tenant can configure workflow HTTP request steps or webhook filters that execute from shared worker/API infrastructure — cross-tenant SSRF primitive into provider-internal services.
Suggested remediation
- Replace regex matching with IP parsing and CIDR classification, e.g. using
ipaddr.jswithprocess(...)to normalize IPv4-mapped IPv6. - Treat only globally reachable public IPs as allowed by default (
addr.range() === 'unicast'after IPv4-mapped unwrap, or equivalent). - Explicitly deny all special-use ranges, including at least:
0.0.0.0/8,10.0.0.0/8,100.64.0.0/10,127.0.0.0/8,169.254.0.0/16,172.16.0.0/12,192.168.0.0/16- multicast (
224.0.0.0/4), documentation (192.0.2.0/24,198.51.100.0/24,203.0.113.0/24,2001:db8::/32), benchmarking (198.18.0.0/15), reserved (240.0.0.0/4) - IPv6 ULA (
fc00::/7), link-local (fe80::/10), loopback (::1), and the IPv4-mapped variants of all of the above - Add regression tests for:
100.64.0.1,100.100.100.200- hostnames resolving to those addresses
- IPv4-mapped variants of denied IPv4 ranges (e.g.,
::ffff:100.64.0.1) - Consider connection-time validation or a guarded lookup agent so the actual request cannot resolve to a different IP than the preflight checked (DNS-rebinding TOCTOU mitigation).
Notes
This report is intentionally scoped to the concrete 100.64.0.0/10 bypass. Additional missed ranges exist in the current regex guard (multicast 224.0.0.0/4, broadcast 255.255.255.255, benchmarking, documentation, 0.0.0.0/8 outside /32, and IPv4-mapped variants), but CGNAT is the highest-confidence real-world issue because it includes a known cloud metadata endpoint (100.100.100.200 on Alibaba Cloud).
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "@novu/application-generic"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "3.17.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-28T14:59:22Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "Hi Novu team,\n\nReporting an SSRF blocklist gap in the shared `validateUrlSsrf` guard. A complete self-contained reproduction is inlined below \u2014 copy the four files into a directory and run `docker compose up`, plus a single-file probe that runs against Node directly. Locally validated against HEAD `291817c`.\n\n## Summary\n\nNovu\u0027s shared SSRF guard `validateUrlSsrf(url)` is used before server-side requests to user-configured URLs. The guard resolves hostnames and blocks a regex list of private/reserved IP ranges, but it does not block `100.64.0.0/10` shared address space. As a result, Novu features protected by this guard can still send server-side requests to destinations such as `100.100.100.200` (Alibaba Cloud metadata service) and any other service reachable in `100.64.0.0/10`.\n\n## Affected code\n\nGuard:\n\n- `libs/application-generic/src/utils/ssrf-url-validation.ts`\n - `isPrivateIp(...)` regex list at lines 9-28\n - DNS resolution and address validation at lines 55-72\n\nProduct call-sites:\n\n- Workflow HTTP request step: `apps/worker/src/app/workflow/usecases/send-message/execute-http-request-step.usecase.ts` \u2014 calls `validateUrlSsrf(url)` at line 149, then uses `HttpClientService` to send the request.\n- Webhook filter condition: `libs/application-generic/src/usecases/conditions-filter/conditions-filter.usecase.ts` \u2014 calls `validateUrlSsrf(child.webhookUrl)` at line 265, then sends `axios.post(child.webhookUrl, ...)` at line 277.\n\nHTTP client:\n\n- `libs/application-generic/src/services/http-client/http-client.service.ts` \u2014 uses `got(gotOptions)` at lines 120 and 142 after the preflight validation.\n\n## Root cause\n\nThe SSRF guard uses a hand-written regex deny-list:\n\n```ts\n/^0\\.0\\.0\\.0$/i,\n/^127\\./,\n/^10\\./,\n/^172\\.(1[6-9]|2[0-9]|3[01])\\./,\n/^192\\.168\\./,\n/^169\\.254\\./,\n/^::ffff:127\\./i,\n/^::ffff:10\\./i,\n/^::ffff:172\\.(1[6-9]|2[0-9]|3[01])\\./i,\n/^::ffff:192\\.168\\./i,\n/^::ffff:169\\.254\\./i,\n/^::1$/,\n/^fc00:/i,\n/^fe80:/i,\n```\n\nThis list omits `100.64.0.0/10`, also called shared address space or CGNAT. These addresses are not RFC1918 private addresses, but they are also not normal public-internet destinations. Cloud and infrastructure providers commonly use special-use address ranges for metadata and internal services; **Alibaba Cloud metadata is available at `100.100.100.200`**.\n\n## Reproduction \u2014 Part 1: unit-level probe (no Docker required)\n\nSave the following file and run with `node novu_ssrf_guard_probe.js`. The script replicates `validateUrlSsrf` from `libs/application-generic/src/utils/ssrf-url-validation.ts` **verbatim** (the `isPrivateIp` regex list is copied as-is) and tests several URL categories.\n\n### `novu_ssrf_guard_probe.js`\n\n```javascript\nconst dns = require(\u0027dns/promises\u0027);\n\nfunction isPrivateIp(ip) {\n const privateRanges = [\n /^0\\.0\\.0\\.0$/i,\n /^127\\./,\n /^10\\./,\n /^172\\.(1[6-9]|2[0-9]|3[01])\\./,\n /^192\\.168\\./,\n /^169\\.254\\./,\n /^::ffff:127\\./i,\n /^::ffff:10\\./i,\n /^::ffff:172\\.(1[6-9]|2[0-9]|3[01])\\./i,\n /^::ffff:192\\.168\\./i,\n /^::ffff:169\\.254\\./i,\n /^::1$/,\n /^fc00:/i,\n /^fe80:/i,\n ];\n return privateRanges.some((range) =\u003e range.test(ip));\n}\n\nasync function validateUrlSsrf(url) {\n let parsed;\n try {\n parsed = new URL(url);\n } catch {\n return \u0027Invalid URL format.\u0027;\n }\n if (parsed.protocol !== \u0027http:\u0027 \u0026\u0026 parsed.protocol !== \u0027https:\u0027) {\n return `URL scheme \"${parsed.protocol}\" is not allowed.`;\n }\n const hostname = parsed.hostname.toLowerCase();\n const blockedHostnames = [\u0027localhost\u0027, \u0027metadata.google.internal\u0027];\n if (blockedHostnames.includes(hostname)) {\n return `Requests to \"${hostname}\" are not allowed.`;\n }\n let addresses;\n try {\n addresses = await dns.lookup(hostname, { all: true });\n } catch {\n return `Unable to resolve hostname \"${hostname}\".`;\n }\n for (const { address } of addresses) {\n if (isPrivateIp(address)) {\n return `Requests to private or reserved IP addresses are not allowed (resolved: ${address}).`;\n }\n }\n return null;\n}\n\nasync function main() {\n for (const url of [\n \u0027http://127.0.0.1/\u0027,\n \u0027http://0.0.0.0/\u0027,\n \u0027http://0.0.0.1/\u0027,\n \u0027http://169.254.169.254/\u0027,\n \u0027http://100.64.0.1/\u0027,\n \u0027http://100.100.100.200/\u0027,\n \u0027http://224.0.0.1/\u0027,\n \u0027http://[fd00::1]/\u0027,\n \u0027http://[64:ff9b::7f00:1]/\u0027,\n \u0027http://[::ffff:100.64.0.1]/\u0027,\n \u0027http://8.8.8.8/\u0027,\n ]) {\n console.log(JSON.stringify({ url, verdict: (await validateUrlSsrf(url)) ?? \u0027ALLOW\u0027 }));\n }\n}\n\nmain().catch((e) =\u003e { console.error(e); process.exitCode = 1; });\n```\n\n### Expected output (relevant lines)\n\n```json\n{\"url\":\"http://127.0.0.1/\",\"verdict\":\"Requests to private or reserved IP addresses are not allowed (resolved: 127.0.0.1).\"}\n{\"url\":\"http://169.254.169.254/\",\"verdict\":\"Requests to private or reserved IP addresses are not allowed (resolved: 169.254.169.254).\"}\n{\"url\":\"http://100.64.0.1/\",\"verdict\":\"ALLOW\"}\n{\"url\":\"http://100.100.100.200/\",\"verdict\":\"ALLOW\"}\n{\"url\":\"http://8.8.8.8/\",\"verdict\":\"ALLOW\"}\n```\n\nThe 2nd and 3rd `ALLOW` rows are the bypass \u2014 both are non-public destinations the guard should refuse.\n\n## Reproduction \u2014 Part 2: end-to-end Docker CGNAT proof\n\nSave the three files below into a directory, then:\n\n```bash\ndocker compose up --abort-on-container-exit --exit-code-from novu-client\n```\n\nThis mirrors the product sequence in `execute-http-request-step.usecase.ts`: resolve hostname \u2192 validate with `validateUrlSsrf` \u2192 send HTTP request. The \"target\" container is bound to a CGNAT address (`100.64.0.20`) on a custom subnet, simulando a cloud-internal service reachable on the CGNAT range.\n\n### `docker-compose.yml`\n\n```yaml\nservices:\n cgnat-target:\n image: python:3.12-alpine\n command: python -u /srv/target.py\n volumes:\n - ./target.py:/srv/target.py:ro\n networks:\n novu-cgnat:\n ipv4_address: 100.64.0.20\n\n novu-client:\n image: node:22-alpine\n command: node /srv/client.js\n volumes:\n - ./client.js:/srv/client.js:ro\n depends_on:\n - cgnat-target\n networks:\n novu-cgnat:\n ipv4_address: 100.64.0.10\n\nnetworks:\n novu-cgnat:\n ipam:\n config:\n - subnet: 100.64.0.0/24\n```\n\n### `target.py`\n\n```python\nfrom http.server import BaseHTTPRequestHandler, HTTPServer\n\nclass Handler(BaseHTTPRequestHandler):\n def do_POST(self):\n print(f\"[target] {self.client_address[0]} POST {self.path}\", flush=True)\n self.send_response(200)\n self.send_header(\"content-type\", \"application/json\")\n self.end_headers()\n self.wfile.write(b\u0027{\"marker\":\"NOVU_CGNAT_SSRF_OK\"}\\n\u0027)\n def log_message(self, fmt, *args): return\n\nHTTPServer((\"100.64.0.20\", 8080), Handler).serve_forever()\n```\n\n### `client.js`\n\n```javascript\nconst dns = require(\u0027dns/promises\u0027);\n\nfunction isPrivateIp(ip) {\n const privateRanges = [\n /^0\\.0\\.0\\.0$/i, /^127\\./, /^10\\./,\n /^172\\.(1[6-9]|2[0-9]|3[01])\\./, /^192\\.168\\./, /^169\\.254\\./,\n /^::ffff:127\\./i, /^::ffff:10\\./i,\n /^::ffff:172\\.(1[6-9]|2[0-9]|3[01])\\./i,\n /^::ffff:192\\.168\\./i, /^::ffff:169\\.254\\./i,\n /^::1$/, /^fc00:/i, /^fe80:/i,\n ];\n return privateRanges.some((range) =\u003e range.test(ip));\n}\n\nasync function validateUrlSsrf(url) {\n const parsed = new URL(url);\n if (![\u0027http:\u0027, \u0027https:\u0027].includes(parsed.protocol)) return \u0027bad scheme\u0027;\n if ([\u0027localhost\u0027, \u0027metadata.google.internal\u0027].includes(parsed.hostname.toLowerCase())) {\n return \u0027blocked hostname\u0027;\n }\n const addresses = await dns.lookup(parsed.hostname, { all: true });\n for (const { address } of addresses) {\n if (isPrivateIp(address)) return `blocked ${address}`;\n }\n return null;\n}\n\nasync function waitForTarget(url) {\n for (let attempt = 0; attempt \u003c 20; attempt += 1) {\n try {\n const r = await fetch(url, { method: \u0027POST\u0027 });\n await r.text();\n return;\n } catch (_e) {\n await new Promise((resolve) =\u003e setTimeout(resolve, 250));\n }\n }\n}\n\nasync function main() {\n const url = \u0027http://cgnat-target:8080/workflow-http-step\u0027;\n const addresses = await dns.lookup(\u0027cgnat-target\u0027, { all: true });\n const validation = await validateUrlSsrf(url);\n console.log(JSON.stringify({ url, addresses, validation: validation ?? \u0027ALLOW\u0027 }));\n\n if (validation) { process.exitCode = 2; return; }\n\n await waitForTarget(url);\n const response = await fetch(url, {\n method: \u0027POST\u0027,\n headers: { \u0027content-type\u0027: \u0027application/json\u0027 },\n body: JSON.stringify({ source: \u0027novu-http-request-step\u0027 }),\n });\n const body = await response.text();\n console.log(JSON.stringify({ status: response.status, body }));\n}\n\nmain().catch((e) =\u003e { console.error(e); process.exitCode = 1; });\n```\n\n### Expected output\n\n```\nnovu-client-1 | {\"url\":\"http://cgnat-target:8080/workflow-http-step\",\"addresses\":[{\"address\":\"100.64.0.20\",\"family\":4}],\"validation\":\"ALLOW\"}\ncgnat-target-1 | [target] 100.64.0.10 POST /workflow-http-step\nnovu-client-1 | {\"status\":200,\"body\":\"{\\\"marker\\\":\\\"NOVU_CGNAT_SSRF_OK\\\"}\\n\"}\n```\n\nThe chain is:\n\n1. Resolve hostname `cgnat-target` \u2192 `100.64.0.20` (a CGNAT address).\n2. Run Novu\u0027s `validateUrlSsrf` against the URL \u2014 returns `ALLOW` because `100.64.0.0/10` is missing from `isPrivateIp`.\n3. Send the actual server-side HTTP POST \u2192 reaches the CGNAT-bound target \u2192 response with marker `NOVU_CGNAT_SSRF_OK` is received.\n\n## Impact\n\nAny Novu feature that allows a user to configure an outbound HTTP URL and relies on `validateUrlSsrf` may still reach `100.64.0.0/10`. Impact is highest for:\n\n- **Alibaba Cloud deployments**, where `http://100.100.100.200/latest/meta-data/` may expose instance metadata.\n- **Self-hosted deployments** where `100.64.0.0/10` routes to private infrastructure, service meshes, VPNs, carrier-grade NAT, or provider-side internal services.\n- **Multi-tenant deployments** where one tenant can configure workflow HTTP request steps or webhook filters that execute from shared worker/API infrastructure \u2014 cross-tenant SSRF primitive into provider-internal services.\n\n## Suggested remediation\n\n- Replace regex matching with IP parsing and CIDR classification, e.g. using `ipaddr.js` with `process(...)` to normalize IPv4-mapped IPv6.\n- Treat only globally reachable public IPs as allowed by default (`addr.range() === \u0027unicast\u0027` after IPv4-mapped unwrap, or equivalent).\n- Explicitly deny all special-use ranges, including at least:\n - `0.0.0.0/8`, `10.0.0.0/8`, `100.64.0.0/10`, `127.0.0.0/8`, `169.254.0.0/16`, `172.16.0.0/12`, `192.168.0.0/16`\n - multicast (`224.0.0.0/4`), documentation (`192.0.2.0/24`, `198.51.100.0/24`, `203.0.113.0/24`, `2001:db8::/32`), benchmarking (`198.18.0.0/15`), reserved (`240.0.0.0/4`)\n - IPv6 ULA (`fc00::/7`), link-local (`fe80::/10`), loopback (`::1`), and the IPv4-mapped variants of all of the above\n- Add regression tests for:\n - `100.64.0.1`, `100.100.100.200`\n - hostnames resolving to those addresses\n - IPv4-mapped variants of denied IPv4 ranges (e.g., `::ffff:100.64.0.1`)\n- Consider connection-time validation or a guarded lookup agent so the actual request cannot resolve to a different IP than the preflight checked (DNS-rebinding TOCTOU mitigation).\n\n## Notes\n\nThis report is intentionally scoped to the concrete `100.64.0.0/10` bypass. Additional missed ranges exist in the current regex guard (multicast `224.0.0.0/4`, broadcast `255.255.255.255`, benchmarking, documentation, `0.0.0.0/8` outside `/32`, and IPv4-mapped variants), but CGNAT is the highest-confidence real-world issue because it includes a known cloud metadata endpoint (`100.100.100.200` on Alibaba Cloud).",
"id": "GHSA-vg6v-j97m-h5xq",
"modified": "2026-07-28T14:59:22Z",
"published": "2026-07-28T14:59:22Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/novuhq/novu/security/advisories/GHSA-vg6v-j97m-h5xq"
},
{
"type": "PACKAGE",
"url": "https://github.com/novuhq/novu"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "@novu/application-generic: `validateUrlSsrf` permits CGNAT (100.64.0.0/10) destinations \u2014 affects Workflow HTTP request step + Webhook filter condition"
}
GHSA-VG8M-4P2Q-GCJH
Vulnerability from github – Published: 2026-08-21 00:31 – Updated: 2026-08-21 00:31SitemapLoader.parse_sitemap in langchain_community/document_loaders/sitemap.py applies the documented restrict_to_same_domain control only to leaf url entries. The loop over url elements filters cross-domain locations, but the loop over nested sitemap elements passes the child loc straight to self.scrape_all([loc.text], "xml"), which reaches WebBaseLoader.scrape_all and an aiohttp GET, with no domain comparison and no check for private, loopback or link-local destinations. An attacker who controls or influences an ingested sitemap can therefore point a nested sitemap entry at an internal address and make the server fetch it even when the deploying application set restrict_to_same_domain to True specifically to confine outbound requests. The fetched content is parsed and surfaces in the returned Documents, so internal responses are disclosed to the caller rather than merely requested.
{
"affected": [],
"aliases": [
"CVE-2026-72848"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2026-08-20T22:18:05Z",
"severity": "HIGH"
},
"details": "SitemapLoader.parse_sitemap in langchain_community/document_loaders/sitemap.py applies the documented restrict_to_same_domain control only to leaf url entries. The loop over url elements filters cross-domain locations, but the loop over nested sitemap elements passes the child loc straight to self.scrape_all([loc.text], \"xml\"), which reaches WebBaseLoader.scrape_all and an aiohttp GET, with no domain comparison and no check for private, loopback or link-local destinations. An attacker who controls or influences an ingested sitemap can therefore point a nested sitemap entry at an internal address and make the server fetch it even when the deploying application set restrict_to_same_domain to True specifically to confine outbound requests. The fetched content is parsed and surfaces in the returned Documents, so internal responses are disclosed to the caller rather than merely requested.",
"id": "GHSA-vg8m-4p2q-gcjh",
"modified": "2026-08-21T00:31:23Z",
"published": "2026-08-21T00:31:23Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-72848"
},
{
"type": "WEB",
"url": "https://github.com/langchain-ai/langchain/issues/38814"
},
{
"type": "WEB",
"url": "https://github.com/langchain-ai/langchain-community"
},
{
"type": "WEB",
"url": "https://github.com/langchain-ai/langchain-community/blob/main/libs/community/langchain_community/document_loaders/sitemap.py"
},
{
"type": "WEB",
"url": "https://www.vulncheck.com/advisories/langchain-community-sitemaploader-does-not-apply-restrict-to-same-domain-to-nested-sitemap-index-entries-allowing-server-side-request-forgery"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N",
"type": "CVSS_V3"
},
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:H/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"
}
]
}
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.