CVE-2026-1581 WordPress wpForo Forum Plugin is vulnerable to a high priority SQL Injection
CVE-2026-1581 WordPress wpForo Forum Plugin is vulnerable to a high priority SQL Injection

Overview
Published: 2026-02-19
CVE-ID: CVE-2026-1581
CVSS: 7.5
Affected Plugin: WordPress wpForo Forum Plugin
Affected Versions: <= 2.4.14
Vulnerability Type: High priority SQL Injection
CWE: CWE-89 Improper Neutralization of Special Elements used in an SQL Command.
Description
The wpForo Forum plugin for WordPress is vulnerable to time-based SQL Injection via the ‘wpfob’ parameter in all versions up to, and including, 2.4.14 due to insufficient escaping on the user supplied parameter and lack of sufficient preparation on the existing SQL query. This makes it possible for unauthenticated attackers to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database.
Patch And Commit Analysis

Based on Changelog of the product, we will compare between wpForo plugin version 2.4.14 (Patch) and 2.4.14 (Vulnerable) and analyze how developer patch and get into sink.

By comparing the source code of the vulnerable version with the patched release, we can pinpoint the exact remediation strategy applied by the developers. The core of the patch is located in the includes/functions.php file, where a new custom sanitization function named wpforo_sanitize_orderby()was introduced.
As anticipated for ORDER BY SQL injection vulnerabilities—where traditional Prepared Statements cannot be used for column names—the developers implemented a strict Whitelisting approach. The wpforo_sanitize_orderby function acts as a security gatekeeper:
Contextual Whitelists: It defines an $allowed array containing explicitly permitted column names, meticulously categorized by their execution context (e.g., topics, posts, members, and search).
Input Validation: Whenever the application processes the user-supplied wpfob parameter to sort results, it now routes the input through this function.
Neutralization: The input is strictly validated against the predefined whitelist. If the injected payload does not match any of the allowed safe columns, it is immediately discarded and replaced with a safe default value.
This robust mechanism effectively neutralizes the vulnerability. Attackers can no longer append arbitrary SQL functions (such as SLEEP()) to the query, successfully mitigating the Time-based SQL Injection vector at the source.



The Patch also shows exact locations where the sink existed. The clean_text_field() function (which has no effect on SQLi errors in ORDER BY) has been completely replaced by the wpforo_sanitize_orderby() function with the corresponding context.
Root Cause Analysis: Tracing from Sink to Source
To fully understand how this vulnerability is triggered, we will trace the data flow backward: starting from the execution point of the SQL query (The Sink) all the way up to where the user input is initially processed (The Source).
The Sink: Vulnerable SQL Execution (classes/Topics.php)
The execution point of this Time-based SQL Injection lies within the get_topics() function in the \classes\Topics class.
When observing how the SQL query is constructed, we can see the ORDER BY clause being dynamically built:
1 | $sql .= " ORDER BY " . str_replace( ',', ' ' . esc_sql( $order ) . ',', esc_sql( $orderby ) ) . " " . esc_sql( $order ); |
Why is this vulnerable?
The developer attempted to secure the input using WordPress’s native esc_sql() function. However, esc_sql() is designed to escape string values by adding slashes to quotes (e.g., ‘ becomes '). In SQL syntax, column names or functions within an ORDER BY clause are not enclosed in quotes. Therefore, an attacker can simply inject an SQL function like (SELECT SLEEP(5)) without using any quotes, entirely bypassing the esc_sql() protection. The query is then executed directly via WPF()->db->get_results( $sql, ARRAY_A );.

The Data Flow: Variable Extraction
Moving one step backward, we need to find where the $orderby variable originates within the get_topics() function.
At the beginning of this function, the $args array passed into the function is unpacked into local variables using PHP’s extract() function:


This means that if the $args array contains a key named orderby (i.e., $args['orderby'] = "malicious_payload"), it will be directly extracted and overwrite the local $orderby variable, which is then passed down to the vulnerable SQL string concatenation.
The Source: Unsafe Input Handling (wpforo.php)
Finally, we trace back to where the $args['orderby'] is initially populated from the user’s request. This takes us to the core plugin file, specifically within the init_current_object() method of the wpforo class.
1 | public function init_current_object() { |
This method handles routing and object initialization based on the requested template (e.g., recent, search, members). When preparing the arguments to fetch topics for the 'recent' template, we find the exact entry point of the user input:
1 | $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? sanitize_text_field( WPF()->GET['wpfob'] ) : 'modified'; |
The Flaw at the Source:
The application receives the wpfob parameter directly from the user’s GET request WPF()->GET['wpfob']. It attempts to clean this input using sanitize_text_field(). While this function is excellent for stripping HTML tags and preventing Cross-Site Scripting (XSS), it does absolutely nothing to strip or neutralize SQL commands like SLEEP(), BENCHMARK(), or CASE WHEN….
Consequently, the payload travels securely from the user’s browser, passes through inadequate sanitization at the Source, flows through the $args array, and is ultimately executed at the Sink, resulting in a critical Time-based SQL Injection.
Based on all of it we have an attack flow :

Proof Of Concept POC
Based on the source code analysis and data flow tracing, we can successfully exploit this Time-Based SQL Injection vulnerability by injecting payloads into the wpfob parameter.

First, we need to establish a baseline. Sending a standard GET /community/recent/ request without any payload takes approximately 23 seconds to process on our specific test environment.

Next, we inject the sleep(5) payload via the wpfob parameter (?wpfob=sleep(5)). The server response time increases to roughly 29 seconds. This demonstrates a clear 5-to-6-second delay directly caused by our injected SQL command.

To definitively confirm the vulnerability and rule out random network latency, we increase the payload to sleep(10). As expected, the response time jumps to 34 seconds (baseline + 10 seconds). This precise control over the database’s execution time conclusively proves the existence of the SQL Injection vulnerability.