The Exploit
Authenticated Contributor-level attacker.
curl -i -X POST 'https://TARGET/wp-admin/admin-ajax.php' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data 'action=smack_upload&securekey=NONCE_VALUE&url=https://bit.ly/3ABCdef'
If the Bitly redirect resolves to an internal destination, the WordPress server will follow the shortlink and issue an HTTP request to that final URL. The plugin returns its normal AJAX JSON response, while the attacker can verify SSRF by observing the internal service receiving the request.
What the Patch Did
Before
check_ajax_referer('smack-ultimate-csv-importer', 'securekey');
$file_url = esc_url_raw($_POST['url']);
$file_url = wp_http_validate_url($file_url);
$media_type = '';
...
if(strstr($file_url, 'https://bit.ly/')){
$file_url = $this->unshorten_bitly_url($file_url);
}
After
check_ajax_referer('smack-ultimate-csv-importer', 'securekey');
$file_url = esc_url_raw($_POST['url']);
$file_url = wp_http_validate_url($file_url);
$host = wp_parse_url($file_url, PHP_URL_HOST);
$ip = gethostbyname($host);
if (!filter_var(
$ip,
FILTER_VALIDATE_IP,
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
)) {
$response['success'] = false;
$response['message'] = 'Download Failed. Invalid or restricted URL destination.';
echo wp_json_encode($response);
die();
}
...
if (strstr($file_url, 'https://bit.ly/')) {
$file_url = $this->unshorten_bitly_url($file_url);
$file_url = wp_http_validate_url($file_url);
if (!$file_url) {
$response['success'] = false;
$response['message'] = 'Download Failed. Resolved URL is not valid.';
echo wp_json_encode($response);
die();
}
$host = wp_parse_url($file_url, PHP_URL_HOST);
$ip = gethostbyname($host);
if (!filter_var(
$ip,
FILTER_VALIDATE_IP,
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
)) {
$response['success'] = false;
$response['message'] = 'Download Failed. Invalid or restricted URL destination.';
echo wp_json_encode($response);
die();
}
}
The patch adds destination IP validation using filter_var() with FILTER_FLAG_NO_PRIV_RANGE and FILTER_FLAG_NO_RES_RANGE, and it re-validates the resolved URL after the Bitly redirect with wp_http_validate_url().
Root Cause
This is a server-side request forgery issue (CWE-918). The attacker-controlled value enters via $_POST['url'], is cleaned with esc_url_raw() and initially validated with wp_http_validate_url(), then flows to unshorten_bitly_url() when the input contains https://bit.ly/. The trust boundary crossed is the transition from a user-supplied shortlink to the final URL destination: the plugin validated only the original Bitly URL, not the URL returned by the redirect resolution, allowing internal/private endpoints to be contacted.
Why It Works
The single load-bearing fix is the second validation of the resolved URL and IP after unshorten_bitly_url(). Without wp_http_validate_url() and the follow-up filter_var(... FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) on the expanded URL, a Bitly redirect to http://127.0.0.1 or http://169.254.169.254 would still bypass the initial checks. The earlier host/IP validation on the original URL is useful for direct URLs, but it is the post-unshortening re-check that blocks the bypass.
Hardening Checklist
- Validate every URL immediately before fetching with
wp_http_validate_url(). - After resolving redirects or shortlinks, re-run validation on the final destination.
- Use
wp_parse_url()plusgethostbyname(), thenfilter_var(..., FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)to block private and reserved IP ranges. - Restrict upload and URL-fetching endpoints with capability checks such as
current_user_can('upload_files')or a more specific role-based gate. - Keep CSRF protection in place with
check_ajax_referer()and never expose backend URL fetchers to unauthenticated users.
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-14627