# RCA Report: CVE-2026-57947 — Pinpoint Webhook SSRF

## Summary

Pinpoint 3.1.0 allows any authenticated user (minimum `ROLE_USER`) to register arbitrary webhook URLs through the `/api/webhook` and `/api/application/webhook` endpoints. The URL validation in `WebhookController.validateURL()` only checks that the supplied string is syntactically a valid URL (`new URL(...).toURI()`), with no filtering of private IP ranges, loopback addresses, or link-local metadata endpoints. The same lack of SSRF protection exists in the batch alarm sender (`WebhookSenderImpl.validateURL()`). When an alarm rule fires, the Pinpoint batch server performs a real HTTP POST to every registered webhook URL, so an attacker can coerce the server into sending requests to internal services such as `127.0.0.1` or `169.254.169.254`.

## Impact

- **Package/component affected:** `maven:com.navercorp.pinpoint:pinpoint-web` (also `pinpoint-batch`, which contains the alarm sender)
- **Affected versions:** Pinpoint through 3.1.0 (per CVE record and VulnCheck advisory)
- **Risk level and consequences:** Medium-to-high. A authenticated user can force the Pinpoint server to issue server-side requests to internal network resources, including cloud metadata endpoints (e.g., AWS IMDSv1 `http://169.254.169.254/`). The webhook payload contains user-group member details, so a controlled internal endpoint can also exfiltrate names, e-mails, and phone numbers. This enables internal port scanning, metadata credential theft, and lateral movement from the Pinpoint server IP.

## Impact Parity

- **Disclosed/claimed maximum impact:** SSRF — the server makes unauthorized outbound HTTP requests to internal/attacker-controlled URLs when alarm thresholds are breached.
- **Reproduced impact from this run:** The reproduction started the real `pinpointdocker/pinpoint-web:3.1.0` container, authenticated as a non-admin user, registered webhooks pointing to `http://169.254.169.254/...`, `http://127.0.0.1/...`, and a controlled internal listener (`http://ssrf-listener:9999/callback`). It then started the real `pinpointdocker/pinpoint-batch:3.1.0` alarm job and observed the batch server make a real HTTP POST to the controlled listener.
- **Parity:** `full` for the SSRF claim — the server-side request was demonstrated end-to-end across the real product.
- **Not demonstrated:** Actual extraction of cloud metadata credentials, because no live metadata service was present in the sandbox. The listener stands in for the attacker-controlled internal endpoint and proves the server-side request path.

## Root Cause

The vulnerable code is in the webhook module of Pinpoint 3.1.0:

- `webhook/src/main/java/com/navercorp/pinpoint/web/webhook/controller/WebhookController.java` (lines 117–120):
  ```java
  private void validateURL(Webhook webhook) throws MalformedURLException, URISyntaxException {
      URL u = new URL(webhook.getUrl());
      webhook.setUrl(u.toURI().toString());
  }
  ```
  This only validates URL syntax. It does **not** reject `127.0.0.1`, `169.254.169.254`, `10.0.0.0/8`, etc.

- `batch-alarmsender/src/main/java/com/navercorp/pinpoint/batch/alarm/sender/WebhookSenderImpl.java` (lines 132–135):
  ```java
  private String validateURL(String url) throws MalformedURLException, URISyntaxException {
      URL u = new URL(url);
      return u.toURI().toString();
  }
  ```
  The same missing checks are applied before the alarm sender calls `restTemplate.exchange(validatedUrl, HttpMethod.POST, httpEntity, String.class)`.

There is no post-DNS guard, no IP allowlist/denylist, and no redirect protection. Any scheme/host/address accepted by `java.net.URL` is accepted by the application.

The issue was disclosed as GitHub issue #13857 ("Webhook URL missing SSRF protection allows authenticated users to reach internal network resources"). A patched version is not explicitly stated in the advisory, but the issue is closed against the 3.1.1 milestone.

## Reproduction Steps

The full reproduction is captured in `bundle/repro/reproduction_steps.sh`. At a high level it:

1. Brings up the real Pinpoint backend (`pinpoint-hbase:3.1.0`, MySQL, ZooKeeper, Redis) and the vulnerable `pinpoint-web:3.1.0` container with basic authentication enabled.
2. Inserts a fake application row into the HBase `ApplicationIndex` table so the alarm batch job recognises the target application.
3. Starts a controlled internal HTTP listener (`ssrf-listener`) inside the same Docker network.
4. Authenticates as `testuser` (a user with only `ROLE_USER`) and registers three webhooks:
   - `http://169.254.169.254/latest/meta-data/iam/security-credentials/`
   - `http://127.0.0.1:9999/ssrf-poc`
   - `http://ssrf-listener:9999/callback` (via the `/api/application/webhook` variant)
5. Verifies that all three internal URLs are stored in the database via `GET /api/webhook?applicationId=<app>`.
6. Authenticates as `adminuser`, creates a user group, and creates an alarm rule (`TOTAL COUNT`, threshold `0`) linked to the listener webhook with `webhookSend=true`.
7. Starts the real `pinpoint-batch:3.1.0` container with `alarmJob` enabled and waits for the batch server to deliver a POST to the controlled listener.

Expected evidence:
- Registration responses contain `{"result":"SUCCESS"}` and the stored webhook list contains the internal URLs.
- `bundle/logs/ssrf-listener.log` shows a POST request from `Apache-HttpClient/5.4.4 (Java/17.0.19)` to `/callback`.
- `bundle/logs/ppssrf-batch.log` shows `Successfully sent webhook : Webhook{...url='http://ssrf-listener:9999/callback'...}`.

## Evidence

- `bundle/logs/reproduction_steps.log` — full console output of the reproduction, including 401 for unauthenticated access, successful login, webhook registrations, and the listener receiving the POST.
- `bundle/logs/ppssrf-web.log` — Pinpoint Web startup logs showing the active Spring profiles (`release`, `basicLogin`) and the vulnerable `webhookEnable=true` configuration.
- `bundle/logs/ppssrf-batch.log` — batch alarm job logs showing the `TOTAL COUNT` checker detecting the alarm and the successful webhook send:
  ```
  ResponseCountChecker result is true for application (test-app-...). value is 0. (threshold : 0).
  Successfully sent webhook : Webhook{webhookId='12'...url='http://ssrf-listener:9999/callback'...}
  ```
- `bundle/logs/ssrf-listener.log` — the controlled listener records the server-side POST:
  ```
  LISTENER_POST path=/callback headers={...'User-Agent': 'Apache-HttpClient/5.4.4 (Java/17.0.19)'...}
  LISTENER "POST /callback HTTP/1.1" 200 -
  ```
- `bundle/repro/artifacts/stored_webhooks.json` — JSON list of stored webhooks, including `http://169.254.169.254/...` and `http://127.0.0.1:9999/ssrf-poc`.
- `bundle/repro/artifacts/register_metadata.json`, `register_loopback.json`, `register_listener.json` — each contains `{"result":"SUCCESS"}`.
- `bundle/repro/artifacts/create_alarm.json` — alarm rule created with `webhookSend=true` and linked to the listener webhook.

## Recommendations / Next Steps

1. **Fix approach:** Add a strict URL validator that rejects private/reserved/loopback/link-local IP addresses and metadata endpoints both at registration time and in the batch sender. A robust fix should also:
   - Resolve the hostname to IP addresses and check the resolved set against an allowlist/denylist (post-DNS guard).
   - Disable HTTP redirects or validate redirect targets.
   - Optionally require an explicit allowlist of webhook domains/IPs configurable by the operator.
2. **Upgrade guidance:** Upgrade to a patched release > 3.1.0 once available. Until then, restrict access to the `/api/webhook` and `/api/alarmRule` endpoints and apply network-level egress controls on the Pinpoint server.
3. **Testing recommendations:** Regression tests should assert that `127.0.0.1`, `169.254.169.254`, `10.0.0.0/8`, `192.168.0.0/16`, `172.16.0.0/12`, and `::1` are rejected by the webhook registration endpoint, and that the alarm sender does not send to those addresses even when already stored in the database.

## Additional Notes

- **Idempotency:** The script uses a unique timestamp suffix for the application name, user group, and HBase row qualifier, so repeated runs do not collide with leftover MySQL/HBase state. Two consecutive executions with `REUSE_STACK=1` both completed successfully.
- **Limitations:** The reproduction seeds a fake `ApplicationIndex` row in HBase so the alarm batch job recognises the target application. In a real deployment this row is created automatically when a Pinpoint agent reports for the application. Seeding the row is a sandbox-only convenience and does not change the vulnerability path.
- **Clean-run note:** A full clean run requires several minutes for the HBase container to bootstrap. The script is designed to perform that bootstrap automatically when `REUSE_STACK` is not set.
