CVE-2026-9857

Invoice123 <= 1.7.0 - Missing Authorization to Authenticated (Subscriber+) Setting Modification via s123_submit_api_key & s123_submit_invoice_settings AJAX actions

mediumMissing Authorization
4.3
CVSS Score
4.3
CVSS Score
medium
Severity
1.7.1
Patched in
1d
Time to patch

Description

The Invoice123 plugin for WordPress is vulnerable to authorization bypass in all versions up to, and including, 1.7.0. This is due to the plugin not properly verifying that a user is authorized to perform an action. This makes it possible for authenticated attackers, with subscriber-level access and above, to overwrite the plugin's API key stored in wp_options, modify invoice plugin settings, and alter WooCommerce tax rate data in the wp_woocommerce_tax_rates table.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
None
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=1.7.0
PublishedJuly 9, 2026
Last updatedJuly 10, 2026
Affected pluginsaskaita123-lt

What Changed in the Fix

Changes introduced in v1.7.1

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2026-9857 (Invoice123) ## 1. Vulnerability Summary The **Invoice123** plugin (<= 1.7.0) fails to implement capability checks on two AJAX actions: `s123_submit_api_key` and `s123_submit_invoice_settings`. While these actions are protected by a WordPress nonce (`s123…

Show full research plan

Exploitation Research Plan: CVE-2026-9857 (Invoice123)

1. Vulnerability Summary

The Invoice123 plugin (<= 1.7.0) fails to implement capability checks on two AJAX actions: s123_submit_api_key and s123_submit_invoice_settings. While these actions are protected by a WordPress nonce (s123_security), they lack any current_user_can() check. This allows any authenticated user (including low-privileged Subscribers) to modify the plugin's API key, overwrite critical invoice settings, and manipulate WooCommerce tax rate mappings in the database.

2. Attack Vector Analysis

  • Endpoints: /wp-admin/admin-ajax.php
  • AJAX Actions:
    • s123_submit_api_key (Registered in S123\Includes\Pages\S123_ApiKey::s123_register)
    • s123_submit_invoice_settings (Registered in S123\Includes\Pages\S123_InvoiceSettings::s123_register)
  • Authentication: Authenticated (Subscriber+)
  • Parameters:
    • action: s123_submit_api_key or s123_submit_invoice_settings
    • s123_security: The nonce value (Action name: s123_security)
    • api_key: The new API key to set (for s123_submit_api_key)
    • api_vats[]: An array of strings in format {api_vat_id}-{woo_tax_id} (for s123_submit_invoice_settings)
  • Preconditions:
    1. Plugin active.
    2. WooCommerce active (to verify tax rate modification).
    3. A valid Subscriber account.

3. Code Flow

API Key Modification (S123_ApiKey.php)

  1. User sends POST request to admin-ajax.php with action=s123_submit_api_key.
  2. s123_submit_api_key() is called.
  3. wp_verify_nonce($_POST['s123_security'], 's123_security') validates the nonce.
  4. Security Gap: No current_user_can('manage_options') check follows.
  5. saveApiKey($data) is called.
  6. $this->s123_update_options($options) writes the attacker-controlled api_key to the wp_options table.

Invoice Settings & Tax Mapping (S123_InvoiceSettings.php)

  1. User sends POST request with action=s123_submit_invoice_settings.
  2. s123_submit_invoice_settings() validates the nonce.
  3. Security Gap: No capability check.
  4. If api_vats is present, mapVats() is called.
  5. mapVats() iterates through api_vats, explodes the string by -, and executes:
    UPDATE {$wpdb->prefix}woocommerce_tax_rates SET s123_tax_id='$vat[0]' WHERE tax_rate_id='$vat[1]'
  6. This allows arbitrary modification of the s123_tax_id column in the WooCommerce tax table.

4. Nonce Acquisition Strategy

The nonce s123_security is required. In version 1.7.0, the plugin typically registers settings pages. Low-privileged users might find this nonce in the admin dashboard if the script is enqueued globally.

  1. Search Admin Pages: Low-privileged users can access wp-admin/profile.php. Check if the plugin enqueues its localization object there.
  2. Browser Extraction:
    • Login as a Subscriber.
    • Navigate to /wp-admin/profile.php.
    • Use browser_eval to search for the nonce:
      // Look for localized data. Common patterns for this plugin:
      // window.s123_vars.s123_security
      // window.s123_params.s123_security
      Object.keys(window).filter(k => k.includes('s123')).forEach(k => console.log(k, window[k]));
      
  3. Specific Variable: Based on common patterns in this plugin's era, check for a localized object (e.g., s123_obj or s123_data) containing the s123_security key.

5. Exploitation Strategy

Step 1: Obtain Nonce

Use the browser_navigate and browser_eval tools as a Subscriber to extract the s123_security nonce from the page source or global JS variables.

Step 2: Overwrite API Key

Submit an HTTP request to change the plugin's API key.

  • Action: s123_submit_api_key
  • Method: POST
  • URL: [TARGET]/wp-admin/admin-ajax.php
  • Body (URL-encoded):
    action=s123_submit_api_key&s123_security=[NONCE]&api_key=pwned_api_key
    
  • Expected Response: JSON success message {"success": true, "data": "..."} or a validation error from makeRequestToValidateKey (the key is saved before the request).

Step 3: Modify WooCommerce Tax Rates

Submit an HTTP request to modify tax mappings.

  • Action: s123_submit_invoice_settings
  • Method: POST
  • URL: [TARGET]/wp-admin/admin-ajax.php
  • Body (URL-encoded):
    action=s123_submit_invoice_settings&s123_security=[NONCE]&api_vats[]=99999-1&use_custom_inputs=1
    
    (Note: 1 in 99999-1 assumes a tax rate with ID 1 exists in the wp_woocommerce_tax_rates table).

6. Test Data Setup

  1. User: Create a Subscriber user (e.g., attacker / attacker123).
  2. Plugin: Install and activate saskaita123-lt version 1.7.0.
  3. WooCommerce: Ensure WooCommerce is installed and at least one tax rate is created.
    • wp eval "BWC_Tax::insert_tax_rate(array('tax_rate_country' => 'US', 'tax_rate_id' => 1));" (or via UI).

7. Expected Results

  1. Response: The AJAX response should return HTTP 200 with a JSON payload indicating success (e.g., "API Key saved successfully!").
  2. API Key: The WordPress option (likely s123_options or similar) will contain api_key => 'pwned_api_key'.
  3. Tax Table: The table wp_woocommerce_tax_rates will have the s123_tax_id column updated to 99999 for the row where tax_rate_id is 1.

8. Verification Steps

After the exploit, use WP-CLI to verify the state:

  1. Check API Key:
    wp option get s123_options --format=json
    
  2. Check Tax Rates:
    wp db query "SELECT tax_rate_id, s123_tax_id FROM $(wp db prefix)woocommerce_tax_rates WHERE tax_rate_id=1"
    

9. Alternative Approaches

If the nonce is not found on profile.php:

  1. Check if the plugin adds an admin menu item that a Subscriber can see (unlikely).
  2. Check for frontend nonces: Some Invoice123 versions may enqueue scripts on the frontend if WooCommerce "My Account" or "Checkout" pages are active.
    • wp post create --post_type=page --post_content='[woocommerce_my_account]' --post_status=publish
    • Navigate to this page and search for s123_security.
  3. If makeRequestToValidateKey() in S123_ApiKey.php blocks success because the key is fake, note that saveApiKey() is executed before the validation request. The change is persistent even if the AJAX response returns an error.
Research Findings
Static analysis — not yet PoC-verified

Summary

The Invoice123 plugin for WordPress (<= 1.7.0) contains an authorization bypass vulnerability in its AJAX handlers for API key and invoice setting modifications. Due to a lack of capability checks, authenticated users with subscriber-level permissions can overwrite the plugin's API key and manipulate WooCommerce tax rate mappings in the database.

Vulnerable Code

// includes/pages/S123_ApiKey.php:27
public function s123_submit_api_key()
{
    if (isset($_POST['s123_security']) && wp_verify_nonce($_POST['s123_security'], 's123_security')) {
        $keys = ['api_key'];
        $data = [];

        foreach ($keys as $key) {
            $data[$key] = isset($_POST[$key]) ? sanitize_text_field(trim($_POST[$key])) : null;
        }

        if (!isset($_POST Bryan["api_key"]) || $_POST["api_key"] === '') {
            S123_ResponseHelpers::s123_sendErrorResponse(__('API Key cannot be empty!', 's123-invoices'));
        }

        $this->saveApiKey($data);

        // send versions and validate api key
        $this->makeRequestToValidateKey();
    } else {
        S123_ResponseHelpers::s123_sendErrorResponse(__('Invalid secret key specified.', 's123-invoices'));
    }
}

---

// includes/pages/S123_InvoiceSettings.php:24
public function s123_submit_invoice_settings()
{
    if (isset($_POST['s123_security']) && wp_verify_nonce($_POST['s123_security'], 's123_security')) {
        $keys = ['use_custom_inputs', 'use_order_status', 'default_advanced_invoices', 'create_invoice_on_order_creation', 'create_unpaid_invoices'];
        $data = [];
        if (isset($_POST['api_vats'])) {
            // map woocommerce vat with app.invoice123.com vat
            $this->mapVats($_POST['api_vats']);
        }

        foreach ($keys as $key) {
            $data[$key] = isset($_POST[$key]) ? sanitize_text_field(trim($_POST[$key])) : null;
        }
// ...

---

// includes/pages/S123_InvoiceSettings.php:56
public function mapVats($vats)
{
    foreach ($vats as $vat) {
        $vat = sanitize_text_field(trim($vat));
        // [0] api vat id, [1] woo tax vat id
        $vat = explode('-', $vat);

        global $wpdb;
        $tableName = $wpdb->prefix . "woocommerce_tax_rates";
        $wpdb->query($wpdb->prepare("UPDATE {$tableName} SET s123_tax_id='$vat[0]' WHERE tax_rate_id='$vat[1]'"));
    }
}

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/saskaita123-lt/1.7.0/includes/pages/S123_ApiKey.php /home/deploy/wp-safety.org/data/plugin-versions/saskaita123-lt/1.7.1/includes/pages/S123_ApiKey.php
--- /home/deploy/wp-safety.org/data/plugin-versions/saskaita123-lt/1.7.0/includes/pages/S123_ApiKey.php	2023-06-15 12:43:10.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/saskaita123-lt/1.7.1/includes/pages/S123_ApiKey.php	2026-07-03 08:08:26.000000000 +0000
@@ -26,6 +26,10 @@
      */
     public function s123_submit_api_key()
     {
+        if (!current_user_can('manage_woocommerce')) {
+            S123_ResponseHelpers::s123_sendErrorResponse(__('Sorry, you are not allowed to perform this action.', 's123-invoices'));
+        }
+
         if (isset($_POST['s123_security']) && wp_verify_nonce($_POST['s123_security'], 's123_security')) {
             $keys = ['api_key'];
             $data = [];
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/saskaita123-lt/1.7.0/includes/pages/S123_InvoiceSettings.php /home/deploy/wp-safety.org/data/plugin-versions/saskaita123-lt/1.7.1/includes/pages/S123_InvoiceSettings.php
--- /home/deploy/wp-safety.org/data/plugin-versions/saskaita123-lt/1.7.0/includes/pages/S123_InvoiceSettings.php	2026-05-25 07:32:52.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/saskaita123-lt/1.7.1/includes/pages/S123_InvoiceSettings.php	2026-07-03 08:08:26.000000000 +0000
@@ -23,6 +23,10 @@
      */
     public function s123_submit_invoice_settings()
     {
+        if (!current_user_can('manage_woocommerce')) {
+            S123_ResponseHelpers::s123_sendErrorResponse(__('Sorry, you are not allowed to perform this action.', 's123-invoices'));
+        }
+
         if (isset($_POST['s123_security']) && wp_verify_nonce($_POST['s123_security'], 's123_security')) {
             $keys = ['use_custom_inputs', 'use_order_status', 'default_advanced_invoices', 'create_invoice_on_order_creation', 'create_unpaid_invoices'];
             $data = [];
@@ -55,14 +59,21 @@
 
     public function mapVats($vats)
     {
+        global $wpdb;
+        $tableName = $wpdb->prefix . "woocommerce_tax_rates";
+
         foreach ($vats as $vat) {
             $vat = sanitize_text_field(trim($vat));
+            $parts = explode('-', $vat);
+            if (count($parts) < 2) {
+                continue;
+            }
             // [0] api vat id, [1] woo tax vat id
-            $vat = explode('-', $vat);
-
-            global $wpdb;
-            $tableName = $wpdb->prefix . "woocommerce_tax_rates";
-            $wpdb->query($wpdb->prepare("UPDATE {$tableName} SET s123_tax_id='$vat[0]' WHERE tax_rate_id='$vat[1]'"));
+            $wpdb->query($wpdb->prepare(
+                "UPDATE {$tableName} SET s123_tax_id = %s WHERE tax_rate_id = %d",
+                $parts[0],
+                $parts[1]
+            ));
         }
     }

Exploit Outline

To exploit this vulnerability, an authenticated attacker first needs to obtain a valid WordPress nonce named 's123_security'. This nonce is often exposed to subscribers in the administrative interface via localized scripts if the plugin enqueues settings-related JavaScript globally. Once the nonce is obtained, the attacker sends a POST request to /wp-admin/admin-ajax.php using either the 's123_submit_api_key' or 's123_submit_invoice_settings' actions. By including the 'api_key' parameter in the first action, an attacker can overwrite the store's API connection string. Alternatively, by using the 'api_vats[]' parameter in the second action (formatted as 'new_tax_id-target_tax_rate_id'), the attacker can directly modify values in the wp_woocommerce_tax_rates database table.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.