[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fdMGORLwM8s5TFELezocb00K8xoPwnsdtPIB20kfIQio":3},{"id":4,"url_slug":5,"title":6,"description":7,"plugin_slug":8,"theme_slug":9,"affected_versions":10,"patched_in_version":11,"severity":12,"cvss_score":13,"cvss_vector":14,"vuln_type":15,"published_date":16,"updated_date":17,"references":18,"days_to_patch":20,"patch_diff_files":21,"patch_trac_url":9,"research_status":25,"research_verified":26,"research_rounds_completed":27,"research_plan":28,"research_summary":29,"research_vulnerable_code":30,"research_fix_diff":31,"research_exploit_outline":32,"research_model_used":33,"research_started_at":34,"research_completed_at":35,"research_error":9,"poc_status":9,"poc_video_id":9,"poc_summary":9,"poc_steps":9,"poc_tested_at":9,"poc_wp_version":9,"poc_php_version":9,"poc_playwright_script":9,"poc_exploit_code":9,"poc_has_trace":26,"poc_model_used":9,"poc_verification_depth":9,"poc_exploit_code_gated":26,"source_links":36},"CVE-2026-2512","code-embed-authenticated-contributor-stored-cross-site-scripting-via-custom-fields","Code Embed \u003C= 2.5.1 - Authenticated (Contributor+) Stored Cross-Site Scripting via Custom Fields","The Code Embed plugin for WordPress is vulnerable to Stored Cross-Site Scripting via custom field meta values in all versions up to, and including, 2.5.1. This is due to the plugin's sanitization function `sec_check_post_fields()` only running on the `save_post` hook, while WordPress allows custom fields to be added via the `wp_ajax_add_meta` AJAX endpoint without triggering `save_post`. The `ce_filter()` function then outputs these unsanitized meta values directly into page content without escaping. This makes it possible for authenticated attackers, with Contributor-level access and above, to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page.","simple-embed-code",null,"\u003C=2.5.1","2.5.2","medium",6.4,"CVSS:3.1\u002FAV:N\u002FAC:L\u002FPR:L\u002FUI:N\u002FS:C\u002FC:L\u002FI:L\u002FA:N","Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')","2026-03-17 00:00:00","2026-03-18 15:28:28",[19],"https:\u002F\u002Fwww.wordfence.com\u002Fthreat-intel\u002Fvulnerabilities\u002Fid\u002F375ed04b-a3cb-4e60-83c8-18bff583aaf4?source=api-prod",2,[22,23,24],"includes\u002Fsecure.php","readme.txt","simple-code-embed.php","researched",false,3,"# Research Plan: CVE-2026-2512 - Code Embed Stored XSS\n\n## 1. Vulnerability Summary\nThe **Code Embed** plugin (versions \u003C= 2.5.1) is vulnerable to **Stored Cross-Site Scripting (XSS)**. The plugin uses WordPress Custom Fields (post meta) to store and embed code snippets (JavaScript, HTML, CSS) into posts and pages. \n\nThe security flaw exists in `includes\u002Fsecure.php`. While the plugin attempts to sanitize custom fields using `wp_kses_post()` within the `sec_check_post_fields()` function, this function is only hooked to `save_post`. However, WordPress core provides an AJAX endpoint (`wp_ajax_add_meta`) that allows users with `edit_posts` capabilities (like **Contributors**) to add or update custom fields without triggering the `save_post` hook. Consequently, the sanitization logic is bypassed. When the post is rendered, the plugin's output filter fetches these unsanitized values and injects them directly into the page content.\n\n## 2. Attack Vector Analysis\n*   **Vulnerable Endpoint**: `\u002Fwp-admin\u002Fadmin-ajax.php`\n*   **Action**: `add-meta`\n*   **Required Parameter**: `metakeyinput` (must match the plugin's keyword prefix, default: `CODE`)\n*   **Payload Parameter**: `metavalue` (the XSS payload)\n*   **Authentication Level**: Authenticated (Contributor or higher). Contributors can edit their own posts and thus access the `add-meta` AJAX action for those posts.\n*   **Preconditions**: \n    1.  The attacker must have a post they are permitted to edit.\n    2.  The post must contain a \"placeholder\" identifier (e.g., `{{CODE1}}`) that the plugin will replace with the malicious meta value.\n\n## 3. Code Flow\n1.  **Injection**:\n    *   The attacker sends a POST request to `admin-ajax.php` with `action=add-meta`.\n    *   WordPress core executes `wp_ajax_add_meta()`, which calls `add_post_meta()` or `update_post_meta()`.\n    *   The `save_post` hook is **not** triggered by this AJAX action.\n    *   `sec_check_post_fields()` in `includes\u002Fsecure.php` is never called, so `wp_kses_post()` is bypassed.\n2.  **Storage**: The raw payload (e.g., `\u003Cscript>alert(1)\u003C\u002Fscript>`) is stored in the `wp_postmeta` table.\n3.  **Execution**:\n    *   A victim (e.g., Administrator) views the post.\n    *   The plugin (likely via `includes\u002Fadd-embeds.php`, referred to as `ce_filter` in descriptions) parses the content for identifiers like `{{CODE1}}`.\n    *   It retrieves the meta value for the key `CODE1`.\n    *   The plugin outputs the raw value into the HTML without further escaping, triggering the XSS.\n\n## 4. Nonce Acquisition Strategy\nThe `add-meta` action requires a core WordPress nonce. This nonce is specific to the post being edited.\n\n1.  **Identify Post**: Create a post as a Contributor.\n2.  **Navigate to Editor**: Use `browser_navigate` to go to the edit page for that post: `wp-admin\u002Fpost.php?post=POST_ID&action=edit`.\n3.  **Extract Nonce**: The nonce for adding meta is stored in a hidden input field with the ID `_ajax_nonce-add-meta`.\n4.  **JavaScript Execution**:\n    ```javascript\n    browser_eval(\"document.getElementById('_ajax_nonce-add-meta').value\")\n    ```\n5.  **Alternative (Global)**: If the hidden input is missing (due to Gutenberg), the nonce is often found in the `wp-lists` initialization or the `_wpnonce` parameter of other meta-related requests. However, for most WordPress versions, `_ajax_nonce-add-meta` remains the standard.\n\n## 5. Exploitation Strategy\n1.  **Setup User**: Create a Contributor user (`contributor` \u002F `password`).\n2.  **Setup Content**: \n    *   As the Contributor, create a post with the title \"XSS Test\" and content `{{CODE1}}`.\n    *   Capture the `POST_ID`.\n3.  **Acquire Nonce**:\n    *   Log in as the Contributor.\n    *   Navigate to the edit screen for `POST_ID`.\n    *   Extract the `add-meta` nonce using the strategy in Section 4.\n4.  **Inject Payload**:\n    *   Use `http_request` to call `admin-ajax.php`.\n    *   **Method**: `POST`\n    *   **URL**: `http:\u002F\u002Fvulnerable-wp.local\u002Fwp-admin\u002Fadmin-ajax.php`\n    *   **Headers**: `Content-Type: application\u002Fx-www-form-urlencoded`\n    *   **Body Parameters**:\n        *   `action`: `add-meta`\n        *   `post_id`: `POST_ID`\n        *   `metakeyselect`: `#NONE#`\n        *   `metakeyinput`: `CODE1`\n        *   `metavalue`: `\u003Cscript>alert(document.domain)\u003C\u002Fscript>`\n        *   `_ajax_nonce-add-meta`: `[EXTRACTED_NONCE]`\n5.  **Trigger**: Navigate to the public URL of the post (as any user).\n\n## 6. Test Data Setup\n*   **Plugin Configuration**: Default settings (Keyword: `CODE`, Identifiers: `{{` and `}}`).\n*   **User Role**: `contributor`\n*   **Target Post**:\n    *   Title: `Vulnerable Post`\n    *   Content: `This is a test. {{CODE1}}`\n    *   Status: `publish`\n\n## 7. Expected Results\n*   The AJAX request should return a successful response (usually a partial HTML block for the custom fields table).\n*   When viewing the post, the HTML source should contain: `\u003Cdiv>...\u003Cscript>alert(document.domain)\u003C\u002Fscript>...\u003C\u002Fdiv>`.\n*   A browser alert box should appear showing the domain.\n\n## 8. Verification Steps\n1.  **Database Check**: \n    ```bash\n    wp post meta get [POST_ID] CODE1\n    ```\n    Confirm the output is the raw payload `\u003Cscript>alert(document.domain)\u003C\u002Fscript>` and has **not** been stripped to empty or sanitized.\n2.  **Frontend Check**:\n    ```bash\n    http_request GET http:\u002F\u002Fvulnerable-wp.local\u002F?p=[POST_ID]\n    ```\n    Check the response body for the presence of the unescaped script tag.\n\n## 9. Alternative Approaches\n*   **Identifier Variation**: The `readme.txt` mentions identifiers could be `%` (e.g., `%CODE1%`). If `{{CODE1}}` fails, try `%CODE1%`.\n*   **Keyword Variation**: If the site has changed the keyword identifier in settings, use `wp option get artiss_code_embed` to find the `keyword_ident` value.\n*   **Global Embeds**: The plugin supports global embeds. An attacker might try to set meta on a \"global\" post if configured, potentially affecting all pages on the site.\n*   **XSS to RCE**: In a real-world scenario, the payload would be a script to create a new Administrator user via the `\u002Fwp-admin\u002Fuser-new.php` CSRF.","The Code Embed plugin for WordPress is vulnerable to Stored Cross-Site Scripting via custom field meta values because it only performs sanitization during the 'save_post' hook. Authenticated attackers with Contributor-level access can bypass this by using the WordPress AJAX 'add-meta' endpoint to inject malicious scripts into custom fields, which the plugin then renders without escaping.","\u002F* includes\u002Fsecure.php lines 32-62 *\u002F\nfunction sec_check_post_fields( $post_id, $post, $update ) {\n\n\t$options = get_option( 'artiss_code_embed' );\n\n\t\u002F\u002F Check if it's an autosave or if the current user has the 'unfiltered_html' capability.\n\tif ( ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) || ( current_user_can( 'unfiltered_html' ) ) ) {\n\t\treturn;\n\t}\n\n\t\u002F\u002F Fetch all post meta (custom fields) associated with the post.\n\t$custom_fields = get_post_meta( $post_id );\n\n\t\u002F\u002F If there are custom fields, read through them.\n\tif ( ! empty( $custom_fields ) ) {\n\n\t\tforeach ( $custom_fields as $key => $value ) {\n\n\t\t\t\u002F\u002F Check to see if any begining with this plugin's prefix.\n\t\t\tif ( substr( $key, 0, strlen( $options['keyword_ident'] ) ) === $options['keyword_ident'] ) {\n\n\t\t\t\t\u002F\u002F Filter the meta value.\n\t\t\t\t$new_value = wp_kses_post( $value[0] );\n\n\t\t\t\t\u002F\u002F Now write out the new value.\n\t\t\t\tupdate_post_meta( $post_id, $key, $new_value );\n\t\t\t}\n\t\t}\n\t}\n}\n\nadd_action( 'save_post', 'sec_check_post_fields', 10, 3 );","diff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fsimple-embed-code\u002F2.5.1\u002Fincludes\u002Fsecure.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fsimple-embed-code\u002F2.5.2\u002Fincludes\u002Fsecure.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fsimple-embed-code\u002F2.5.1\u002Fincludes\u002Fsecure.php\t2024-11-05 18:57:12.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fsimple-embed-code\u002F2.5.2\u002Fincludes\u002Fsecure.php\t2026-03-15 10:00:20.000000000 +0000\n@@ -1,8 +1,8 @@\n \u003C?php\n \u002F**\n- * Meta boxes\n+ * Security\n  *\n- * Functions related to meta-box management.\n+ * Functions related to sanitizing Code Embed meta values.\n  *\n  * @package simple-embed-code\n  *\u002F\n@@ -14,42 +14,58 @@\n }\n \n \u002F**\n- * Remove Custom Fields\n+ * Sanitize Code Embed meta on every write\n  *\n- * Remove the custom field meta boxes if the user doesn't have the unfiltered HTML permissions.\n+ * Filter that fires on every call to update_metadata \u002F add_metadata — including the\n+ * wp_ajax_add_meta AJAX handler and the REST API, not just save_post.\n  *\n- * @param    string  $post_id   Post ID.\n- * @param    string  $post      Post object.\n- * @param    boolean $update    Whether this is an existing post being updated.\n+ * @param mixed  $check      Null to allow the operation, non-null to short-circuit.\n+ * @param int    $object_id  Post ID.\n+ * @param string $meta_key   Meta key being written.\n+ * @param mixed  $meta_value Meta value being written.\n+ * @return mixed             Null (to proceed with the write).\n  *\u002F\n-function sec_check_post_fields( $post_id, $post, $update ) {\n+function sec_sanitize_meta_on_write( $check, $object_id, $meta_key, $meta_value ) {\n+\n+\t\u002F\u002F Allow admins \u002F editors with unfiltered_html to write without restriction.\n+\tif ( current_user_can( 'unfiltered_html' ) ) {\n+\t\treturn $check;\n+\t}\n \n \t$options = get_option( 'artiss_code_embed' );\n \n-\t\u002F\u002F Check if it's an autosave or if the current user has the 'unfiltered_html' capability.\n-\tif ( ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) || ( current_user_can( 'unfiltered_html' ) ) ) {\n-\t\treturn;\n+\tif ( ! is_array( $options ) || empty( $options['keyword_ident'] ) ) {\n+\t\treturn $check;\n \t}\n \n-\t\u002F\u002F Fetch all post meta (custom fields) associated with the post.\n-\t$custom_fields = get_post_meta( $post_id );\n+\t$prefix = $options['keyword_ident'];\n \n-\t\u002F\u002F If there are custom fields, read through them.\n-\tif ( ! empty( $custom_fields ) ) {\n+\t\u002F\u002F Only act on meta keys that belong to this plugin.\n+\tif ( substr( $meta_key, 0, strlen( $prefix ) ) !== $prefix ) {\n+\t\treturn $check;\n+\t}\n \n-\t\tforeach ( $custom_fields as $key => $value ) {\n+\t\u002F\u002F Strip dangerous markup while preserving safe HTML.\n+\t$clean = wp_kses_post( $meta_value );\n \n-\t\t\t\u002F\u002F Check to see if any begining with this plugin's prefix.\n-\t\t\tif ( substr( $key, 0, strlen( $options['keyword_ident'] ) ) === $options['keyword_ident'] ) {\n+\tif ( $clean === $meta_value ) {\n+\t\t\u002F\u002F Value is already clean — let the normal write proceed.\n+\t\treturn $check;\n+\t}\n \n-\t\t\t\t\u002F\u002F Filter the meta value.\n-\t\t\t\t$new_value = wp_kses_post( $value[0] );\n+\t\u002F\u002F The value was dirty. Remove this filter temporarily to avoid infinite recursion, write the sanitized value ourselves, then\n+\t\u002F\u002F re-add the filter and short-circuit the original write.\n+\tremove_filter( 'update_post_metadata', 'sec_sanitize_meta_on_write', 10 );\n+\tremove_filter( 'add_post_metadata', 'sec_sanitize_meta_on_write', 10 );\n \n-\t\t\t\t\u002F\u002F Now write out the new value.\n-\t\t\t\tupdate_post_meta( $post_id, $key, $new_value );\n-\t\t\t}\n-\t\t}\n-\t}\n+\tupdate_post_meta( $object_id, $meta_key, $clean );\n+\n+\tadd_filter( 'update_post_metadata', 'sec_sanitize_meta_on_write', 10, 4 );\n+\tadd_filter( 'add_post_metadata', 'sec_sanitize_meta_on_write', 10, 4 );\n+\n+\t\u002F\u002F Return a non-null value to short-circuit the original (unsanitized) write.\n+\treturn true;\n }\n \n-add_action( 'save_post', 'sec_check_post_fields', 10, 3 );\n+add_filter( 'update_post_metadata', 'sec_sanitize_meta_on_write', 10, 4 );\n+add_filter( 'add_post_metadata', 'sec_sanitize_meta_on_write', 10, 4 );","1. Authenticate as a user with Contributor level permissions or higher.\n2. Create a post and include a placeholder for a custom field in the content (e.g., {{CODE1}}), then publish it.\n3. Navigate to the post editor page to obtain a valid WordPress AJAX nonce for the 'add-meta' action (typically found in the '_ajax_nonce-add-meta' hidden input field).\n4. Send a POST request to \u002Fwp-admin\u002Fadmin-ajax.php with the following parameters: action=add-meta, metakeyinput=CODE1, metavalue=\u003Cscript>alert(1)\u003C\u002Fscript>, and the extracted nonce.\n5. Because the WordPress AJAX 'add-meta' action bypasses the 'save_post' hook, the payload is stored in the database without being sanitized by the plugin's wp_kses_post filter.\n6. View the published post; the plugin will replace {{CODE1}} with the unsanitized script payload, resulting in execution in the victim's browser.","gemini-3-flash-preview","2026-04-18 03:01:31","2026-04-18 03:01:56",{"type":37,"vulnerable_version":38,"fixed_version":11,"vulnerable_browse":39,"vulnerable_zip":40,"fixed_browse":41,"fixed_zip":42,"all_tags":43},"plugin","2.5.1","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Fsimple-embed-code\u002Ftags\u002F2.5.1","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Fsimple-embed-code.2.5.1.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Fsimple-embed-code\u002Ftags\u002F2.5.2","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Fsimple-embed-code.2.5.2.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Fsimple-embed-code\u002Ftags"]