CVE-2026-39562

Client Invoicing by Sprout Invoices <= 20.8.10 - Missing Authorization

mediumMissing Authorization
5.3
CVSS Score
5.3
CVSS Score
medium
Severity
20.8.11
Patched in
28d
Time to patch

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:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Unchanged
None
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=20.8.10
PublishedMarch 19, 2026
Last updatedApril 15, 2026
Affected pluginsprout-invoices

What Changed in the Fix

Changes introduced in v20.8.11

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# 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) or sa_create_private_note (for adding notes).
  • Authentication: None required (exploiting the nopriv hooks).
  • Parameters:
    • action: si_change_doc_status
    • id: The post ID of the target sa_invoice or sa_estimate.
    • status: The desired new status (e.g., paid, void, `temp
Research Findings
Static analysis — not yet PoC-verified

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

--- /home/deploy/wp-safety.org/data/plugin-versions/sprout-invoices/20.8.10/controllers/_Controller.php	2026-02-16 21:08:28.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/sprout-invoices/20.8.11/controllers/_Controller.php	2026-03-16 15:33:18.000000000 +0000
@@ -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.