CVE-2026-32401

Client Invoicing by Sprout Invoices <= 20.8.9 - Authenticated (Author+) Local File Inclusion

highImproper Control of Filename for Include/Require Statement in PHP Program ('PHP Remote File Inclusion')
7.5
CVSS Score
7.5
CVSS Score
high
Severity
20.8.10
Patched in
54d
Time to patch

Description

The Client Invoicing by Sprout Invoices plugin for WordPress is vulnerable to Local File Inclusion in versions up to, and including, 20.8.9. This makes it possible for authenticated attackers, with author-level access and above, to include and execute arbitrary files on the server, allowing the execution of any PHP code in those files. This can be used to bypass access controls, obtain sensitive data, or achieve code execution in cases where images and other "safe" file types can be uploaded and included.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=20.8.9
PublishedFebruary 21, 2026
Last updatedApril 15, 2026
Affected pluginsprout-invoices

What Changed in the Fix

Changes introduced in v20.8.10

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Research Plan: CVE-2026-32401 - Authenticated LFI in Sprout Invoices ## Vulnerability Summary The **Client Invoicing by Sprout Invoices** plugin (versions <= 20.8.9) is vulnerable to **Local File Inclusion (LFI)**. The vulnerability exists in the way the plugin handles "Doc Templates" for Invoice…

Show full research plan

Research Plan: CVE-2026-32401 - Authenticated LFI in Sprout Invoices

Vulnerability Summary

The Client Invoicing by Sprout Invoices plugin (versions <= 20.8.9) is vulnerable to Local File Inclusion (LFI). The vulnerability exists in the way the plugin handles "Doc Templates" for Invoices and Estimates. An authenticated user with Author-level permissions or higher can modify the template associated with an invoice or estimate to point to an arbitrary file on the server. When that document is subsequently viewed on the frontend, the plugin includes the specified file via a template_include filter, leading to PHP code execution or sensitive information disclosure.

Attack Vector Analysis

  • Endpoint: Frontend view of an Invoice (sa_invoice) or Estimate (sa_estimate).
  • Vulnerable Parameter: The post meta field _doc_template_option (defined as SI_Templating_API::TEMPLATE_OPTION).
  • Injection Point: The post update process (wp-admin/post.php) where the template selection is saved.
  • Authentication: Author-level access (user must be able to create or edit their own Invoices/Estimates).
  • Preconditions: An invoice or estimate must exist or be created by the attacker.

Code Flow

  1. Registration: SI_Templating_API::init() (in controllers/templating/Templating.php) registers the template_include filter:
    add_filter( 'template_include', array( __CLASS__, 'override_template' ) );
    
  2. Metadata Setup: SI_Templating_API::init() also hooks the saving of the template selection:
    add_action( 'si_save_line_items_meta_box', array( __CLASS__, 'save_doc_template_selection' ) );
    
  3. Saving: When an invoice is saved, save_doc_template_selection (inferred) reads a POST parameter (likely sa_doc_template) and updates the post meta _doc_template_option.
  4. Retrieval: When viewing the document, override_template is triggered. It calls get_doc_current_template($doc_id):
    public static function get_doc_current_template( $doc_id ) {
        $template_id = get_post_meta( $doc_id, self::TEMPLATE_OPTION, true );
        // ... returns the malicious path ...
    }
    
  5. Inclusion (Sink): override_template uses the retrieved $template_id to locate a file. If the plugin fails to sanitize this path and passes it back to WordPress or calls include/require directly, the file is included.

Nonce Acquisition Strategy

The vulnerability requires updating post meta, which is typically done through the standard WordPress post edit screen.

  1. Navigate to the Edit Page: Use browser_navigate to go to the edit screen of an existing sa_invoice.
  2. Extract Nonces: Use browser_eval to extract the standard WordPress nonces required for post.php.
    • _wpnonce: document.querySelector('#_wpnonce').value
  3. Identify Field Name: Verify the exact name of the template selection dropdown.
    • browser_eval("document.querySelector('select[name*=\"template\"]')?.name")
    • Based on the hook si_save_line_items_meta_box, the expected parameter name is sa_doc_template.

Exploitation Strategy

  1. Setup:
    • Create a user with the author role.
    • Create a new sa_invoice post via WP-CLI.
  2. Inject Malicious Path:
    • Use the http_request tool to send a POST request to wp-admin/post.php.
    • Set action=editpost.
    • Set post_ID to the ID of the created invoice.
    • Set sa_doc_template (or the discovered field name) to the traversal path: ../../../../../../../../../../../../etc/passwd.
    • Include the necessary _wpnonce.
  3. Trigger Inclusion:
    • Find the frontend URL for the invoice (e.g., ?post_type=sa_invoice&p=[ID]).
    • Use the http_request tool to perform a GET request to that URL while authenticated as the author.
  4. Capture Output:
    • The response body should contain the contents of /etc/passwd.

Test Data Setup

  1. Author User:
    wp user create attacker attacker@example.com --role=author --user_pass=password
    
  2. Invoice Post:
    wp post create --post_type=sa_invoice --post_status=publish --post_title="LFI Test" --post_author=$(wp user get attacker --field=ID)
    

Expected Results

  • A successful POST request to post.php will return a 302 redirect back to the edit page.
  • A GET request to the invoice frontend URL will return a 200 OK response.
  • The response body will contain /etc/passwd content (e.g., root:x:0:0:root:/root:/bin/bash).

Verification Steps

  1. Check Meta State:
    wp post meta get [POST_ID] _doc_template_option
    
    Confirm it matches the injected path.
  2. Verify Response:
    Check the http_request output for the string root:x:0:0:.

Alternative Approaches

  • PHP Filter Wrapper: If /etc/passwd is blocked or if the plugin appends .php, try:
    php://filter/convert.base64-encode/resource=wp-config
  • Uploaded Payload: If the plugin only includes files relative to the uploads directory:
    1. Upload a text file poc.txt containing <?php echo "VULNERABLE"; ?> via the Media Library.
    2. Get its path in wp-content/uploads/.
    3. Set sa_doc_template to ../../../../uploads/YYYY/MM/poc.txt.
  • Estimate Post Type: If sa_invoice is restricted, repeat the process using sa_estimate.
Research Findings
Static analysis — not yet PoC-verified

Summary

The Sprout Invoices plugin for WordPress is vulnerable to Local File Inclusion (LFI) because it fails to properly validate the 'doc_template' parameter when saving invoice or estimate metadata. This allows authenticated attackers with Author-level permissions or higher to specify arbitrary file paths, including those with directory traversal, which are subsequently executed via the plugin's template inclusion logic.

Vulnerable Code

// controllers/templating/Templating.php:686
public static function save_doc_template_selection( $post_id = 0 ) {
    $doc_template = ( isset( $_POST['doc_template'] ) ) ? sanitize_text_field( wp_unslash( $_POST['doc_template'] ) ) : '' ;
    self::save_doc_current_template( $post_id, $doc_template );
}

---

// controllers/_Controller.php:444
// Note: This logic retrieves the stored path and checks for its existence within the templates folder without traversal protection
foreach ( $possibilities as $p ) {
    if ( file_exists( SI_PATH.'/views/templates/'.$p ) ) {
        return SI_PATH.'/views/templates/'.$p;
    }
}

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/sprout-invoices/20.8.9/controllers/_Controller.php /home/deploy/wp-safety.org/data/plugin-versions/sprout-invoices/20.8.10/controllers/_Controller.php
--- /home/deploy/wp-safety.org/data/plugin-versions/sprout-invoices/20.8.9/controllers/_Controller.php	2026-02-04 16:51:30.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/sprout-invoices/20.8.10/controllers/_Controller.php	2026-02-16 21:08:28.000000000 +0000
@@ -433,6 +433,21 @@
 	protected static function locate_template( $possibilities, $default = '' ) {
 		$possibilities = apply_filters( 'sprout_invoice_template_possibilities', $possibilities );
 		$possibilities = array_filter( $possibilities );
+
+		// Security: Validate each possibility for path traversal attempts
+		foreach ( $possibilities as $key => $p ) {
+			// Reject any path containing directory traversal sequences
+			if ( empty( $p ) || strpos( $p, '..' ) !== false || strpos( $p, "\0" ) !== false ) {
+				unset( $possibilities[ $key ] );
+				// Only log in debug mode to prevent DoS via database flooding
+				if ( self::DEBUG ) {
+					do_action( 'si_error', 'Path traversal attempt detected in template path', array(
+						'attempted_path' => $p,
+					) );
+				}
+			}
+		}
+
 		// check if the theme has an override for the template
 		$theme_overrides = array();
 		foreach ( $possibilities as $p ) {
@@ -444,9 +459,16 @@
 		}
 
 		// check for it in the templates directory
+		// Security: Compute base path once before loop to avoid repeated filesystem calls
+		$base_path = realpath( SI_PATH . '/views/templates/' );
+
 		foreach ( $possibilities as $p ) {
-			if ( file_exists( SI_PATH.'/views/templates/'.$p ) ) {
-				return SI_PATH.'/views/templates/'.$p;
+			$template_path = SI_PATH . '/views/templates/' . $p;
+			// Security: Ensure the resolved path is within the templates directory
+			$real_path = realpath( $template_path );
+
+			if ( $real_path && $base_path && strpos( $real_path, $base_path ) === 0 && file_exists( $real_path ) ) {
+				return $real_path;
 			}
 		}
 
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/sprout-invoices/20.8.9/controllers/templating/Templating.php /home/deploy/wp-safety.org/data/plugin-versions/sprout-invoices/20.8.10/controllers/templating/Templating.php
--- /home/deploy/wp-safety.org/data/plugin-versions/sprout-invoices/20.8.9/controllers/templating/Templating.php	2026-02-04 16:51:30.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/sprout-invoices/20.8.10/controllers/templating/Templating.php	2026-02-16 21:08:28.000000000 +0000
@@ -686,6 +686,32 @@
 	 */
 	public static function save_doc_template_selection( $post_id = 0 ) {
 		$doc_template = ( isset( $_POST['doc_template'] ) ) ? sanitize_text_field( wp_unslash( $_POST['doc_template'] ) ) : '' ;
+
+		// Security: Validate template against whitelist to prevent path traversal attacks
+		if ( '' !== $doc_template ) {
+			$post_type = get_post_type( $post_id );
+			$valid_templates = array();
+
+			// Get valid templates based on post type
+			if ( SI_Invoice::POST_TYPE === $post_type ) {
+				$valid_templates = array_keys( self::get_invoice_templates() );
+			} elseif ( SI_Estimate::POST_TYPE === $post_type ) {
+				$valid_templates = array_keys( self::get_estimate_templates() );
+			}
+
+			// Only save if template is in the whitelist
+			if ( ! in_array( $doc_template, $valid_templates, true ) ) {
+				// Invalid template - reject and log (only in debug mode to prevent log flooding)
+				if ( self::DEBUG ) {
+					do_action( 'si_error', 'Invalid template selection attempted', array(
+						'post_id' => $post_id,
+						'attempted_template' => $doc_template,
+					) );
+				}
+				$doc_template = ''; // Reset to default
+			}
+		}
+
 		self::save_doc_current_template( $post_id, $doc_template );
 	}

Exploit Outline

To exploit this vulnerability, an attacker with at least Author-level access follows these steps: 1. Create a new Invoice (`sa_invoice`) or Estimate (`sa_estimate`) or edit an existing one they own. 2. When saving the document, intercept the POST request to `wp-admin/post.php` (action `editpost`). 3. Inject a directory traversal payload into the `doc_template` parameter, such as `../../../../../../etc/passwd` or `../../../../wp-config.php` (potentially using PHP filters to encode the output). 4. Update the post, which saves the malicious path into the `_doc_template_option` post meta. 5. Access the public-facing URL of the Invoice or Estimate. The plugin's `template_include` logic will trigger `SI_Templating_API::override_template`, which eventually calls `SI_Controller::locate_template`. Because the path is not restricted to the views directory, the server will include and execute the targeted file.

Check if your site is affected.

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