WP Activity Log before 4.1.5 unauthenticated SQLi
Status: patched. This is a fix-verified writeup of an unauthenticated SQL injection we originally disclosed in WP Activity Log (then WP Security Audit Log, now sold by Melapress) back in November 2020. Melapress shipped the fix in 4.1.5, and the entry has sat quietly on Patchstack and WPScan’s databases ever since, credited to WP Deeply. We’re revisiting it now because the exact class of mistake — string-concatenated SQL with a hand-rolled “escaping” function instead of a prepared statement — is still one of the most common findings in 2026’s plugin vulnerability reports, including in code shipped by tools that never had a human read it line by line.
At a glance
- Plugin: WP Activity Log (formerly WP Security Audit Log), developed by Melapress
- Affected: versions below 4.1.5
- Fixed: 4.1.5, released November 2020
- Class: Unauthenticated SQL injection (CWE-89 / OWASP A1: Injection)
- Location: the external-database mirroring feature,
classes/Connector/MySQLDB.php - Reported by: WP Deeply, credited on Patchstack’s and WPScan’s public disclosure records
What the plugin does
WP Activity Log logs administrative and user activity across a WordPress install so site owners have a record of who did what, when. It’s a genuinely useful category of plugin — audit trails matter — and one of its premium features lets that activity log be mirrored to an external MySQL database for archiving or offsite storage.
That mirroring feature is where the bug lived. Across the codebase’s MySQL-handling classes, queries were built by concatenating strings directly rather than using WordPress’s $wpdb->prepare(), which is the standard, built-in way to parameterize a query in WP plugin development. Once you’re concatenating raw values into SQL, you’re relying entirely on whatever manual escaping you wrote catching every case — and in this plugin, it didn’t.
The vulnerable code
The clearest example sits in the MirroringAlertsToDB method, which pulls event metadata — including fields like User Agent, which is attacker-controlled input on essentially every request — and re-inserts it into the mirror database:
// Load data Meta from WP.
$meta = new WSAL_Adapters_MySQL_Meta( $_wpdb );
...
$sql = 'SELECT * FROM ' . $meta->GetTable() . ' WHERE occurrence_id >= ' . $first_occurrence_id;
$metadata = $_wpdb->get_results( $sql, ARRAY_A );
if ( ! empty( $metadata ) ) {
$meta_new = new WSAL_Adapters_MySQL_Meta( $mirroring_db );
$sql = 'INSERT INTO ' . $meta_new->GetTable() . ' (occurrence_id, name, value) VALUES ';
foreach ( $metadata as $entry ) {
$sql .= '(' . $entry['occurrence_id'] . ', \'' . $entry['name'] . '\', \''
. str_replace( array( "'", "\'" ), "\'", $entry['value'] ) . '\'), ';
}
$sql = rtrim( $sql, ', ' );
$mirroring_db->query( $sql );
}
Data pulled from one table gets dropped straight into an INSERT statement against another, and the only thing standing between that value and the query itself is this:
str_replace( array( "'", "\'" ), "\'", $entry['value'] )
That’s not an escaping function — it’s a find-and-replace that doesn’t account for how MySQL actually parses backslash-escaped quotes. A value containing a backslash-quote sequence breaks out of the intended string context, which is a textbook SQL injection entry point. Because the mirroring feature runs as part of normal event logging rather than behind an authenticated admin action, this was exploitable without logging in at all — hence “unauthenticated.”
The second bug: a query filter that phishes the admin
The same disclosure covered a second, smaller issue. In classes/Sensors/Database.php, the plugin hooks WordPress’s query filter to watch for schema changes:
add_action( 'dbdelta_queries', array( $this, 'EventDBDeltaQuery' ) );
add_filter( 'query', array( $this, 'EventDropQuery' ) );
Inside EventDropQuery, the plugin pattern-matches on the raw query string:
preg_match( '|DROP TABLE ([^ ]*)|', $query )
...
preg_match( '|CREATE TABLE IF NOT EXISTS ([^ ]*)|', $query )
Any query anywhere in the request lifecycle that happens to contain the literal string “DROP TABLE” or “CREATE TABLE IF NOT EXISTS” — regardless of whether it actually executed, or came from the attacker’s own injected payload rather than a real schema change — triggers an admin-facing notification. Combined with the SQLi above, that’s a way to generate convincing-looking “your database schema just changed” alerts on demand, which is a solid primitive for phishing an administrator into panicked, ill-considered action.
Why this one still matters in 2026
Two things make this five-year-old fix worth revisiting rather than filing away. First, mirroring, syncing, and export features — the parts of a plugin that talk to a second database or external system — are consistently under-scrutinized compared to the plugin’s main admin-facing forms, because they don’t feel like “user input” even when the data flowing through them originated as a browser header. Second, per Patchstack’s 2026 ecosystem report, 91% of newly disclosed WordPress vulnerabilities in 2025 were in plugins, and the report specifically calls out a rise in vulnerabilities from LLM-generated plugin code that nobody with security context reviewed line by line — which is exactly the failure mode here, just five years earlier and human-written. A hand-rolled str_replace standing in for a prepared statement is precisely the kind of shortcut an unreviewed AI-generated pull request would also take, because it looks like escaping if you don’t know what MySQL actually does with a backslash-quote sequence.
Remediation
- Update WP Activity Log to 4.1.5 or later if you’re somehow still on an older branch — this has been fixed for five years, but stale installs on this plugin family have shown up in later, unrelated CVEs too (see the 2024 authenticated SQLi in WP Activity Log Premium, CVE-2024-2018, fixed in 4.6.4.1), so staying current matters more than any single patch.
- Never hand-roll SQL escaping. Use
$wpdb->prepare()for every query touching user-influenced data, full stop — including data that arrived indirectly, like values pulled from your own database that originated as request input somewhere upstream. - Audit secondary data paths, not just primary forms. Export, mirroring, webhook, and sync features move the same attacker-reachable data as your contact form does — they just don’t look like it in the code.
- If you’re reviewing AI-assisted plugin code, treat every raw SQL string as a stop-and-check point before merging, regardless of how confident the generated escaping logic looks.
Disclosure timeline
- Discovered by WP Deeply via manual code review while scanning WordPress plugins for common vulnerability patterns
- Reported to Melapress (then developing under the WP Security Audit Log name) through responsible disclosure
- Patched in version 4.1.5
- Publicly listed by Patchstack and WPScan, November 2020, credited to WP Deeply
This vulnerability is fully patched and has been for five years. It’s documented here as a case study in a still-common escaping mistake, not as guidance for testing against unpatched installs. If you administer a WordPress site, confirm your plugins are current — WP Activity Log 4.1.5+ closes this specific issue.