AddFunc Head & Footer Code <= 2.3 - Authenticated (Contributor+) Stored Cross-Site Scripting via Custom Fields
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:NTechnical Details
<=2.3What Changed in the Fix
Changes introduced in v2.4
Source Code
WordPress.org SVN# 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 toaFhfc_head_code,aFhfc_body_code, oraFhfc_footer_code) andaddmeta[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
Injection:
- A Contributor-level user sends a POST request to
wp-admin/post.phpwithaction=editpost. - The request includes a new custom field:
aFhfc_head_codewith a malicious script. - WordPress core's
edit_post()function processes theaddmetaarray and saves the value to thewp_postmetatable because the key is not protected.
- A Contributor-level user sends a POST request to
Execution (Head):
addfunc-head-footer-code.phpregisters 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).
Execution (Body/Footer):
- Similar flows exist for
aFhfc_body_code(Line 78) andaFhfc_footer_code(Line 89).
- Similar flows exist for
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.
- Navigate to Edit Page: Use
browser_navigateto go to the edit page of a post owned by the Contributor:wp-admin/post.php?post=POST_ID&action=edit. - Extract Nonce: Use
browser_evalto extract the_wpnoncevalue from the form.- Script:
document.querySelector('input[name="_wpnonce"]').value
- Script:
- 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.phpworks 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.
- Setup User: Create a Contributor user and a post.
- Obtain Nonce: Log in as the Contributor, navigate to the post's edit page, and grab the
_wpnonce. - Inject Payload:
- Send a POST request to
/wp-admin/post.phpusing thehttp_requesttool. - 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>
- Send a POST request to
- Trigger: Navigate to the frontend URL of the post (
?p=[POST_ID]) as any user (including an unauthenticated guest).
6. Test Data Setup
- Contributor User:
- Username:
low_priv_user - Role:
contributor
- Username:
- Target Post:
- Title:
Vulnerable Post - Author:
low_priv_user - Note the
POST_ID.
- Title:
7. Expected Results
- The POST request to
post.phpshould return a302 Redirectback 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
- Verify Database Storage:
- Use WP-CLI:
wp post meta get [POST_ID] aFhfc_head_code. - Confirm it contains the
<script>payload.
- Use WP-CLI:
- Verify Frontend Execution:
- Use
http_request(GET) on the post's permalink. - Check if the payload exists in the response body.
- Use
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 usespreg_replaceto inject immediately after the<body>tag (Line 115). - Footer Injection: Use
aFhfc_footer_code, which outputs during thewp_footeraction.
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, thepost.php(Classic/Heartbeat) method is the most reliable.
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
@@ -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 <head> and/or <footer> 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.