Better Search Replace Security & Risk Analysis

wordpress.org/plugins/better-search-replace

A simple plugin to update URLs or other text in a database.

1.0M active installs v1.4.10 PHP + WP 3.0.1+ Updated Dec 8, 2025
search-and-replacesearch-replacesearch-replace-databaseupdate-database-urlsupdate-live-url
61
C · Use Caution
CVEs total2
Unpatched0
Last CVEJan 24, 2024
Safety Verdict

Is Better Search Replace Safe to Use in 2026?

Use With Caution

Score 61/100

Better Search Replace has potential security issues identified during discovery analysis: 8 issues, currently under coordinated disclosure. Evaluate alternatives until patched.

2 known CVEs 8 under disclosure Last CVE: Jan 24, 2024Updated 5mo ago
Risk Assessment

The "better-search-replace" plugin version 1.4.10 exhibits a mixed security posture. On one hand, it demonstrates good practices by having a very limited attack surface with no apparent unprotected entry points and a reasonable percentage of SQL queries using prepared statements. The presence of nonce and capability checks, while minimal, is also a positive sign. However, the static analysis reveals a significant concern with the `unserialize` function, which is a known vector for deserialization vulnerabilities if not handled with extreme caution and strict validation of the serialized data. The taint analysis, while showing no critical or high severity flows, doesn't completely alleviate the risk associated with `unserialize` as it might not cover all potential exploitation scenarios.

The plugin's vulnerability history is a more concerning aspect. It has a history of two High severity CVEs, specifically related to "Deserialization of Untrusted Data" and "SQL Injection." Although currently unpatched, the fact that these vulnerabilities have occurred indicates a recurring need for careful code auditing, especially concerning data handling and database interactions. The most recent vulnerability was in January 2024, suggesting that security issues are not entirely in the distant past. While the current version's static analysis doesn't show immediate critical flaws, the historical pattern, particularly around deserialization and SQL injection, warrants a cautious approach. Therefore, while the plugin has some strengths in its design regarding attack surface, the presence of a dangerous function and a history of significant vulnerabilities necessitate vigilance.

Key Concerns

  • Dangerous function: unserialize detected
  • High severity CVEs in history
  • SQL queries not always prepared
  • Output escaping not fully proper
Coordinated disclosure in progress8 potential issues

wp-safety.org's automated analysis identified 3 medium, 5 low severity findings in this plugin. Specifics are withheld pending standard responsible-disclosure coordination with the vendor; full advisories will publish once the disclosure window closes.

Architectural analysis also surfaced 5 cross-cutting amplifiers (1 high impact) that increase the severity of any single finding in this plugin.

Site owners should monitor for plugin updates, evaluate alternatives, and consider their risk tolerance.

Vulnerabilities
2 published+ 8 under disclosure

Better Search Replace Security Vulnerabilities

CVEs by Year

1 CVE in 2022
2022
1 CVE in 2024
2024
Patched Has unpatched

Severity Breakdown

High
2

2 total CVEs

CVE-2023-6933high · 8.8Deserialization of Untrusted Data

Better Search Replace <= 1.4.4 - Unauthenticated PHP Object Injection

Jan 24, 2024 Patched in 1.4.5 (188d)
CVE-2022-2593high · 7.2Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')

Better Search Replace <= 1.4 - Authenticated (Administrator+) SQL Injection

Aug 1, 2022 Patched in 1.4.1 (540d)

How this plugin works

Analyzed 5/9/2026

Better Search Replace (v1.4.10) — Architecture Summary

Overview

Better Search Replace is a pure admin-utility WordPress plugin that performs serialization-aware search/replace operations across all database tables. It has zero public-facing surface: no shortcodes, no Gutenberg blocks, no REST routes, and no frontend assets. All functionality is gated behind the WordPress admin panel under Tools → Better Search Replace.

Auth Model

The plugin uses a two-layer defense-in-depth auth model:

  1. Bootstrap gate (bsr_enabled_for_user() on after_setup_theme): The entire plugin — including all hook registrations — is conditionally instantiated only when the current user passes current_user_can(apply_filters('bsr_capability', 'manage_options')). This means unauthenticated or lower-privileged users never see any registered hooks.

  2. Per-handler gate (BSR_Utils::check_admin_referer()): Every write/destructive handler additionally verifies both a WordPress nonce (check_admin_referer()) and re-checks the capability (bsr_enabled_for_user()). Exception: BSR_Admin::load_details() (admin_post_bsr_view_details) has no explicit nonce or capability check — it relies solely on WordPress core's admin-post.php logged-in requirement.

The bsr_capability filter is the single most important extension point and risk: any plugin can hook it to lower the required capability below manage_options, exposing full database write access to lower-privileged roles.

Custom AJAX Transport

The plugin does not use WordPress's standard wp-admin/admin-ajax.php. Instead it hooks init (priority 1/2) and dispatches on $_GET['bsr-ajax'], sanitized via sanitize_text_field(). The resolved URL is <admin_url>/tools.php?page=better-search-replace&bsr-ajax=<action>. This fires on every WordPress page load (including frontend) when the parameter is present, though the auth gate prevents exploitation.

Batch Processing Architecture

The core operation is a client-driven pagination loop: JavaScript POSTs to the custom AJAX endpoint with incrementing bsr_step (table index) and bsr_page (row-block offset) until the server responds with step='done'. Batch state is persisted in wp_options['bsr_data'] between calls. The siteurl option is specially deferred to avoid mid-run URL breakage.

Database Access

All DB access is via $wpdb. Table names are validated via table_exists() (SHOW TABLES whitelist) and esc_sql(). UPDATE queries are constructed by hand using a custom mysql_escape_mimic() function rather than $wpdb->prepare(). Column names come from DESCRIBE output (schema-controlled). The search/replace strings flow through PHP str_replace/str_ireplace and are never directly interpolated into SQL.

Serialization Safety

Deserialization is centralized in BSR_DB::unserialize() which enforces allowed_classes=false (PHP 7+) or a namespaced polyfill (BSR\Brumann\Polyfill\Unserialize) for PHP < 7. This prevents PHP object injection attacks.

Persistence

Three wp_options keys: bsr_page_size (config), bsr_data (run state including raw search/replace strings), bsr_update_site_url (deferred siteurl). One transient: bsr_results (24h TTL, holds per-table stats plus verbatim search/replace strings). No uninstall hook — all options persist after plugin removal.

No External Dependencies

Zero outbound HTTP calls at runtime. No AI integrations. No telemetry. All hardcoded external URLs are static HTML anchor tags only. Updates flow through WordPress.org infrastructure.

Multisite

Declared Network: true. bsr_enabled_for_user() uses current_user_can() which evaluates against the current blog's capabilities — a sub-site admin with manage_options gains full search/replace access. No is_super_admin() floor is enforced.

Component Overview

flowchart
Loading diagram…

Data Flow

flowchart
Loading diagram…

External Dependencies

pie

External Dependency Types

4 Total
  • WordPress Core (jQuery, jQuery UI, Thickbox)375%
  • Bundled PHP Library (brumann/polyfill-unserialize)125%
  • Outbound HTTP APIs00%

Auth Tier Distribution

pie

Entry Points by Auth Tier

5 Total
  • Admin (manage_options or bsr_capability filter)480%
  • Logged-in (any WordPress user)120%
  • Unauthenticated00%

Custom Tables Er

erd
wp_options (existing)3 fields
PK option_idbigint
option_namevarchar — bsr_page_size | bsr_data | bsr_update_site_url
option_valuelongtext (serialized for bsr_data)
→ 1..1 e-transient (stored in same wp_options table)
wp_options (transient bsr_results)3 fields
PK option_name_transient_bsr_results
option_valueserialized array: search_for, replace_with, table_reports
ttlDAY_IN_SECONDS (86400)

Auth Capability Map

flowchart
Loading diagram…

Sink Reachability

flowchart
Loading diagram…

Ajax Action Map

tree
BSR_AJAX::do_bsr_ajax() [init priority 2]
do_action('bsr_ajax_' + sanitize_text_field($_GET['bsr-ajax']))
└─ bsr_ajax_process_search_replace
→ BSR_AJAX::process_search_replace()
Auth: check_admin_referer('bsr_ajax_nonce') + bsr_enabled_for_user()
Sinks: arbitrary-table-write, bsr_data option, bsr_results transient

Plugin Lifecycle

state
Loading diagram…

Architectural amplifiers

Cross-cutting patterns observed in the codebase that multiply the impact of any single finding — not vulnerabilities themselves, but the structural reasons why individual issues become more dangerous in this plugin.

  • Filterable capability gate (`bsr_capability`) has no validation floor
    The sole authorization control for every plugin feature is `current_user_can(apply_filters('bsr_capability', 'manage_options'))`. Any co-installed plugin or theme can hook `bsr_capability` and return any string (including `'read'`, `''`, or `true`), silently lowering access to the full-database search/replace engine for all logged-in users or even unauthenticated users. There is no validation of the filtered value and no minimum floor capability enforced.
  • No uninstall hook — sensitive option data persists after plugin removal
    No `register_uninstall_hook()`, `register_deactivation_hook()`, or `register_activation_hook()` calls exist. The options `bsr_page_size`, `bsr_data` (may contain plaintext search/replace strings), and `bsr_update_site_url` (may contain a pending siteurl value) persist indefinitely in `wp_options` after the plugin is deleted.
  • Custom `init`-hook AJAX channel fires on every page load
    The plugin implements its own AJAX transport by hooking `init` (priority 1/2) and reading `$_GET['bsr-ajax']`. This fires on every WordPress request including frontend pages — not just admin requests. Auth checks are entirely delegated to individual handlers; there is no global middleware check at the dispatch layer. The action suffix from `$_GET['bsr-ajax']` is sanitized via `sanitize_text_field()` before dispatch, but auth is handler-responsibility only.
  • Hand-rolled SQL escaping (`mysql_escape_mimic`) instead of `$wpdb->prepare()`
    All UPDATE and WHERE clause values in `BSR_DB::srdb()` are escaped via a custom `mysql_escape_mimic()` function rather than `$wpdb->prepare()`. This function is functionally similar to `mysql_real_escape_string` for common cases but has not been formally audited against multi-byte charset attacks or all edge cases. It is a plugin-wide pattern affecting every database write the plugin performs.
  • Transient `bsr_results` stores verbatim user-controlled search/replace strings
    The `bsr_results` transient (24h TTL) stores the raw `search_for` and `replace_with` values supplied by the operator. This transient is read by multiple display paths. Critically, `admin_post_bsr_view_details` reads and renders this transient without any capability or nonce check, exposing its contents to any logged-in WordPress user.
Version History

Better Search Replace Release Timeline

v1.4.10Current9 files changed
v1.4.75 files changed
v1.4.66 files changed
v1.4.58 files changed
v1.4.41 CVE4 files changed
v1.4.31 CVE9 files changed
v1.4.21 CVE6 files changed
v1.4.11 CVE4 files changed
v1.42 CVEs22 files changed
Code Analysis
Analyzed Mar 16, 2026

Better Search Replace Code Analysis

Dangerous Functions
1
Raw SQL Queries
4
4 prepared
Unescaped Output
17
26 escaped
Nonce Checks
1
Capability Checks
1
File Operations
0
External Requests
0
Bundled Libraries
0

Dangerous Functions Found

unserialize$unserialized_string = @unserialize( $serialized_string, array('allowed_classes' => false ) );includes\class-bsr-db.php:457

SQL Query Safety

50% prepared8 total queries

Output Escaping

60% escaped43 total outputs
Data Flows · Security
All sanitized

Data Flow Analysis

4 flows
download_sysinfo (includes\class-bsr-admin.php:278)
Source (user input) Sink (dangerous op) Sanitizer Transform Unsanitized Sanitized
Attack Surface

Better Search Replace Attack Surface

Entry Points0
Unprotected0
WordPress Hooks 11
actionafter_setup_themebetter-search-replace.php:81
actioninitincludes\class-bsr-ajax.php:23
actioninitincludes\class-bsr-ajax.php:24
actionadmin_enqueue_scriptsincludes\class-bsr-main.php:122
actionadmin_menuincludes\class-bsr-main.php:123
actionadmin_initincludes\class-bsr-main.php:126
actionadmin_post_bsr_view_detailsincludes\class-bsr-main.php:127
actionadmin_post_bsr_download_sysinfoincludes\class-bsr-main.php:128
actionplugin_row_metaincludes\class-bsr-main.php:129
filterupdate_footerincludes\class-bsr-main.php:132
filteradmin_footer_textincludes\class-bsr-main.php:133
Maintenance & Trust

Better Search Replace Maintenance & Trust

Maintenance Signals

WordPress version tested6.9.4
Last updatedDec 8, 2025
PHP min version
Downloads17.4M

Community Trust

Rating86/100
Number of ratings541
Active installs1.0M
Developer Profile

Better Search Replace Developer Profile

WP Engine

16 plugins · 3.5M total installs

73
trust score
Avg Security Score
91/100
Avg Patch Time
831 days
View full developer profile
Detection Fingerprints

How We Detect Better Search Replace

Patterns used to identify this plugin on WordPress sites during automated security audits and web crawling.

Asset Fingerprints

Asset Paths
/wp-content/plugins/better-search-replace/assets/css/better-search-replace.css/wp-content/plugins/better-search-replace/assets/css/jquery-ui.min.css/wp-content/plugins/better-search-replace/assets/js/better-search-replace.js
Script Paths
/wp-content/plugins/better-search-replace/assets/js/better-search-replace.js
Version Parameters
/wp-content/plugins/better-search-replace/assets/css/better-search-replace/wp-content/plugins/better-search-replace/assets/js/better-search-replace

HTML / DOM Fingerprints

CSS Classes
bsr-dashboard-sectionbsr-rowbsr-submit
HTML Comments
<!-- The main plugin class that is used to define internationalization, dashboard-specific hooks, and public-facing site hooks. --><!-- The callback for creating a new submenu page under the "Tools" menu. --><!-- Trying to show results? --><!-- Have results with required fields set with correctly typed data? -->
Data Attributes
data-search-replace-endpointdata-noncedata-page-size
JS Globals
bsr_object_vars
REST Endpoints
/wp-json/better-search-replace/v1/search/wp-json/better-search-replace/v1/replace
FAQ

Frequently Asked Questions about Better Search Replace