Automated Threat Modeling

CVE-2026-119155: Unauthenticated Time-Based SQL Injection in wpForo Forum via href[locale], with Patch Analysis

A look at CVE-2026-119155, an unauthenticated time-based SQL injection in the wpForo Forum WordPress plugin through version 3.2.2. It covers the root cause in the board lookup query builder and how version 3.2.3 fixes it.

CVE Analysis

8 min read

Gaurav Sarraf
Gaurav Sarraf

2026-11-07

CVE-2026-119155: Unauthenticated Time-Based SQL Injection in wpForo Forum via href[locale], with Patch Analysis
Experimental AI-Generated Content

This CVE analysis is an experimental publication that is completely AI-generated. The content may contain errors or inaccuracies and is subject to change as more information becomes available. We are continuously refining our process.

If you have feedback, questions, or notice any errors, please reach out to us.

[email protected]

Introduction

Through version 3.2.2, any anonymous visitor who could load a wpForo forum page could reach a board lookup query that placed their input directly into SQL. The only prerequisite is a nonce, and that nonce is handed to every visitor in page JavaScript. So this time-based SQL injection gives unauthenticated attackers a way to extract sensitive information from the site database.

wpForo Forum is a WordPress plugin that adds community forum features, including responsive layouts and moderation tools, to an existing WordPress site. The WordPress.org listing names Tomdever as the author and gVectors Team as a contributor, and WPScan reports about 20,000 active installations. Forum plugins sit directly on public, anonymous request paths, which is why an injection flaw in their request handling matters more than one hidden behind an administrator screen.

Technical Information

Overview

CVE-2026-119155 is classified as CWE-89 (SQL injection). Wordfence, acting as the CNA, scores it CVSS 3.1 7.5 (High) with the vector CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N: network reachable, low complexity, no privileges, no user interaction, and high confidentiality impact. NVD has not supplied an independent score. The injection point is the href[locale] parameter, and the advisory describes the root cause as insufficient escaping combined with a lack of query preparation.

Root cause: esc_like() is not SQL escaping

The Wordfence advisory points to classes/Boards.php lines 293 and 489. Those correspond to the build_sql_select() query builder and the init_current() board resolver. When a forum page loads, init_current() looks up the board by slug and locale, and the locale comes from the caller's arguments:

'locale_like' => ( $locale = (string) wpfval( $args, 'locale' ) ) ? $locale : wpforo_get_site_default_locale(), 'locale_empty' => true,

In 3.2.2, build_sql_select() then concatenated that value into the SQL string:

if( ! is_null( $args['locale_like'] ) ) { $locale_like = "`locale` LIKE '%" . WPF()->db->esc_like( $args['locale_like'] ) . "%'";

The likely assumption was that esc_like() made the value safe. It does not. The WordPress esc_like() helper only escapes the LIKE wildcard characters (% and _) and backslashes so that they match literally. It does not escape single quotes. A ' in the locale value closes the string literal, and whatever follows, for example a SLEEP() expression, is executed as SQL. The locale_notlike, title_like and title_notlike branches of the same builder were built the same way.

This is the distinction OWASP draws in its SQL Injection Prevention guidance: SQL structure must be defined separately from bound data values, and concatenating untrusted data before preparation defeats that protection. Processing a value for LIKE semantics is a different job from keeping data out of SQL syntax.

How attacker input reaches the sink

On the request side, classes/Actions.php registers both the authenticated and unauthenticated AJAX hooks for wpforo_get_member_template, mapping them to get_member_template(). That handler:

  1. Verifies the wpforo_get_member_template nonce.
  2. Reads href from the POST body.
  3. Passes the value to init_current_url(), followed by init_current_object().

This confirms a publicly callable request path rather than an administrator-only interface. The reviewed source excerpts do not independently document every intermediate call between this AJAX handler and the database operation, but the advisory ties the href[locale] parameter to the board lookup, and the vulnerable sink in build_sql_select() consumes the locale supplied to init_current().

Why the nonce is not a barrier

The advisory states that the required nonce is publicly issued to unauthenticated visitors on any forum page via the wpforo.nonces JavaScript object. A single page load is enough to obtain it. WordPress's own developer documentation warns that nonces should never be relied on for authentication or access control; they protect against request misuse, not against an anonymous user who can simply read the token. A request can therefore pass the nonce check while remaining both unauthenticated and unsafe at the database boundary.

Attack flow

Based on the advisory and the source references, exploitation proceeds conceptually as follows:

  1. The attacker loads any public forum page and reads the nonce from the wpforo.nonces JavaScript object.
  2. The attacker sends a request to the publicly registered AJAX action with that nonce and an href array whose locale element contains a single quote followed by attacker-chosen SQL.
  3. The locale value flows into the board lookup, where build_sql_select() places it inside the locale LIKE clause without preparation.
  4. Because the response content does not need to change, the attacker uses conditional delays and measures response time to answer true or false questions about database contents, which is the time-based blind SQL injection technique described by OWASP.
  5. Repeating step 4 lets the attacker extract sensitive information from the database one inference at a time.

No account, prior interaction, or victim action is required.

Impact scope and exploitation status

The supported impact is database confidentiality exposure. The CVSS vector scores integrity and availability as having no impact, and the reviewed sources do not establish remote code execution or any specific stolen dataset. Active exploitation in the wild, named threat actors, and public exploit code have not been publicly confirmed; VulDB reports KEV membership as no. Wordfence credits the discovery to a researcher credited as UKO from Korea University. At the time of our review, the CVE.org record still displayed the identifier as RESERVED, while Wordfence (November 6, 2026) and NVD (November 7, 2026) carried full details.

Patch Information

wpForo Forum 3.2.3 (released 01.11.2026 per the vendor changelog) fixes this bug. The changelog lists it as "Security: Fixed SQL injection vulnerability in board URL handling", and Wordfence marks 3.2.3 as the first patched version. The Trac changeset linked by NVD was behind a bot check and could not be fetched, so the analysis below comes from comparing classes/Boards.php in the public SVN tags 3.2.2 and 3.2.3.

What 3.2.3 changed

The fix keeps esc_like() for its real job, matching wildcards literally, and then hands the value to prepare() with a %s placeholder, which handles the quoting:

if( ! is_null( $args['locale_like'] ) ) { $locale_like = WPF()->db->prepare( "`locale` LIKE %s", '%' . WPF()->db->esc_like( (string) $args['locale_like'] ) . '%' ); if( $args['locale_empty'] ) { $locale_like = "( `locale` = '' OR `locale` IS NULL OR " . $locale_like . " )"; } $wheres[] = $locale_like; } if( ! is_null( $args['locale_notlike'] ) ) { $locale_notlike = WPF()->db->prepare( "`locale` NOT LIKE %s", '%' . WPF()->db->esc_like( (string) $args['locale_notlike'] ) . '%' );

The two title filters, which had the same flaw, were fixed the same way:

$wheres[] = WPF()->db->prepare( "`title` LIKE %s", '%' . WPF()->db->esc_like( (string) $args['title_like'] ) . '%' ); ... $wheres[] = WPF()->db->prepare( "`title` NOT LIKE %s", '%' . WPF()->db->esc_like( (string) $args['title_notlike'] ) . '%' );

Why this works

  • The value is now data, not SQL. prepare() escapes the whole %...% pattern and wraps it in its own quotes, so a ' in href[locale] can no longer close the literal.
  • The two escaping steps are layered in the right order. esc_like() makes wildcards literal, then prepare() makes the result safe as a SQL string. This is the order the WordPress documentation recommends for LIKE queries.
  • Input is cast to a string. The new (string) casts ensure the value is always treated as a string before it reaches esc_like() and prepare(). That matters because the parameter arrives as part of a request array (href[...]).
  • Wrapping the result is safe. The locale_empty branch still joins strings together, but only around a fragment that prepare() has already escaped.

Because the sink itself is fixed in 3.2.3, the same filter arguments are safe regardless of which request path reaches the board lookup. Other parts of build_sql_select() were left unchanged: slug_include and slug_exclude still use esc_sql() inside quotes, the ID lists still use intval() and wpforo_bigintval(), and orderby is still concatenated, although init_current() only passes fixed orderby strings written in the code.

Release 3.2.3 also fixed unrelated security bugs (privilege escalation in theme management and usergroup assignment, and XSS in moderation tools), so updating addresses those as well. The plugin listing reviewed for this post also shows a later 3.2.4 release; any version from 3.2.3 onward includes this fix.

Affected Systems and Versions

  • Product: wpForo Forum plugin for WordPress (NVD vendor identifier: tomdever)
  • Affected: all versions up to and including 3.2.2
  • Fixed: 3.2.3 and later patched releases
  • Vulnerable configuration: any site running an affected version where a forum page is reachable by visitors, since that page load issues the wpforo.nonces object needed to reach the vulnerable AJAX path. No authentication or special plugin setting is required.

Note that the 20,000 active installation figure reported by WPScan describes the plugin's overall footprint, not the number of installations running an affected version.

Vendor Security History

wpForo has a recurring history of SQL injection disclosures, each with its own patch boundary:

  • CVE-2024-3200: SQL injection requiring contributor-level access or higher, allowing sensitive database information extraction; affects versions through 2.3.3, fixed in 2.3.4.
  • CVE-2026-40798: unauthenticated SQL injection allowing sensitive database information extraction; affects versions through 3.0.4, fixed in 3.0.5.
  • CVE-2026-119155: unauthenticated time-based SQL injection through href[locale]; affects versions through 3.2.2, fixed in 3.2.3.

The shift from an authenticated issue in 2024 to two unauthenticated issues in 2026 is worth noting for anyone reviewing the plugin's public request paths, but the available sources do not establish that these flaws share a root cause. Being on a release that fixed an earlier SQL injection does not imply protection from this one. WPScan lists 51 vulnerabilities for the plugin in total; this is a historical inventory, not a count of open issues. On the other side of the ledger, the 3.2.3 changelog shows the maintainers shipping fixes across several vulnerability classes in a single security release.

References

Detect & fix
what others miss

Works with
  • GitHub
  • GitLab
  • Bitbucket
  • Azure DevOps Services
  • Gerrit
  • CVS
  • Jira
  • Linear
  • Slack
  • Security Compass
Explore integrations
Security magnifying glass visualization