Zotpress SQLi: When Citations Become Remote Execution
Jan 6, 2026·6 min read
Executive Summary (TL;DR)
Zotpress versions prior to 6.1.3 fail to sanitize the 'api_user_id' parameter in AJAX requests. This allows any unauthenticated visitor to inject SQL commands directly into the WordPress database. Attackers can dump administrator credentials, modify content, or gain remote code execution capabilities. The fix involves wrapping the query in WordPress's prepared statements.
A critical SQL Injection vulnerability in the Zotpress WordPress plugin allows unauthenticated attackers to execute arbitrary SQL commands via the 'api_user_id' parameter, potentially leading to full site compromise.
The Hook: Scholarly Pursuits & Database Dumps
Academia is usually a quiet place. Libraries, citations, and the endless struggle to format a bibliography correctly. Enter Zotpress, a popular WordPress plugin designed to bridge the gap between the Zotero reference manager and WordPress websites. It allows researchers, universities, and students to display their citations dynamically.
But back in 2016, Zotpress offered up something far juicier than a well-formatted MLA citation: a wide-open, unauthenticated SQL injection vulnerability. It's the kind of bug that makes security researchers sigh and script kiddies salivate.
This wasn't some complex heap overflow or a race condition requiring frame-perfect timing. This was a classic 'concatenate user input directly into the database query' situation. It’s the digital equivalent of leaving your house key under the doormat, but the doormat is made of glass, and there is a neon sign pointing to it.
Because Zotpress is heavily used in educational sectors—environments often rich in PII and intellectual property—this vulnerability provided a direct line to compromise university servers and research blogs with a single URL.
The Flaw: Trusting the Syllabus
The root of the problem lies in how Zotpress handles data retrieval via its AJAX interface. WordPress plugins often use admin-ajax.php to handle dynamic requests. In this case, the action zpRetrieveViaShortcode was responsible for fetching account data.
The logic flow is painfully simple. The plugin looks for a parameter named api_user_id in the global $_GET array. In a secure application, this input would be treated as radioactive waste until proven innocent. It should be cast to an integer, validated against a whitelist, or, at the very least, parameterized.
Instead, Zotpress took the 'trust everyone' approach. The code blindly accepted whatever string the user provided and handed it off to the zp_get_account() function. There was no type checking. If you sent a number, it worked. If you sent a SQL query, it also worked—just not in the way the developer intended.
This is a textbook example of CWE-89 (SQL Injection). By failing to separate the data (the user ID) from the command (the SQL query), the application allowed the user to rewrite the database instructions on the fly.
The Code: The Smoking Gun
Let's look at the crime scene. The vulnerability lived in lib/request/request.functions.php. Here is the vulnerable function zp_get_account in version 6.1.2 and below:
// Vulnerable Code (v6.1.2)
function zp_get_account ($wpdb, $api_user_id_incoming=false)
{
if ($api_user_id_incoming !== false)
// ERROR: Direct concatenation of user input
$zp_account = $wpdb->get_results("SELECT * FROM ".$wpdb->prefix."zotpress WHERE api_user_id='".$api_user_id_incoming."'");
else
$zp_account = $wpdb->get_results("SELECT * FROM ".$wpdb->prefix."zotpress ORDER BY id DESC LIMIT 1");
return $zp_account;
}See that api_user_id_incoming variable being stitched right into the string? That is the kill shot. An attacker simply needs to close the single quote ', add their payload, and comment out the rest.
The fix in version 6.1.3 was straightforward. The developer implemented WordPress's $wpdb->prepare(), which handles escaping and quoting automatically (similar to parameterized queries in other languages).
// Patched Code (v6.1.3)
function zp_get_account ($wpdb, $api_user_id_incoming=false)
{
if ($api_user_id_incoming !== false)
{
// FIXED: Using prepare() with %s placeholder
$zp_account = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM ".$wpdb->prefix."zotpress WHERE api_user_id='%s'",
$api_user_id_incoming
)
);
}
...
}By wrapping the query in prepare and using the %s placeholder, the input is treated strictly as a string literal, rendering SQL syntax characters harmless.
The Exploit: Grading the Attack
Exploiting this requires no authentication, making it a CVSS 9.8 critical issue. The entry point is the standard WordPress AJAX handler.
The Attack Chain:
- Recon: The attacker identifies a site using Zotpress (looking for
wp-content/plugins/zotpressin the source). - Payload Construction: The goal is to inject SQL via the
api_user_idparameter. Since the query expects a string, we close the quote and start our own query.
Conceptual Payload:
GET /wp-admin/admin-ajax.php?action=zpRetrieveViaShortcode&api_user_id=1' UNION SELECT 1, user_login, user_pass, 4, 5 FROM wp_users -- - HTTP/1.1
Host: target-university.eduWait, you might ask: "Doesn't WordPress require a nonce for AJAX?" Usually, yes. The code likely checks check_ajax_referer. However, because Zotpress is designed to display citations on the frontend to public visitors, the nonce is often embedded in the page source for the frontend JavaScript to use.
An attacker simply scrapes the homepage, grabs the nonce (often named something like zp_shortcode_nonce), and appends it to the request. Once the query executes, the results (including the admin password hash) can be returned in the response or extracted via Blind SQLi techniques (e.g., SLEEP()) if error reporting is suppressed.
The Impact: Expulsion
What happens when you inject SQL into a WordPress site? You own it.
Data Exfiltration: The most immediate threat is the dumping of the wp_users table. WordPress stores password hashes (MD5-phpass). While not plaintext, they are crackable, especially if the admin uses a weak password. Once the attacker has the admin password, they log in.
Remote Code Execution (RCE): SQLi in WordPress is often a stepping stone to RCE. Once logged in as an administrator, an attacker can upload a malicious plugin or edit the theme's header.php to include a web shell.
Defacement & SEO Spam: Attackers can modify post content to inject spam links (common in pharmaceutical hacks) or completely wipe the database, causing catastrophic data loss for the institution.
Given the EPSS score of ~11%, this vulnerability is not theoretical. It has been actively scanned for and exploited in the wild, particularly because automation tools (like SQLMap) can identify and exploit this standard error in seconds.
The Fix: Extra Credit
If you are running Zotpress version 6.1.2 or older, you are currently failing this course. The remediation is simple:
- Update Immediately: Go to your WordPress dashboard and update Zotpress to the latest version (at least 6.1.3).
- Verify the Code: If you cannot update for some reason (why?), you can manually patch
lib/request/request.functions.phpby wrapping the query in$wpdb->prepareas shown in the Code section.
Defense in Depth:
Even with the patch, you should have a Web Application Firewall (WAF) like Cloudflare or Wordfence. These tools have rulesets specifically designed to block requests containing SQL keywords like UNION SELECT or SLEEP in query parameters. This acts as a safety net for when—not if—another plugin developer forgets to sanitize their inputs.
Fix Analysis (1)
Technical Appendix
CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:HAffected Systems
Affected Versions Detail
| Product | Affected Versions | Fixed Version |
|---|---|---|
Zotpress Katie Seaborn | < 6.1.3 | 6.1.3 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-89 (SQL Injection) |
| Attack Vector | Network (Remote) |
| CVSS v3.0 | 9.8 (Critical) |
| EPSS Score | 0.11 (11.4%) |
| Privileges Required | None |
| Exploit Status | Functional PoC / Automated |
MITRE ATT&CK Mapping
The software constructs all or part of an SQL command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended SQL command when it is sent to a downstream component.
Known Exploits & Detection
Vulnerability Timeline
Subscribe to updates
Get the latest CVE analysis reports delivered to your inbox.