CVE-2026-0702 WordPress VidShop Plugin <= 1.1.4 is vulnerable to a high priority SQL Injection

image

VidShop Plugin Vulnerable to SQL Injection

Overview

  • Published: 2026-01-28
  • CVE-ID: CVE-2026-0702
  • CVSS: 7.5 High
  • Affected Plugin: VidShop Plugin
  • Affected Versions: <= 1.1.4
  • Vulnerability Type: High priority SQL Injection
  • CWE: CWE-89 Improper Neutralization of Special Elements used in an SQL Command

Description

The VidShop – Shoppable Videos for WooCommerce plugin for WordPress is vulnerable to time-based SQL Injection via the ‘fields’ parameter in all versions up to, and including, 1.1.4 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

image

Based on the development change log, version 1.1.4 was identified as the vulnerable version, and version 1.1.5 was selected to perform a patch differential analysis. The changelog explicitly mentions “Added column name validation in Query Builder” alongside the API improvements.

Vulnerability Analysis (Version 1.1.4)
Analysis of the code diffs reveals a critical chain of vulnerabilities stemming from a logic flaw in the Query Builder and a lack of validation in the Controller.

Root Cause: Insecure Query Builder Logic

  • In includes/Utils/Query_Builder.php (Version 1.1.4), the select() method accepted an array of columns without validation. Crucially, the internal logic allowed raw SQL injection if the column name contained specific keywords like AS or . to support aliases. This design flaw meant that if an attacker could pass a string containing AS to the select() method, the builder would not escape it, executing it as raw SQL.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function select( $columns = array( '*' ) ) {
$this->columns = is_array( $columns ) ? $columns : func_get_args();
return $this;
}
/**
* Set raw SQL for the SELECT clause (for aggregation queries)
*
* @param string $raw_sql The raw SQL for SELECT clause
* @return $this
*/
public function select_raw( $raw_sql ) {
$this->raw_columns = $raw_sql;
return $this;
}

Entry Point: Lack of Input Sanitization

  • In includes/REST_API/V1/Videos_Controller.php (Version 1.1.4), the API parameters fields and ids were defined without sanitize_callback. This allowed unvalidated user input to be passed directly to the vulnerable Query Builder.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public function get_items( $request ) {
$page = $request->get_param( 'page' );
$per_page = $request->get_param( 'per_page' );
$search = $request->get_param( 'search' );
$status = $this->check_private_permission( $request ) ? $request->get_param( 'status' ) : 'published';
$orderby = $request->get_param( 'orderby' );
$order = $request->get_param( 'order' );
$fields = $request->get_param( 'fields' );
$ids = $request->get_param( 'ids' );

$query = Video_Model::query();

if ( $ids ) {
$ids = explode( ',', $ids );
$query->where_in( 'id', $ids );
}
.....

if ( $fields ) {
$selected_fields = explode( ',', $fields );
$query->select( $selected_fields );
}

Patch Analysis (Version 1.1.5)
In version 1.1.5, the vendor applied a Defense in Depth strategy, fixing the vulnerability at both the Entry Point (Controller) and the Root Cause (Query Builder).

Layer 1: Hardening the Root Cause (Query Builder)

Query_Builder.php

As seen in the Query_Builder.php diff, the developer implemented strict validation logic to prevent bypasses:

  • is_valid_column_name(): A new protected method was added using Regular Expressions (preg_match) to enforce strict naming conventions:
  • Standard columns must match /^[a-zA-Z_][a-zA-Z0-9_]*$/ (Alphanumeric and underscores only).
  • Aliases using AS are now strictly parsed to ensure both the column and the alias are safe, preventing the injection of special characters like parentheses () or comments --.
  • sanitize_columns(): This method filters the input array using the validator above.
  • select() Update: The select method now explicitly calls $this->sanitize_columns() before processing the query, ensuring that even if bad data reaches this layer, it is neutralized.

Layer 2: Securing the Entry Point (Controller)

wtyK9IqSfx

RRMARf4NPR

Nd4ploI4Ya

In Videos_Controller.php, the developer implemented input validation to reject malicious requests early:

  • Whitelist Implementation: Added get_allowed_fields() to define a hardcoded list of permissible columns (e.g., id, title).
  • Sanitization Callbacks: Added sanitize_fields_param and sanitize_ids_param to the API route definition. This ensures that the fields parameter is stripped of any values not in the whitelist, and ids are forced to integers using absint.
  • Prepared Statements: In the get_items method, raw SQL concatenation in the ORDER BY clause was replaced with $wpdb->prepare, using %d placeholders for IDs.

Data Flow Analysis: Sink-to-Source Trace

The Sink (Vulnerability enforcement point)

  • Location: includes/Utils/Query_Builder.php
  • Method: to_sql()

This is where the final SQL statement is built and prepared to be sent to the Database. The vulnerability lies in the column name checking logic.

1
2
3
if ( strpos( $column, '.' ) !== false || stripos( $column, ' as ' ) !== false ) {
return $column; // <--- SINK: Return raw sql query without escape
}

Explanation: If the $column variable contains the keyword AS or a dot, it will bypass the backticks and be appended directly to the SELECT statement.

Propagation (Data propagation)

  • Location: includes/Utils/Query_Builder.php
  • Method: select()

Malicious data from the Controller is transferred to the Query Builder here.

1
2
3
4
5
public function select( $columns = array( '*' ) ) {
// Dirty data is assigned to class properties
$this->columns = is_array( $columns ) ? $columns : func_get_args();
return $this;
}

The Source (Input data source)

  • Location: includes/REST_API/V1/Videos_Controller.php
  • Method: get_items()

This is the entry point (Entry Point), where hackers inject payload into the system via API Request.

1
2
3
4
5
6
7
8
9
10
11
12
// Receive raw input from request (Tainted Data)
$fields = $request->get_param( 'fields' );

// ...

if ( $fields ) {
// Convert string to array
$selected_fields = explode( ',', $fields );

// Pass dirty data into Query Builder (Propagation)
$query->select( $selected_fields );
}

SQL Injection Attack-2026-02-14-084219

Proof Of Concept POC

The vulnerability is a Time-Based Blind SQL Injection affecting the fields parameter. To confirm the flaw, we compare the response time of a legitimate request against a request containing a SQL SLEEP() command.

Prerequisite: The VidShop plugin must have at least one video created in the dashboard. If the database table is empty, the SQL query will return an empty set immediately, and the SLEEP() function will not execute.

image

image

First, we send a standard GET request to the API endpoint asking for valid columns (id and title).

  • Request: GET /wp-json/vsfw/v1/videos?fields=id,title
  • Observation: The server processes the request normally.
  • Response Time: Approximately 1.1 seconds.

image

Next, we inject a** time-based payload**. We append (SELECT SLEEP(5)) AS injection to the fields parameter. The AS keyword is critical here, as it triggers the specific logic flaw in the Query_Builder that bypasses backtick escaping.

  • Payload: (SELECT SLEEP(5)) AS injection
  • Request: GET /wp-json/vsfw/v1/videos?fields=id,title,(SELECT+SLEEP(5))+AS+injection
  • Observation: The server executes the injected SQL command, causing the database to sleep for 5 seconds before returning the response.
  • Response Time: Approximately 6.1 seconds (1.1s baseline + 5s sleep).

The significant delay of exactly 5 seconds in the second request confirms that the arbitrary SQL command was successfully executed by the database. This proves the existence of an unauthenticated SQL Injection vulnerability in the fields parameter.