HTTP Headers <= 1.19.2 - Authenticated (Administrator+) Stored Cross-Site Scripting via 'Custom Headers' Plugin Setting
Description
The HTTP Headers plugin for WordPress is vulnerable to Stored Cross-Site Scripting via admin settings in all versions up to, and including, 1.19.2 due to insufficient input sanitization and output escaping. This makes it possible for authenticated attackers, with administrator-level permissions and above, to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page. This only affects multi-site installations and installations where unfiltered_html has been disabled.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:C/C:L/I:L/A:NTechnical Details
# Research Plan: CVE-2026-1379 - Authenticated Stored XSS in HTTP Headers ## 1. Vulnerability Summary The **HTTP Headers** plugin (<= 1.19.2) is vulnerable to **Stored Cross-Site Scripting (XSS)** via the "Custom Headers" setting. The plugin allows administrators to define custom HTTP headers to be…
Show full research plan
Research Plan: CVE-2026-1379 - Authenticated Stored XSS in HTTP Headers
1. Vulnerability Summary
The HTTP Headers plugin (<= 1.19.2) is vulnerable to Stored Cross-Site Scripting (XSS) via the "Custom Headers" setting. The plugin allows administrators to define custom HTTP headers to be sent by the server. However, it fails to sufficiently sanitize the input when saving these headers and fails to escape the output when displaying them back in the administrative interface.
In a standard WordPress installation, Administrators have the unfiltered_html capability, making this "intended functionality." However, in Multi-site environments or installations where DISALLOW_UNFILTERED_HTML is defined as true, this vulnerability allows an Administrator to bypass those restrictions and inject malicious scripts that execute in the context of other users (including Super Admins) who visit the plugin's settings page.
2. Attack Vector Analysis
- Endpoint: WordPress Admin Settings page for the plugin, typically
wp-admin/options-general.php?page=http-headersor a specific sub-page for "Custom". - Vulnerable Parameter: Likely a POST parameter named
custom_headersor similar, nested within the plugin's options array. - Authentication: Authenticated, Administrator+ level permissions.
- Preconditions:
- The plugin
http-headers(<= 1.19.2) must be active. unfiltered_htmlmust be disabled (common in Multi-site or viadefine('DISALLOW_UNFILTERED_HTML', true);inwp-config.php).
- The plugin
3. Code Flow (Inferred)
- Entry Point: The Administrator navigates to the plugin settings and submits a form to save custom headers.
- Processing: The request hits
wp-admin/options.php(if using the Settings API) or a customadmin_inithook. The plugin receives the array of custom headers. - Storage: The plugin calls
update_option()to save the custom headers into the database (likely under an option name likehttp_headers_settingsorhttp_headers_custom). - Sink (Output): When any administrator visits the "Custom Headers" configuration tab, the plugin retrieves the saved option and echoes it into the HTML value attribute of an input field or within a table row without using
esc_attr()oresc_html(). - Execution: The injected script (e.g.,
"><script>alert(1)</script>) breaks out of the HTML attribute and executes in the browser.
4. Nonce Acquisition Strategy
The plugin likely uses the standard WordPress Settings API or a custom form on an admin page.
- Identify the Page: Use
wp-clito find the settings page slug:wp admin-menu list | grep "HTTP Headers" - Navigate and Extract:
- Use
browser_navigateto go to the identified settings page (e.g.,/wp-admin/options-general.php?page=http-headers). - The form will contain a hidden
_wpnoncefield. - Action String: If the plugin uses
settings_fields('http_headers_group'), the nonce action will behttp_headers_group-options. - JS Extraction:
browser_eval("document.querySelector('input[name=\"_wpnonce\"]')?.value")
- Use
5. Exploitation Strategy
Setup Environment: Ensure
DISALLOW_UNFILTERED_HTMLis set totrueinwp-config.php.Identify Parameter Name: Inspect the settings page to find the exact name of the custom header input field. (Inferred:
http_headers_custom[0][value]or similar).Payload Construction:
- Simple:
"><script>alert(document.domain)</script> - Breaking out of an input:
"><img src=x onerror=alert(1)>
- Simple:
HTTP Request (Playwright):
Submit a POST request towp-admin/options.php(if Settings API) or the plugin's own admin page.Example Request:
POST /wp-admin/options.php HTTP/1.1 Content-Type: application/x-www-form-urlencoded option_page=http_headers_settings_group& action=update& _wpnonce=[EXTRACTED_NONCE]& http_headers_settings[custom_headers][0][name]=X-XSS-Test& http_headers_settings[custom_headers][0][value]=%22%3E%3Cscript%3Ealert(1)%3C%2Fscript%3ETrigger: Navigate back to the "Custom Headers" settings page to trigger the stored payload.
6. Test Data Setup
- Plugin Installation:
wp plugin install http-headers --version=1.19.2 --activate - Config Change: Add
define( 'DISALLOW_UNFILTERED_HTML', true );towp-config.phpusing a file write tool orsed. - User Creation: Ensure an administrator user exists (e.g.,
admin/password). - Page Visit: Visit the settings page once to ensure default options are initialized.
7. Expected Results
- The POST request should return a
302 Redirectback to the settings page withsettings-updated=true. - Upon navigating to the "Custom Headers" tab, the browser should execute the
alert(1)script. - The HTML source of the page should show the unescaped payload:
<input ... value=""><script>alert(1)</script>">.
8. Verification Steps
- Database Check: Use
wp-clito verify the payload is stored in the database:wp option get http_headers_settings --format=json(or the specific option name found during research). - DOM Check: Use
browser_evalto check for the presence of the injected script:browser_eval("!!document.querySelector('script')") - Visual Confirmation: Capture a screenshot of the alert/injected element using the
screenshottool.
9. Alternative Approaches
- AJAX Entry Point: If the plugin doesn't use
options.php, check forwp_ajax_save_http_headershooks in the source. - Header Injection (Frontend): If the plugin also renders these headers in the site frontend (as actual HTTP headers), test if it's possible to inject a
Set-CookieorRefreshheader to perform session fixation or redirects, though the primary vulnerability is XSS in the admin UI. - Attribute Breakout: If the input is inside a complex JavaScript structure, use a JSON-breaking payload:
\";alert(1)//.
Summary
The HTTP Headers plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the 'Custom Headers' setting in versions up to 1.19.2. This occurs because the plugin fails to sanitize user-provided header values before storage and fails to escape them when displaying them back in the administrative interface, allowing attackers to bypass 'unfiltered_html' restrictions in Multi-site environments.
Vulnerable Code
// File: http-headers/pages/custom.php (Inferred based on plugin structure and research plan) // The plugin iterates through custom headers and echoes the 'value' key directly into an input attribute without escaping. for ($i = 0; $i < count($http_headers_custom); $i++) { echo '<tr>'; echo '<td><input name="http_headers_custom[' . $i . '][name]" type="text" value="' . $http_headers_custom[$i]['name'] . '" /></td>'; echo '<td><input name="http_headers_custom[' . $i . '][value]" type="text" value="' . $http_headers_custom[$i]['value'] . '" /></td>'; echo '</tr>'; }
Security Fix
@@ -3,6 +3,6 @@ for ($i = 0; $i < count($http_headers_custom); $i++) { echo '<tr>'; - echo '<td><input name="http_headers_custom[' . $i . '][name]" type="text" value="' . $http_headers_custom[$i]['name'] . '" /></td>'; - echo '<td><input name="http_headers_custom[' . $i . '][value]" type="text" value="' . $http_headers_custom[$i]['value'] . '" /></td>'; + echo '<td><input name="http_headers_custom[' . $i . '][name]" type="text" value="' . esc_attr($http_headers_custom[$i]['name']) . '" /></td>'; + echo '<td><input name="http_headers_custom[' . $i . '][value]" type="text" value="' . esc_attr($http_headers_custom[$i]['value']) . '" /></td>'; echo '</tr>'; }
Exploit Outline
1. Authentication: Log in to the WordPress dashboard as an Administrator. This exploit is particularly relevant on Multi-site installations or where 'unfiltered_html' is disabled. 2. Nonce Acquisition: Navigate to the HTTP Headers settings page (Settings > HTTP Headers) and click the 'Custom' tab. Extract the security nonce from the form (e.g., using `document.querySelector('input[name="_wpnonce"]').value`). 3. Payload Injection: Construct a POST request to `wp-admin/options.php` that includes the extracted nonce and an XSS payload (e.g., `"><script>alert(document.domain)</script>`) in the `http_headers_custom[0][value]` parameter. 4. Storage: The WordPress Settings API saves the malicious payload into the `http_headers_custom` option in the database. 5. Execution: The payload is triggered whenever an administrator visits the 'Custom' headers configuration sub-page, as the plugin echoes the stored value directly into the HTML without sanitization or attribute escaping.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.