SSRF: DNS pinning is never applied (WordPress ignores $args['curl']) #3

Closed
opened 2026-10-16 17:51:15 -07:00 by eric · 0 comments
Owner

Found in security audit run 3 (2026-10-16, 06d630d). Severity: low.

Problem

Apprise_Client::make_request() tries to pin the validated IP by setting $args['curl'][ CURLOPT_RESOLVE ] (includes/class-apprise-client.php:237), then calls wp_remote_post() with the hostname URL (:239, :261).

WordPress core never reads a curl key. WP_Http::request() builds the Requests options without it (checked against 7.1.3), so the pin is silently dropped and cURL looks the hostname up again when it connects. The IP that was validated and the IP actually contacted can therefore differ. Anyone controlling DNS for the configured host can rebind it, or publish a public A record plus an internal or loopback AAAA record, and get past the private/reserved-IP check. resolve_hostname() checks only the first A record, while cURL prefers IPv6.

The same root cause makes sanitize_curl_options() dead code: filter-injected cURL options were never applied either.

Reproduced locally: the real client's arguments, sent through unmodified WP_Http, fail with "Could not resolve host". The same pin applied through http_api_curl connects.

Fix

  • Apply CURLOPT_RESOLVE to the real handle from the http_api_curl action. Scope it to this request's URL, register it at PHP_INT_MAX, and remove_action after wp_remote_post returns.
  • Keep the IP-URL fallback for the non-cURL HTTP path, which is already pinned correctly.
  • Remove the dead $args['curl'] handling, or move sanitize_curl_options() to act on the handle in the same callback.
  • Fix the docblock at :183-186, which promises pinning that doesn't happen.
  • Regression test: make_request() registers an http_api_curl callback that sets the validated host:port:ip.
Found in security audit run 3 (2026-10-16, `06d630d`). Severity: **low**. ## Problem `Apprise_Client::make_request()` tries to pin the validated IP by setting `$args['curl'][ CURLOPT_RESOLVE ]` (`includes/class-apprise-client.php:237`), then calls `wp_remote_post()` with the hostname URL (`:239`, `:261`). WordPress core never reads a `curl` key. `WP_Http::request()` builds the Requests options without it (checked against 7.1.3), so the pin is silently dropped and cURL looks the hostname up again when it connects. The IP that was validated and the IP actually contacted can therefore differ. Anyone controlling DNS for the configured host can rebind it, or publish a public A record plus an internal or loopback AAAA record, and get past the private/reserved-IP check. `resolve_hostname()` checks only the first A record, while cURL prefers IPv6. The same root cause makes `sanitize_curl_options()` dead code: filter-injected cURL options were never applied either. Reproduced locally: the real client's arguments, sent through unmodified `WP_Http`, fail with "Could not resolve host". The same pin applied through `http_api_curl` connects. ## Fix - Apply `CURLOPT_RESOLVE` to the real handle from the `http_api_curl` action. Scope it to this request's URL, register it at `PHP_INT_MAX`, and `remove_action` after `wp_remote_post` returns. - Keep the IP-URL fallback for the non-cURL HTTP path, which is already pinned correctly. - Remove the dead `$args['curl']` handling, or move `sanitize_curl_options()` to act on the handle in the same callback. - Fix the docblock at `:183-186`, which promises pinning that doesn't happen. - Regression test: `make_request()` registers an `http_api_curl` callback that sets the validated `host:port:ip`.
eric closed this issue 2026-10-16 21:45:17 -07:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
eric/apprise-wordpress-notifications#3
No description provided.