GitHub Advisoryadvisory · npm

GHSA-5vf6-jrqr-78fj 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

MEDIUM Published 2026-08-21 Modified 2026-08-21 Official page ↗
Aliases
CVE-2026-63004
Ecosystem
npm
Packages
unleash-server
Severity
Moderate
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:L/I:L/A:N
Weaknesses
CWE-918
Reviewed
2026-08-21

Affected packages

• npm unleash-server: < 7.5.2 → fixed in 7.5.2 • npm unleash-server: >= 7.6.0, < 7.6.5 → fixed in 7.6.5 • npm unleash-server: >= 8.0.0, < 8.0.2 → fixed in 8.0.2

Description

Summary 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:/…` (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`): ```ts async fetchRetry( url: string, options: any = {}, retries: number = 1, ): Promise { 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`): ```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 ```bash 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. ```ts 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: …

Related

Alias · CVE (1)

  • CVE-2026-63004 Unleash is an open-source feature management platform. vulnerability

Weakness · CWE (1)

  • CWE-918 Server-Side Request Forgery weakness

References