This blog post is about an unauthenticated SQL injection vulnerability in the Paid Membership Subscriptions plugin. If you're a Paid Membership Subscriptions plugin user, please update the plugin to version 2.15.2.
The vulnerabilities mentioned here were discovered and reported by Patchstack Alliance community member ChuongVN.
✌️ Our users are protected from this vulnerability. Are yours?
Identify vulnerabilities in your plugins and get recommendations for fixes.
Request auditProtect your users, improve server health and earn additional revenue.
Patchstack for hostsAbout Paid Membership Subscriptions plugin
The plugin Paid Membership Subscriptions, which has over 10,000 active installations, allows site owners to optimize the site with membership and recurring subscriptions in just a few clicks. It allows integration of various payment methods and offers a smooth subscription styled payments.

Unauthenticated SQL Injection
In versions 2.15.1 and below, the plugin is vulnerable to an SQL injection, which allows any unauthenticated attacker to inject SQL queries into the database. The vulnerability has been patched in version 2.15.2 and is tracked with CVE-2025-49870.
The root cause of the issue lies in the process_webhooks function:
public function process_webhooks() {
if( !isset( $_GET['pay_gate_listener'] ) || $_GET['pay_gate_listener'] !== 'paypal_ipn' )
return;
// Init IPN Verifier
$ipn_verifier = new PMS_IPN_Verifier();
if( pms_is_payment_test_mode() )
$ipn_verifier->is_sandbox = true;
$verified = false;
// Process the IPN
try {
if( $ipn_verifier->checkRequestPost() )
$verified = $ipn_verifier->validate();
} catch ( Exception $e ) {
}
if( $verified ) {
$post_data = $_POST;
// Get payment id from custom variable sent by IPN
$payment_id = isset( $post_data['custom'] ) ? $post_data['custom'] : 0;
// Get the payment
$payment = pms_get_payment( $payment_id );
// Get user id from the payment
$user_id = $payment->user_id;
// TRIMMED
}
}
The function takes the user-input as $post_data
, extracts the $payment_id
value from it, and calls the pms_get_payment
function.
function pms_get_payment( $payment_id = 0 ) {
return new PMS_Payment( $payment_id );
}
The function just calls a new instance of the PMS_Payment
class. The constructor of the respective class gets called.
public function __construct( $id = 0 ) {
// Return if no id provided
if( $id == 0 ) {
$this->id = 0;
return;
}
// Get payment data from the db
$data = $this->get_data( $id );
// Return if data is not in the db
if( is_null($data) ) {
$this->id = 0;
return;
}
// Populate the data
$this->set_instance( $data );
}
The constructor of the class calls get_data
which is vulnerable to SQL injection due to improper concatenation of user input.
public function get_data( $id ) {
global $wpdb;
$result = $wpdb->get_row("SELECT * FROM {$wpdb->prefix}pms_payments WHERE id = {$id}", ARRAY_A );
return $result;
}
The patch
In version 2.15.2, the vendor patched the SQL injection by ensuring that $id
is numeric while passing to the get_data function and used proper prepared statements.

Conclusion
For the SQL query process, always do a safe escape and format the user's input before performing a query. The best practice is always to use a prepared statement and also cast each of the used variables to its intended usage.
Want to learn more about finding and fixing vulnerabilities?
Explore our Academy to master the art of finding and patching vulnerabilities within the WordPress ecosystem. Dive deep into detailed guides on various vulnerability types, from discovery tactics for researchers to robust fixes for developers. Join us and contribute to our growing knowledge base.
Timeline
🤝 You can help us make the Internet a safer place
Streamline your disclosure process to fix vulnerabilities faster and comply with CRA.
Get started for freeProtect your users too! Improve server health and earn added revenue with proactive security.
Patchstack for hostsReport vulnerabilities to our gamified bug bounty program to earn monthly cash rewards.
Learn more