Better Find and Replace
Privilege Escalation Vulnerability
This blog post is about the Better Find and Replace plugin vulnerability. If you’re a Better Find and Replace user, please update the plugin to at least version 1.6.8.
If you are a Patchstack customer, you are protected from this vulnerability already, and no further action is required from you.
For plugin developers, we have security audit services and Enterprise API for hosting companies.
About the Better Find and Replace Plugin
The plugin Better Find and Replace which has over 50,000 active installations, is one of the popular search and replace plugins in the WordPress ecosystem. It allows users to quickly find and replace different texts in the posts, meta and database. This plugin is developed by CodeSolz.
According to their official WordPress plugin page, “The plugin provides an optimized search and replace function, providing an effective solution for efficient database management. Additionally, it incorporates a dynamic real-time word/text replacing feature.”
Since it allows search/replace in the database itself, it can be a problematic issue if low-privileged users are able to search/replace the content. This is exactly what happened with the plugin leading to a subscriber user relacing their privilege to an administrator.
The security vulnerability
The plugin (version 1.6.7 and below) suffers from a privilege escalation vulnerability. The vulnerability occurred due to the leakage of the nonce to low-privileged users and the lack of proper permission checks in the function that is responsible for replacing the texts in the database. The vulnerability has been tracked as CVE-2025-24734.
The routing of the plugin has been done in an interesting way. Instead of following the traditional ajax hook method, a class/method-based approach is used to route the requests.
function __construct() {
add_action( 'wp_ajax_rtafar_ajax', array( $this, 'rtafar_ajax' ) );
add_action( 'wp_ajax_nopriv_rtafar_ajax', array( $this, 'rtafar_ajax' ) );
}
public function rtafar_ajax() {
if ( ! isset( $_REQUEST['cs_token'] ) || false === check_ajax_referer( SECURE_AUTH_SALT, 'cs_token', false ) ) {
wp_send_json(
array(
'status' => false,
'title' => __( 'Invalid token', 'real-time-auto-find-and-replace' ),
'text' => __( 'Sorry! we are unable recognize your auth!', 'real-time-auto-find-and-replace' ),
)
);
}
if ( ! isset( $_REQUEST['data'] ) && isset( $_POST['method'] ) ) {
$data = $_POST;
} else {
$data = $_REQUEST['data'];
}
if ( empty( $method = $data['method'] ) || strpos( $method, '@' ) === false ) {
wp_send_json(
array(
'status' => false,
'title' => __( 'Invalid Request', 'real-time-auto-find-and-replace' ),
'text' => __( 'Method parameter missing / invalid!', 'real-time-auto-find-and-replace' ),
)
);
}
$method = explode( '@', $method );
$class_path = str_replace( '\\\\', '\\', '\\RealTimeAutoFindReplace\\' . $method[0] );
if ( ! class_exists( $class_path ) ) {
wp_send_json(
array(
'status' => false,
'title' => __( 'Invalid Library', 'real-time-auto-find-and-replace' ),
/* Translators: %s is the name of the class name with path. */
'text' => sprintf( __( 'Library Class "%s" not found! ', 'real-time-auto-find-and-replace' ), $class_path ),
)
);
}
if ( ! method_exists( $class_path, $method[1] ) ) {
wp_send_json(
array(
'status' => false,
'title' => __( 'Invalid Method', 'real-time-auto-find-and-replace' ),
/* Translators: %1$s is the method of class and %2$s is the class name with path. */
'text' => sprintf( __( 'Method "%1$s" not found in Class "%2$s"! ', 'real-time-auto-find-and-replace' ), $method[1], $class_path ),
)
);
}
( new $class_path() )->{$method[1]}( $data );
exit;
}
As seen above, the ajax hook is checking for the nonce used check_ajax_referer
initially. The nonce is being generated in the admin page of the plugin which is partially available to subscriber+ users as well. In the page source, the nonce is leaked with the value cs_token
.
After the nonce check is successful, the method
parameter is being deconstructed with @ delimiter. The former part of the delimiter is the class name and the latter part is the method name. In order to prevent the routing from being misused, there is a strict check on the namespace with the hardcoded RealTimeAutoFindReplace
value. The respective method of the class is called after all the checks are completed.
<?php namespace RealTimeAutoFindReplace\admin\functions;
class DbReplacer {
public function db_string_replace( $user_query ) {
// pre_print( $user_query );
$userInput = Util::check_evil_script( $user_query['cs_db_string_replace'] );
if ( ! isset( $userInput['find'] ) ||
empty( $find = $userInput['find'] )
) {
return wp_send_json(
array(
'status' => false,
'title' => __( 'Error!', 'real-time-auto-find-and-replace' ),
'text' => __( 'Please enter string to find and replace.', 'real-time-auto-find-and-replace' ),
)
);
}
//TRIMMED CODE
$dryRunReport = apply_filters( 'bfrp_dryrun_final_report', $dryRunReport );
return wp_send_json(
\array_merge_recursive(
array(
'status' => true,
'title' => __( 'Success!', 'real-time-auto-find-and-replace' ),
/* translators: %1$s: Total rows %2$s : replaced rows */
'text' => sprintf( __( 'Thank you! replacement completed!. Total %1$s replaced : %2$d', 'real-time-auto-find-and-replace' ), $replaceType, $i ),
'nothing_found' => __( 'Sorry! Nothing Found!', 'real-time-auto-find-and-replace' ),
),
$dryRunReport
)
);
}
//TRIMMED CODE
}
The above function db_string_replace
is responsible for replacing the strings in the DB. It can be called with the method value set to admin\functions\DbReplacer@db_string_replace
. Notice that there is no permission check in the function, and since we already have the nonce required as a subscriber, it is possible to call this method and replace anything in the database. Since the database tables are limited to wp_posts
, wp_postmeta
, and wp_options
, it is possible to manipulate the default registration permission in the wp_options
table to achieve the privilege escalation.
A subscriber could change the default registration permission to Administrator and create a new account to gain full access to the WordPress site. Even if the default registration is turned off, an attacker could turn that on in a similar way to complete the attack.
The patch
The vendor patched the issue by adding a strict check for sufficient permission to execute the method.
Conclusion
It is always crucial to ensure that a user’s permission check is not solely reliant on nonce. In case the nonce is leaked somewhere, any user has access to sensitive actions and functions. Along with the nonce check, a strong permission check is important to ensure vulnerability like this one is not introduced in the codebase.
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
Help us make the Internet a safer place
Making the WordPress ecosystem more secure is a team effort, and we believe that plugin developers and security researchers should work together.
- If you’re a plugin developer, join our mVDP program that makes it easier to report, manage and address vulnerabilities in your software.
- If you’re a security researcher, join Patchstack Alliance to report vulnerabilities & earn rewards.