[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fD6sItVuFgx_TaK7-FjNh9kTfTkjtzwqHYYv22Li7Bf8":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":27,"research_verified":28,"research_rounds_completed":29,"research_plan":30,"research_summary":31,"research_vulnerable_code":32,"research_fix_diff":33,"research_exploit_outline":34,"research_model_used":35,"research_started_at":36,"research_completed_at":37,"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":28,"poc_model_used":9,"poc_verification_depth":9,"poc_exploit_code_gated":28,"source_links":38},"CVE-2026-32401","client-invoicing-by-sprout-invoices-authenticated-author-local-file-inclusion","Client Invoicing by Sprout Invoices \u003C= 20.8.9 - Authenticated (Author+) Local File Inclusion","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.","sprout-invoices",null,"\u003C=20.8.9","20.8.10","high",7.5,"CVSS:3.1\u002FAV:N\u002FAC:H\u002FPR:L\u002FUI:N\u002FS:U\u002FC:H\u002FI:H\u002FA:H","Improper Control of Filename for Include\u002FRequire Statement in PHP Program ('PHP Remote File Inclusion')","2026-02-21 00:00:00","2026-04-15 21:12:57",[19],"https:\u002F\u002Fwww.wordfence.com\u002Fthreat-intel\u002Fvulnerabilities\u002Fid\u002F3b3e2629-f50e-4d8f-9a9c-534fbe0ecb3a?source=api-prod",54,[22,23,24,25,26],"Sprout_Invoices.class.php","controllers\u002F_Controller.php","controllers\u002Ftemplating\u002FTemplating.php","readme.txt","sprout-invoices.php","researched",false,3,"# Research Plan: CVE-2026-32401 - Authenticated LFI in Sprout Invoices\n\n## Vulnerability Summary\nThe **Client Invoicing by Sprout Invoices** plugin (versions \u003C= 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.\n\n## Attack Vector Analysis\n- **Endpoint:** Frontend view of an Invoice (`sa_invoice`) or Estimate (`sa_estimate`).\n- **Vulnerable Parameter:** The post meta field `_doc_template_option` (defined as `SI_Templating_API::TEMPLATE_OPTION`).\n- **Injection Point:** The post update process (`wp-admin\u002Fpost.php`) where the template selection is saved.\n- **Authentication:** Author-level access (user must be able to create or edit their own Invoices\u002FEstimates).\n- **Preconditions:** An invoice or estimate must exist or be created by the attacker.\n\n## Code Flow\n1. **Registration:** `SI_Templating_API::init()` (in `controllers\u002Ftemplating\u002FTemplating.php`) registers the `template_include` filter:\n   ```php\n   add_filter( 'template_include', array( __CLASS__, 'override_template' ) );\n   ```\n2. **Metadata Setup:** `SI_Templating_API::init()` also hooks the saving of the template selection:\n   ```php\n   add_action( 'si_save_line_items_meta_box', array( __CLASS__, 'save_doc_template_selection' ) );\n   ```\n3. **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`.\n4. **Retrieval:** When viewing the document, `override_template` is triggered. It calls `get_doc_current_template($doc_id)`:\n   ```php\n   public static function get_doc_current_template( $doc_id ) {\n       $template_id = get_post_meta( $doc_id, self::TEMPLATE_OPTION, true );\n       \u002F\u002F ... returns the malicious path ...\n   }\n   ```\n5. **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`\u002F`require` directly, the file is included.\n\n## Nonce Acquisition Strategy\nThe vulnerability requires updating post meta, which is typically done through the standard WordPress post edit screen.\n1. **Navigate to the Edit Page:** Use `browser_navigate` to go to the edit screen of an existing `sa_invoice`.\n2. **Extract Nonces:** Use `browser_eval` to extract the standard WordPress nonces required for `post.php`.\n   - `_wpnonce`: `document.querySelector('#_wpnonce').value`\n3. **Identify Field Name:** Verify the exact name of the template selection dropdown.\n   - `browser_eval(\"document.querySelector('select[name*=\\\"template\\\"]')?.name\")`\n   - Based on the hook `si_save_line_items_meta_box`, the expected parameter name is `sa_doc_template`.\n\n## Exploitation Strategy\n1. **Setup:**\n   - Create a user with the `author` role.\n   - Create a new `sa_invoice` post via WP-CLI.\n2. **Inject Malicious Path:**\n   - Use the `http_request` tool to send a `POST` request to `wp-admin\u002Fpost.php`.\n   - Set `action=editpost`.\n   - Set `post_ID` to the ID of the created invoice.\n   - Set `sa_doc_template` (or the discovered field name) to the traversal path: `..\u002F..\u002F..\u002F..\u002F..\u002F..\u002F..\u002F..\u002F..\u002F..\u002F..\u002F..\u002Fetc\u002Fpasswd`.\n   - Include the necessary `_wpnonce`.\n3. **Trigger Inclusion:**\n   - Find the frontend URL for the invoice (e.g., `?post_type=sa_invoice&p=[ID]`).\n   - Use the `http_request` tool to perform a `GET` request to that URL while authenticated as the author.\n4. **Capture Output:**\n   - The response body should contain the contents of `\u002Fetc\u002Fpasswd`.\n\n## Test Data Setup\n1. **Author User:**\n   ```bash\n   wp user create attacker attacker@example.com --role=author --user_pass=password\n   ```\n2. **Invoice Post:**\n   ```bash\n   wp post create --post_type=sa_invoice --post_status=publish --post_title=\"LFI Test\" --post_author=$(wp user get attacker --field=ID)\n   ```\n\n## Expected Results\n- A successful `POST` request to `post.php` will return a 302 redirect back to the edit page.\n- A `GET` request to the invoice frontend URL will return a `200 OK` response.\n- The response body will contain `\u002Fetc\u002Fpasswd` content (e.g., `root:x:0:0:root:\u002Froot:\u002Fbin\u002Fbash`).\n\n## Verification Steps\n1. **Check Meta State:**\n   ```bash\n   wp post meta get [POST_ID] _doc_template_option\n   ```\n   Confirm it matches the injected path.\n2. **Verify Response:**\n   Check the `http_request` output for the string `root:x:0:0:`.\n\n## Alternative Approaches\n- **PHP Filter Wrapper:** If `\u002Fetc\u002Fpasswd` is blocked or if the plugin appends `.php`, try:\n  `php:\u002F\u002Ffilter\u002Fconvert.base64-encode\u002Fresource=wp-config`\n- **Uploaded Payload:** If the plugin only includes files relative to the uploads directory:\n  1. Upload a text file `poc.txt` containing `\u003C?php echo \"VULNERABLE\"; ?>` via the Media Library.\n  2. Get its path in `wp-content\u002Fuploads\u002F`.\n  3. Set `sa_doc_template` to `..\u002F..\u002F..\u002F..\u002Fuploads\u002FYYYY\u002FMM\u002Fpoc.txt`.\n- **Estimate Post Type:** If `sa_invoice` is restricted, repeat the process using `sa_estimate`.","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.","\u002F\u002F controllers\u002Ftemplating\u002FTemplating.php:686\npublic static function save_doc_template_selection( $post_id = 0 ) {\n    $doc_template = ( isset( $_POST['doc_template'] ) ) ? sanitize_text_field( wp_unslash( $_POST['doc_template'] ) ) : '' ;\n    self::save_doc_current_template( $post_id, $doc_template );\n}\n\n---\n\n\u002F\u002F controllers\u002F_Controller.php:444\n\u002F\u002F Note: This logic retrieves the stored path and checks for its existence within the templates folder without traversal protection\nforeach ( $possibilities as $p ) {\n    if ( file_exists( SI_PATH.'\u002Fviews\u002Ftemplates\u002F'.$p ) ) {\n        return SI_PATH.'\u002Fviews\u002Ftemplates\u002F'.$p;\n    }\n}","diff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fsprout-invoices\u002F20.8.9\u002Fcontrollers\u002F_Controller.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fsprout-invoices\u002F20.8.10\u002Fcontrollers\u002F_Controller.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fsprout-invoices\u002F20.8.9\u002Fcontrollers\u002F_Controller.php\t2026-02-04 16:51:30.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fsprout-invoices\u002F20.8.10\u002Fcontrollers\u002F_Controller.php\t2026-02-16 21:08:28.000000000 +0000\n@@ -433,6 +433,21 @@\n \tprotected static function locate_template( $possibilities, $default = '' ) {\n \t\t$possibilities = apply_filters( 'sprout_invoice_template_possibilities', $possibilities );\n \t\t$possibilities = array_filter( $possibilities );\n+\n+\t\t\u002F\u002F Security: Validate each possibility for path traversal attempts\n+\t\tforeach ( $possibilities as $key => $p ) {\n+\t\t\t\u002F\u002F Reject any path containing directory traversal sequences\n+\t\t\tif ( empty( $p ) || strpos( $p, '..' ) !== false || strpos( $p, \"\\0\" ) !== false ) {\n+\t\t\t\tunset( $possibilities[ $key ] );\n+\t\t\t\t\u002F\u002F Only log in debug mode to prevent DoS via database flooding\n+\t\t\t\tif ( self::DEBUG ) {\n+\t\t\t\t\tdo_action( 'si_error', 'Path traversal attempt detected in template path', array(\n+\t\t\t\t\t\t'attempted_path' => $p,\n+\t\t\t\t\t) );\n+\t\t\t\t}\n+\t\t\t}\n+\t\t}\n+\n \t\t\u002F\u002F check if the theme has an override for the template\n \t\t$theme_overrides = array();\n \t\tforeach ( $possibilities as $p ) {\n@@ -444,9 +459,16 @@\n \t\t}\n \n \t\t\u002F\u002F check for it in the templates directory\n+\t\t\u002F\u002F Security: Compute base path once before loop to avoid repeated filesystem calls\n+\t\t$base_path = realpath( SI_PATH . '\u002Fviews\u002Ftemplates\u002F' );\n+\n \t\tforeach ( $possibilities as $p ) {\n-\t\t\tif ( file_exists( SI_PATH.'\u002Fviews\u002Ftemplates\u002F'.$p ) ) {\n-\t\t\t\treturn SI_PATH.'\u002Fviews\u002Ftemplates\u002F'.$p;\n+\t\t\t$template_path = SI_PATH . '\u002Fviews\u002Ftemplates\u002F' . $p;\n+\t\t\t\u002F\u002F Security: Ensure the resolved path is within the templates directory\n+\t\t\t$real_path = realpath( $template_path );\n+\n+\t\t\tif ( $real_path && $base_path && strpos( $real_path, $base_path ) === 0 && file_exists( $real_path ) ) {\n+\t\t\t\treturn $real_path;\n \t\t\t}\n \t\t}\n \ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fsprout-invoices\u002F20.8.9\u002Fcontrollers\u002Ftemplating\u002FTemplating.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fsprout-invoices\u002F20.8.10\u002Fcontrollers\u002Ftemplating\u002FTemplating.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fsprout-invoices\u002F20.8.9\u002Fcontrollers\u002Ftemplating\u002FTemplating.php\t2026-02-04 16:51:30.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fsprout-invoices\u002F20.8.10\u002Fcontrollers\u002Ftemplating\u002FTemplating.php\t2026-02-16 21:08:28.000000000 +0000\n@@ -686,6 +686,32 @@\n \t *\u002F\n \tpublic static function save_doc_template_selection( $post_id = 0 ) {\n \t\t$doc_template = ( isset( $_POST['doc_template'] ) ) ? sanitize_text_field( wp_unslash( $_POST['doc_template'] ) ) : '' ;\n+\n+\t\t\u002F\u002F Security: Validate template against whitelist to prevent path traversal attacks\n+\t\tif ( '' !== $doc_template ) {\n+\t\t\t$post_type = get_post_type( $post_id );\n+\t\t\t$valid_templates = array();\n+\n+\t\t\t\u002F\u002F Get valid templates based on post type\n+\t\t\tif ( SI_Invoice::POST_TYPE === $post_type ) {\n+\t\t\t\t$valid_templates = array_keys( self::get_invoice_templates() );\n+\t\t\t} elseif ( SI_Estimate::POST_TYPE === $post_type ) {\n+\t\t\t\t$valid_templates = array_keys( self::get_estimate_templates() );\n+\t\t\t}\n+\n+\t\t\t\u002F\u002F Only save if template is in the whitelist\n+\t\t\tif ( ! in_array( $doc_template, $valid_templates, true ) ) {\n+\t\t\t\t\u002F\u002F Invalid template - reject and log (only in debug mode to prevent log flooding)\n+\t\t\t\tif ( self::DEBUG ) {\n+\t\t\t\t\tdo_action( 'si_error', 'Invalid template selection attempted', array(\n+\t\t\t\t\t\t'post_id' => $post_id,\n+\t\t\t\t\t\t'attempted_template' => $doc_template,\n+\t\t\t\t\t) );\n+\t\t\t\t}\n+\t\t\t\t$doc_template = ''; \u002F\u002F Reset to default\n+\t\t\t}\n+\t\t}\n+\n \t\tself::save_doc_current_template( $post_id, $doc_template );\n \t}","To exploit this vulnerability, an attacker with at least Author-level access follows these steps:\n1. Create a new Invoice (`sa_invoice`) or Estimate (`sa_estimate`) or edit an existing one they own.\n2. When saving the document, intercept the POST request to `wp-admin\u002Fpost.php` (action `editpost`).\n3. Inject a directory traversal payload into the `doc_template` parameter, such as `..\u002F..\u002F..\u002F..\u002F..\u002F..\u002Fetc\u002Fpasswd` or `..\u002F..\u002F..\u002F..\u002Fwp-config.php` (potentially using PHP filters to encode the output).\n4. Update the post, which saves the malicious path into the `_doc_template_option` post meta.\n5. 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.","gemini-3-flash-preview","2026-04-19 01:47:20","2026-04-19 01:48:08",{"type":39,"vulnerable_version":40,"fixed_version":11,"vulnerable_browse":41,"vulnerable_zip":42,"fixed_browse":43,"fixed_zip":44,"all_tags":45},"plugin","20.8.9","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Fsprout-invoices\u002Ftags\u002F20.8.9","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Fsprout-invoices.20.8.9.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Fsprout-invoices\u002Ftags\u002F20.8.10","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Fsprout-invoices.20.8.10.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Fsprout-invoices\u002Ftags"]