CVE-2026-2305

AddFunc Head & Footer Code <= 2.3 - Authenticated (Contributor+) Stored Cross-Site Scripting via Custom Fields

mediumImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
6.4
CVSS Score
6.4
CVSS Score
medium
Severity
2.4
Patched in
1d
Time to patch

Description

The AddFunc Head & Footer Code plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the `aFhfc_head_code`, `aFhfc_body_code`, and `aFhfc_footer_code` post meta values in all versions up to, and including, 2.3. This is due to the plugin outputting these meta values without any sanitization or escaping. While the plugin restricts its own metabox and save handler to administrators via `current_user_can('manage_options')`, it does not use `register_meta()` with an `auth_callback` to protect these meta keys. This makes it possible for authenticated attackers, with Contributor-level access and above, to inject arbitrary web scripts via the WordPress Custom Fields interface that execute when an administrator previews or views the post.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Changed
Low
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=2.3
PublishedApril 9, 2026
Last updatedApril 10, 2026

What Changed in the Fix

Changes introduced in v2.4

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan - CVE-2026-2305 ## 1. Vulnerability Summary The **AddFunc Head & Footer Code** plugin (version <= 2.3) is vulnerable to **Authenticated Stored Cross-Site Scripting (XSS)**. The plugin allows administrators to add custom scripts to the `<head>`, start of the `<body>`, an…

Show full research plan

Exploitation Research Plan - CVE-2026-2305

1. Vulnerability Summary

The AddFunc Head & Footer Code plugin (version <= 2.3) is vulnerable to Authenticated Stored Cross-Site Scripting (XSS). The plugin allows administrators to add custom scripts to the <head>, start of the <body>, and <footer> sections of specific posts via three meta keys: aFhfc_head_code, aFhfc_body_code, and aFhfc_footer_code.

While the plugin restricts its custom UI (metaboxes) to users with the manage_options capability, it fails to protect these meta keys using register_meta() with an auth_callback. Because the meta keys do not start with an underscore (e.g., _aFhfc_head_code), they are considered "public" by WordPress. Consequently, any user with edit_posts capability (Contributor level and above) can use the default WordPress Custom Fields interface to create or update these meta values, injecting arbitrary JavaScript that executes whenever the post is viewed.

2. Attack Vector Analysis

  • Endpoint: wp-admin/post.php
  • Action: editpost
  • Vulnerable Parameters: addmeta[1][key] (set to aFhfc_head_code, aFhfc_body_code, or aFhfc_footer_code) and addmeta[1][value] (the payload).
  • Authentication Level: Authenticated (Contributor or higher). Contributors can edit their own posts and manage custom fields for them.
  • Preconditions: The WordPress site must have "Custom Fields" enabled in the post editor (standard behavior).

3. Code Flow

  1. Injection:

    • A Contributor-level user sends a POST request to wp-admin/post.php with action=editpost.
    • The request includes a new custom field: aFhfc_head_code with a malicious script.
    • WordPress core's edit_post() function processes the addmeta array and saves the value to the wp_postmeta table because the key is not protected.
  2. Execution (Head):

    • addfunc-head-footer-code.php registers a hook: add_action('wp_head', array('aFHFCClass','output_head_code')).
    • When the post is viewed, aFHFCClass::output_head_code() is called.
    • It fetches the value: $meta_head_code = get_post_meta(get_the_ID(),'aFhfc_head_code',true);.
    • It outputs the value raw: echo $meta_head_code."\n"; (Line 66).
  3. Execution (Body/Footer):

    • Similar flows exist for aFhfc_body_code (Line 78) and aFhfc_footer_code (Line 89).

4. Nonce Acquisition Strategy

The exploitation uses the standard WordPress post-editing flow, which requires a core WordPress nonce (_wpnonce), not a plugin-specific one.

  1. Navigate to Edit Page: Use browser_navigate to go to the edit page of a post owned by the Contributor: wp-admin/post.php?post=POST_ID&action=edit.
  2. Extract Nonce: Use browser_eval to extract the _wpnonce value from the form.
    • Script: document.querySelector('input[name="_wpnonce"]').value
  3. Alternative: If the "Custom Fields" metabox is not visible, it might need to be enabled via the "Screen Options" (Classic Editor) or "Preferences" (Gutenberg), but the POST request to post.php works regardless of UI visibility if the ID and nonce are correct.

5. Exploitation Strategy

The goal is to inject a script into the <head> of a post created by a Contributor.

  1. Setup User: Create a Contributor user and a post.
  2. Obtain Nonce: Log in as the Contributor, navigate to the post's edit page, and grab the _wpnonce.
  3. Inject Payload:
    • Send a POST request to /wp-admin/post.php using the http_request tool.
    • Headers: Content-Type: application/x-www-form-urlencoded
    • Body:
      action=editpost
      &post_ID=[POST_ID]
      &_wpnonce=[NONCE]
      &addmeta[1][key]=aFhfc_head_code
      &addmeta[1][value]=<script>alert('XSS_HEAD')</script>
      
  4. Trigger: Navigate to the frontend URL of the post (?p=[POST_ID]) as any user (including an unauthenticated guest).

6. Test Data Setup

  1. Contributor User:
    • Username: low_priv_user
    • Role: contributor
  2. Target Post:
    • Title: Vulnerable Post
    • Author: low_priv_user
    • Note the POST_ID.

7. Expected Results

  • The POST request to post.php should return a 302 Redirect back to the edit page (indicating success).
  • When fetching the frontend post page, the source code should contain <script>alert('XSS_HEAD')</script> inside the <head> tags.

8. Verification Steps

  1. Verify Database Storage:
    • Use WP-CLI: wp post meta get [POST_ID] aFhfc_head_code.
    • Confirm it contains the <script> payload.
  2. Verify Frontend Execution:
    • Use http_request (GET) on the post's permalink.
    • Check if the payload exists in the response body.

9. Alternative Approaches

If aFhfc_head_code is sanitized by a security plugin or WAF, attempt the other keys:

  • Body Start Injection: Use aFhfc_body_code. This is particularly dangerous as it uses preg_replace to inject immediately after the <body> tag (Line 115).
  • Footer Injection: Use aFhfc_footer_code, which outputs during the wp_footer action.

If the site uses Gutenberg and the post.php method is restricted, the same can be achieved via the REST API if the meta is registered (though the vulnerability exists specifically because it isn't registered correctly):

  • Endpoint: POST /wp-json/wp/v2/posts/[POST_ID]
  • Body: {"meta": {"aFhfc_head_code": "<script>alert(1)</script>"}}
    Note: This only works if WordPress considers the meta key "rest-visible", which usually requires registration. Therefore, the post.php (Classic/Heartbeat) method is the most reliable.
Research Findings
Static analysis — not yet PoC-verified

Summary

The AddFunc Head & Footer Code plugin for WordPress is vulnerable to Authenticated (Contributor+) Stored Cross-Site Scripting via the 'aFhfc_head_code', 'aFhfc_body_code', and 'aFhfc_footer_code' post meta values. This occurs because the plugin fails to sanitize or escape these values before outputting them and does not protect the meta keys via register_meta(), allowing users with post-editing privileges to inject scripts through the WordPress Custom Fields interface.

Vulnerable Code

// addfunc-head-footer-code.php line 60-70
    public static function output_head_code()
    {
      $site_head_code = get_option('aFhfc_site_wide_head_code');
      $meta_head_code = ((is_archive()) || (is_author()) || (is_category()) || (is_tag()) || (is_home()) || (is_search()) || (is_404())) ? '' : get_post_meta(get_the_ID(),'aFhfc_head_code',true);
      $head_replace = get_post_meta(get_the_ID(),'aFhfc_head_replace',true);
      if(!empty($head_replace)){
        echo $meta_head_code."\n";
      }else{
        echo $site_head_code."\n".$meta_head_code."\n";
      }
    }

---

// addfunc-head-footer-code.php line 84-94
    public static function output_footer_code()
    {
      $site_footer_code = get_option('aFhfc_site_wide_footer_code');
      $meta_footer_code = ((is_archive()) || (is_author()) || (is_category()) || (is_tag()) || (is_home()) || (is_search()) || (is_404())) ? '' : get_post_meta(get_the_ID(),'aFhfc_footer_code',true);
      $footer_replace = get_post_meta(get_the_ID(),'aFhfc_footer_replace',true);
      if(!empty($footer_replace)){
        echo $meta_footer_code."\n";
      }else{
        echo $site_footer_code."\n".$meta_footer_code."\n";
      }
    }

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/addfunc-head-footer-code/2.3/addfunc-head-footer-code.php /home/deploy/wp-safety.org/data/plugin-versions/addfunc-head-footer-code/2.4/addfunc-head-footer-code.php
--- /home/deploy/wp-safety.org/data/plugin-versions/addfunc-head-footer-code/2.3/addfunc-head-footer-code.php	2019-05-29 19:40:24.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/addfunc-head-footer-code/2.4/addfunc-head-footer-code.php	2026-03-28 16:08:56.000000000 +0000
@@ -1,12 +1,13 @@
-<?php
+<?php if ( ! defined( 'ABSPATH' ) ) exit;
 /*
     Plugin Name: AddFunc Head & Footer Code
     Plugin URI:
     Description: Allows administrators to add code to the &lt;head&gt; and/or &lt;footer&gt; of an individual post and/or site-wide. Ideal for scripts such as Google Analytics conversion tracking codes and any other general or page-specific JavaScript.
-    Version: 2.3
+    Version: 2.4
     Author: AddFunc
     Author URI: http://profiles.wordpress.org/addfunc
-    License: Public Domain
+    Text Domain: addfunc-head-footer-code
+    License: GPLv2 or later
     @since 3.0.1
            ______
        _  |  ___/   _ _ __   ____
@@ -27,84 +28,93 @@
 if(!class_exists('aFHFCClass')) :
   define('AFHDFTRCD_ID', 'aFhfc');
   define('AFHDFTRCD_NICK', 'Head & Footer Code');
-  class aFHFCClass
-  {
-    public static function file_path($file)
-    {
+  class aFHFCClass {
+    public static function file_path($file) {
       return plugin_dir_path(__FILE__).$file;
     }
-    public static function register()
-    {
+    public static function register() {
       register_setting(AFHDFTRCD_ID.'_options', 'aFhfc_site_wide_head_code');
       register_setting(AFHDFTRCD_ID.'_options', 'aFhfc_head_code_priority');
       register_setting(AFHDFTRCD_ID.'_options', 'aFhfc_site_wide_body_code');
       register_setting(AFHDFTRCD_ID.'_options', 'aFhfc_site_wide_footer_code');
       register_setting(AFHDFTRCD_ID.'_options', 'aFhfc_footer_code_priority');
     }
+    public static function register_meta_keys() {
+      register_meta('post', 'aFhfc_head_code', array(
+        'auth_callback'     => function() { return current_user_can('manage_options'); },
+        'sanitize_callback' => 'wp_kses_post',
+        'show_in_rest'      => false,
+      ));
+      register_meta('post', 'aFhfc_body_code', array(
+        'auth_callback'     => function() { return current_user_can('manage_options'); },
+        'sanitize_callback' => 'wp_kses_post',
+        'show_in_rest'      => false,
+      ));
+      register_meta('post', 'aFhfc_footer_code', array(
+        'auth_callback'     => function() { return current_user_can('manage_options'); },
+        'sanitize_callback' => 'wp_kses_post',
+        'show_in_rest'      => false,
+      ));
+    }
-    public static function menu()
-    {
+    public static function menu() {
       add_options_page(AFHDFTRCD_NICK.' Plugin Options', AFHDFTRCD_NICK, 'manage_options', AFHDFTRCD_ID.'_options', array('aFHFCClass', 'options_page'));
     }
-    public static function options_page()
-    {
+    public static function options_page() {
       if (!current_user_can('manage_options'))
       {
-        wp_die(__('You do not have sufficient permissions to access this page.'));
+        wp_die(__('You do not have sufficient permissions to access this page.', 'addfunc-head-footer-code'));
       }
       $plugin_id = AFHDFTRCD_ID;
       include(self::file_path('options.php'));
     }
-    public static function output_head_code()
-    {
+    public static function output_head_code() {
       $site_head_code = get_option('aFhfc_site_wide_head_code');
       $meta_head_code = ((is_archive()) || (is_author()) || (is_category()) || (is_tag()) || (is_home()) || (is_search()) || (is_404())) ? '' : get_post_meta(get_the_ID(),'aFhfc_head_code',true);
       $head_replace = get_post_meta(get_the_ID(),'aFhfc_head_replace',true);
-      if(!empty($head_replace)){
+      if(!empty($head_replace)) {
         echo $meta_head_code."\n";
       }else{
         echo $site_head_code."\n".$meta_head_code."\n";
       }
     }
-    public static function output_body_code()
-    {
+    public static function output_body_code() {
       $site_body_code = get_option('aFhfc_site_wide_body_code');
       $meta_body_code = ((is_archive()) || (is_author()) || (is_category()) || (is_tag()) || (is_home()) || (is_search()) || (is_404())) ? '' : get_post_meta(get_the_ID(),'aFhfc_body_code',true);
       $body_replace = get_post_meta(get_the_ID(),'aFhfc_body_replace',true);
-      if(!empty($body_replace)){
+      if(!empty($body_replace)) {
         return $meta_body_code."\n";
       }else{
         return $site_body_code."\n".$meta_body_code."\n";
       }
     }
-    public static function output_footer_code()
-    {
+    public static function output_footer_code() {
       $site_footer_code = get_option('aFhfc_site_wide_footer_code');
       $meta_footer_code = ((is_archive()) || (is_author()) || (is_category()) || (is_tag()) || (is_home()) || (is_search()) || (is_404())) ? '' : get_post_meta(get_the_ID(),'aFhfc_footer_code',true);
       $footer_replace = get_post_meta(get_the_ID(),'aFhfc_footer_replace',true);
-      if(!empty($footer_replace)){
+      if(!empty($footer_replace)) {
         echo $meta_footer_code."\n";
       }else{
         echo $site_footer_code."\n".$meta_footer_code."\n";
       }
     }
   }
-  if (is_admin())
-  {
+  add_action('init', array('aFHFCClass','register_meta_keys'));
+  if (is_admin()) {
     add_action('admin_init', array('aFHFCClass','register'));
     add_action('admin_menu', array('aFHFCClass','menu'));
   }

Exploit Outline

The exploit leverages the fact that the plugin's custom post meta keys (aFhfc_head_code, aFhfc_body_code, aFhfc_footer_code) are not registered with restricted permissions, allowing any user with the ability to edit a post (Contributor role and above) to modify them via the standard WordPress Custom Fields interface. An attacker logs in as a Contributor, navigates to the 'Edit Post' page for one of their posts, and adds a new Custom Field with a key such as 'aFhfc_head_code' and a value containing a malicious script (e.g., <script>alert(1)</script>). When the post is updated, WordPress core saves this meta value. When any user (including an administrator) subsequently views the post on the frontend, the plugin retrieves the raw script from the database and echoes it directly into the page's HTML, resulting in script execution.

Check if your site is affected.

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