Skip to content
40

40

github.com September 17, 2026

Two related missing authorization checks in the webhook subsystem combine to produce a high-severity SSRF vulnerability:

POST /webhook/config/save does not verify that sourceId (the form key) belongs to the calling user. Any authenticated user can associate an attacker-controlled webhook URL with any other user's form.

POST /webhook/config/sendTest does not validate the URL in the submitted WebhookConfigEntity at all, and uses the caller-supplied URL directly to make an outbound HTTP request (via WebhookSenderService.sendHttp() ), with no SSRF protections (no IP allowlist, no DNS rebinding protection, no scheme restriction).

When a new submission arrives on UserA's form, the server POSTs the full JSON payload -- including all submitted PII -- to the attacker-registered URL, exfiltrating data and allowing the attacker to port-scan and interact with internal services.

Vulnerable endpoint 1 -- webhook save ( tduck-webhook/src/main/java/com/tduck/cloud/webhook/controller/WebhookConfigController.java , lines ~38-43):

Vulnerable endpoint 2 -- test send ( WebhookConfigController.java , lines ~48-56):

HTTP sender with no SSRF guard ( tduck-webhook/src/main/java/com/tduck/cloud/webhook/service/sender/WebhookSenderService.java , lines ~66-85):

The PII payload sent to the attacker URL contains the complete serialized form submission including all field values, IP address, timestamps, and user metadata.

A running TDuck instance.

UserA (form owner) has a published form with formKey FORM_KEY_A .

UserB: any other registered user account.

Attacker controls a server at .

Step 1 -- UserB registers attacker URL as webhook for UserA's form (no ownership check):

Step 2 -- Any submission to UserA's form triggers the server to POST the full data to attacker URL:

Step 3 -- Attacker receives (on ATTACKER_IP:PORT):

Observed output from live validation (server logs from tduckcloud/tduck-platform docker):

The ConnectException: Connection refused error on port 29876 confirms the server made a real TCP dial to the attacker-controlled host. The PII payload in event_data confirms what would have been sent in the body.

Internal network scanning: By providing url values pointing to internal RFC-1918 addresses (e.g., , ) and observing whether the webhook event's last_error contains Connection refused (port closed) vs. ConnectException: Connection timed out or success, an attacker can map internal services. In a cloud/Kubernetes deployment this enables metadata service access (e.g., ).

An attacker with any authenticated account can:

Exfiltrate all future PII submissions to any form in the system -- names, emails, medical data, HR responses -- by registering their webhook endpoint for the target form.

Port-scan internal network : the error response distinguishes open, closed, and filtered ports on internal hosts.

Interact with internal services : send arbitrary POST bodies to internal HTTP endpoints (Redis, Elasticsearch, microservices) using the submission data as the body.

No administrative privileges are required. Self-registration is sufficient if enabled.