{"repro_id":"REPRO-2026-00253","version":6,"title":"Pinpoint through 3.1.0 allows authenticated users to register internal webhook URLs, leading to SSRF when alarm webhooks are delivered.","repro_type":"security","status":"published","severity":"medium","cvss_score":6.3,"description":"Pinpoint through 3.1.0 allows authenticated users to register webhook URLs that resolve to internal hosts or metadata endpoints. The webhook registration endpoint performs only syntactic URL validation and does not block private/loopback/link-local ranges. When an alarm fires, the server issues an outbound HTTP POST to each registered URL, enabling SSRF to internal resources and potential data exposure.","root_cause":"# RCA Report: CVE-2026-57947 — Pinpoint Webhook SSRF\n\n## Summary\n\nPinpoint 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`.\n\n## Impact\n\n- **Package/component affected:** `maven:com.navercorp.pinpoint:pinpoint-web` (also `pinpoint-batch`, which contains the alarm sender)\n- **Affected versions:** Pinpoint through 3.1.0 (per CVE record and VulnCheck advisory)\n- **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.\n\n## Impact Parity\n\n- **Disclosed/claimed maximum impact:** SSRF — the server makes unauthorized outbound HTTP requests to internal/attacker-controlled URLs when alarm thresholds are breached.\n- **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.\n- **Parity:** `full` for the SSRF claim — the server-side request was demonstrated end-to-end across the real product.\n- **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.\n\n## Root Cause\n\nThe vulnerable code is in the webhook module of Pinpoint 3.1.0:\n\n- `webhook/src/main/java/com/navercorp/pinpoint/web/webhook/controller/WebhookController.java` (lines 117–120):\n  ```java\n  private void validateURL(Webhook webhook) throws MalformedURLException, URISyntaxException {\n      URL u = new URL(webhook.getUrl());\n      webhook.setUrl(u.toURI().toString());\n  }\n  ```\n  This only validates URL syntax. It does **not** reject `127.0.0.1`, `169.254.169.254`, `10.0.0.0/8`, etc.\n\n- `batch-alarmsender/src/main/java/com/navercorp/pinpoint/batch/alarm/sender/WebhookSenderImpl.java` (lines 132–135):\n  ```java\n  private String validateURL(String url) throws MalformedURLException, URISyntaxException {\n      URL u = new URL(url);\n      return u.toURI().toString();\n  }\n  ```\n  The same missing checks are applied before the alarm sender calls `restTemplate.exchange(validatedUrl, HttpMethod.POST, httpEntity, String.class)`.\n\nThere 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.\n\nThe 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.\n\n## Reproduction Steps\n\nThe full reproduction is captured in `bundle/repro/reproduction_steps.sh`. At a high level it:\n\n1. 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.\n2. Inserts a fake application row into the HBase `ApplicationIndex` table so the alarm batch job recognises the target application.\n3. Starts a controlled internal HTTP listener (`ssrf-listener`) inside the same Docker network.\n4. Authenticates as `testuser` (a user with only `ROLE_USER`) and registers three webhooks:\n   - `http://169.254.169.254/latest/meta-data/iam/security-credentials/`\n   - `http://127.0.0.1:9999/ssrf-poc`\n   - `http://ssrf-listener:9999/callback` (via the `/api/application/webhook` variant)\n5. Verifies that all three internal URLs are stored in the database via `GET /api/webhook?applicationId=<app>`.\n6. Authenticates as `adminuser`, creates a user group, and creates an alarm rule (`TOTAL COUNT`, threshold `0`) linked to the listener webhook with `webhookSend=true`.\n7. 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.\n\nExpected evidence:\n- Registration responses contain `{\"result\":\"SUCCESS\"}` and the stored webhook list contains the internal URLs.\n- `bundle/logs/ssrf-listener.log` shows a POST request from `Apache-HttpClient/5.4.4 (Java/17.0.19)` to `/callback`.\n- `bundle/logs/ppssrf-batch.log` shows `Successfully sent webhook : Webhook{...url='http://ssrf-listener:9999/callback'...}`.\n\n## Evidence\n\n- `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.\n- `bundle/logs/ppssrf-web.log` — Pinpoint Web startup logs showing the active Spring profiles (`release`, `basicLogin`) and the vulnerable `webhookEnable=true` configuration.\n- `bundle/logs/ppssrf-batch.log` — batch alarm job logs showing the `TOTAL COUNT` checker detecting the alarm and the successful webhook send:\n  ```\n  ResponseCountChecker result is true for application (test-app-...). value is 0. (threshold : 0).\n  Successfully sent webhook : Webhook{webhookId='12'...url='http://ssrf-listener:9999/callback'...}\n  ```\n- `bundle/logs/ssrf-listener.log` — the controlled listener records the server-side POST:\n  ```\n  LISTENER_POST path=/callback headers={...'User-Agent': 'Apache-HttpClient/5.4.4 (Java/17.0.19)'...}\n  LISTENER \"POST /callback HTTP/1.1\" 200 -\n  ```\n- `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`.\n- `bundle/repro/artifacts/register_metadata.json`, `register_loopback.json`, `register_listener.json` — each contains `{\"result\":\"SUCCESS\"}`.\n- `bundle/repro/artifacts/create_alarm.json` — alarm rule created with `webhookSend=true` and linked to the listener webhook.\n\n## Recommendations / Next Steps\n\n1. **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:\n   - Resolve the hostname to IP addresses and check the resolved set against an allowlist/denylist (post-DNS guard).\n   - Disable HTTP redirects or validate redirect targets.\n   - Optionally require an explicit allowlist of webhook domains/IPs configurable by the operator.\n2. **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.\n3. **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.\n\n## Additional Notes\n\n- **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.\n- **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.\n- **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.\n","cve_id":"CVE-2026-57947","cwe_id":"CWE-918","source_url":"https://github.com/pinpoint-apm/pinpoint/issues/13857 (researcher disclosure), https://www.vulncheck.com/advisories/pinpoint-server-side-request-forgery-via-alarm-webhook-registration, https://www.cve.org/CVERecord?id=CVE-2026-57947","package":{"name":"pinpoint-apm/pinpoint","ecosystem":"maven","affected_versions":"<= 3.1.0 (affected from 0 through 3.1.0)","fixed_version":"Not specified in advisory; GitHub issue milestone 3.1.1; no fixed commit identified"},"reproduced_at":"2026-07-06T09:03:21.246679+00:00","duration_secs":3553.0,"tool_calls":338,"handoffs":2,"total_cost_usd":7.794113920000003,"agent_costs":{"hypothesis_generator":0.00987715,"judge":0.248923,"repro":6.087322580000002,"support":0.04882153,"vuln_variant":1.39916966},"cost_breakdown":{"hypothesis_generator":{"accounts/fireworks/models/kimi-k2p7-code":0.00987715},"judge":{"gpt-5.5":0.248923},"repro":{"accounts/fireworks/models/kimi-k2p7-code":6.087322580000002},"support":{"accounts/fireworks/models/kimi-k2p7-code":0.04882153},"vuln_variant":{"accounts/fireworks/models/kimi-k2p7-code":1.39916966}},"quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-07-06T09:03:45.828961+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":18078,"category":"reproduction_script"},{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":8886,"category":"analysis"},{"path":"bundle/vuln_variant/reproduction_steps.sh","filename":"reproduction_steps.sh","size":13959,"category":"reproduction_script"},{"path":"bundle/vuln_variant/rca_report.md","filename":"rca_report.md","size":9357,"category":"analysis"},{"path":"bundle/artifact_promotion_manifest.json","filename":"artifact_promotion_manifest.json","size":8521,"category":"other"},{"path":"bundle/vuln_variant/source_identity.json","filename":"source_identity.json","size":902,"category":"other"},{"path":"bundle/vuln_variant/root_cause_equivalence.json","filename":"root_cause_equivalence.json","size":1621,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":775,"category":"other"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":1103,"category":"other"},{"path":"bundle/logs/reproduction_steps.log","filename":"reproduction_steps.log","size":8835,"category":"log"},{"path":"bundle/logs/ppssrf-web.log","filename":"ppssrf-web.log","size":168926,"category":"log"},{"path":"bundle/logs/ppssrf-batch.log","filename":"ppssrf-batch.log","size":199898,"category":"log"},{"path":"bundle/logs/ssrf-listener.log","filename":"ssrf-listener.log","size":780,"category":"log"},{"path":"bundle/logs/vuln_variant/harness.log","filename":"harness.log","size":3699,"category":"log"},{"path":"bundle/logs/vuln_variant/reproduction_steps.log","filename":"reproduction_steps.log","size":21404,"category":"log"},{"path":"bundle/vuln_variant/patch_analysis.md","filename":"patch_analysis.md","size":11088,"category":"documentation"},{"path":"bundle/vuln_variant/variant_manifest.json","filename":"variant_manifest.json","size":4241,"category":"other"},{"path":"bundle/vuln_variant/validation_verdict.json","filename":"validation_verdict.json","size":1113,"category":"other"},{"path":"bundle/vuln_variant/runtime_manifest.json","filename":"runtime_manifest.json","size":1457,"category":"other"},{"path":"bundle/logs/vuln_variant/compile.log","filename":"compile.log","size":0,"category":"log"},{"path":"bundle/vuln_variant/artifacts/source_identity.txt","filename":"source_identity.txt","size":156,"category":"other"},{"path":"bundle/vuln_variant/artifacts/src/webhook/support/WebhookUrlValidator.java","filename":"WebhookUrlValidator.java","size":9468,"category":"other"},{"path":"bundle/vuln_variant/artifacts/src/batch/alarm/sender/WebhookDnsResolver.java","filename":"WebhookDnsResolver.java","size":2165,"category":"other"},{"path":"bundle/vuln_variant/artifacts/src/test/VariantBypassHarness.java","filename":"VariantBypassHarness.java","size":8641,"category":"other"}]}