Client Invoicing by Sprout Invoices <= 20.8.10 - Missing Authorization
Description
The Client Invoicing by Sprout Invoices plugin for WordPress is vulnerable to unauthorized access due to a missing capability check on a function in versions up to, and including, 20.8.10. This makes it possible for unauthenticated attackers to perform an unauthorized action.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:NTechnical Details
<=20.8.10What Changed in the Fix
Changes introduced in v20.8.11
Source Code
WordPress.org SVN# Exploitation Research Plan - CVE-2026-39562 ## 1. Vulnerability Summary The **Client Invoicing by Sprout Invoices** plugin (<= 20.8.10) contains a missing authorization vulnerability within its AJAX handlers. Specifically, the functions `maybe_create_private_note` and `maybe_change_status` are re…
Show full research plan
Exploitation Research Plan - CVE-2026-39562
1. Vulnerability Summary
The Client Invoicing by Sprout Invoices plugin (<= 20.8.10) contains a missing authorization vulnerability within its AJAX handlers. Specifically, the functions maybe_create_private_note and maybe_change_status are registered with both wp_ajax_ and wp_ajax_nopriv_ hooks in controllers/_Controller.php, but they fail to perform adequate capability checks (e.g., current_user_can( 'edit_sprout_invoices' )).
This allows unauthenticated attackers to perform unauthorized actions such as creating private notes on invoices/estimates or changing the status of a document (e.g., marking an invoice as "paid" or "void").
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - Action:
si_change_doc_status(for status manipulation) orsa_create_private_note(for adding notes). - Authentication: None required (exploiting the
noprivhooks). - Parameters:
action:si_change_doc_statusid: The post ID of the targetsa_invoiceorsa_estimate.status: The desired new status (e.g.,paid,void, `temp
Summary
The Sprout Invoices plugin for WordPress is vulnerable to unauthorized status manipulation due to missing authorization and capability checks in its AJAX handlers. This allows unauthenticated attackers to change the status of any invoice or estimate—for example, marking an invoice as 'paid' or 'void'—by exploiting functions registered with unauthenticated (nopriv) access hooks.
Vulnerable Code
// controllers/_Controller.php // Line 98-101: Registration of nopriv hooks allows unauthenticated access to status change functions add_action( 'wp_ajax_sa_create_private_note', array( static::class, 'maybe_create_private_note' ), 10, 0 ); add_action( 'wp_ajax_nopriv_sa_create_private_note', array( static::class, 'maybe_create_private_note' ), 10, 0 ); add_action( 'wp_ajax_si_change_doc_status', array( static::class, 'maybe_change_status' ), 10, 0 ); add_action( 'wp_ajax_nopriv_si_change_doc_status', array( static::class, 'maybe_change_status' ), 10, 0 ); --- // controllers/_Controller.php around line 1090 public static function maybe_change_status() { // ... nonce checks usually follow, but specific capability checks are missing ... $doc_id = ( isset( $_REQUEST['id'] ) ) ? $_REQUEST['id'] : 0 ; if ( ! $doc_id ) { return; } $view = ''; $new_status = sanitize_text_field( wp_unslash( $_REQUEST['status'] ) ); switch ( get_post_type( $doc_id ) ) { case SI_Invoice::POST_TYPE: $doc = SI_Invoice::get_instance( $doc_id ); $doc->set_status( $new_status ); $view = self::load_view( 'admin/sections/invoice-status-toggle', array( 'id' => $doc_id, 'status' => $new_status, ), false ); break; case SI_Estimate::POST_TYPE: $doc = SI_Estimate::get_instance( $doc_id ); $doc->set_status( $new_status ); $view = self::load_view( 'admin/sections/estimate-status-toggle', array( 'id' => $doc_id, 'status' => $new_status, ), false ); break; default: break; } // ...
Security Fix
@@ -1092,7 +1090,19 @@ $view = ''; $new_status = sanitize_text_field( wp_unslash( $_REQUEST['status'] ) ); - switch ( get_post_type( $doc_id ) ) { + $post_type = get_post_type( $doc_id ); + + // Unauthenticated users are limited to accepting or declining estimates. + // No frontend UI exposes any other status action to unauthenticated clients: + // all four themes and the embeds bundle only render accept/decline buttons on + // estimates, and no invoice templates expose a status-change button at all. + if ( ! is_user_logged_in() ) { + if ( SI_Estimate::POST_TYPE !== $post_type || ! in_array( $new_status, array( 'accept', 'decline' ), true ) ) { + self::ajax_fail( 'You do not have permission to change this status.' ); + } + } + + switch ( $post_type ) { case SI_Invoice::POST_TYPE: $doc = SI_Invoice::get_instance( $doc_id ); $doc->set_status( $new_status );
Exploit Outline
The exploit targets the AJAX endpoint `/wp-admin/admin-ajax.php`. An unauthenticated attacker sends a POST request with the action `si_change_doc_status`. The payload includes the target document's post ID (`id`) and the desired status (e.g., `paid`, `void`, or `temp`). Because the plugin registers this action via `wp_ajax_nopriv_si_change_doc_status` and fails to check user capabilities or restrict unauthenticated requests to legitimate 'accept/decline' estimate transitions, the backend updates the invoice or estimate status based on the attacker's input. Attackers may also exploit `sa_create_private_note` to inject unauthorized comments onto private records.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.