GD Security Headers <= 1.8 - Unauthenticated Stored Cross-Site Scripting
Description
The GD Security Headers plugin for WordPress is vulnerable to Stored Cross-Site Scripting in versions up to, and including, 1.8 due to insufficient input sanitization and output escaping. This makes it possible for unauthenticated attackers to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:NTechnical Details
<=1.8What Changed in the Fix
Changes introduced in v1.9
Source Code
WordPress.org SVN# Exploitation Research Plan - CVE-2026-57403 ## 1. Vulnerability Summary The **GD Security Headers** plugin for WordPress (versions <= 1.8) contains an unauthenticated stored cross-site scripting (XSS) vulnerability. The plugin provides endpoints to receive and log Content Security Policy (CSP) an…
Show full research plan
Exploitation Research Plan - CVE-2026-57403
1. Vulnerability Summary
The GD Security Headers plugin for WordPress (versions <= 1.8) contains an unauthenticated stored cross-site scripting (XSS) vulnerability. The plugin provides endpoints to receive and log Content Security Policy (CSP) and X-XSS-Protection (XXP) violation reports from browsers. These endpoints fail to sufficiently sanitize the incoming JSON data and, more critically, the admin dashboard fails to escape this data when displaying it in the report logs. An attacker can send a forged report containing a malicious script, which will execute in the context of an administrator viewing the reports.
2. Attack Vector Analysis
- Endpoint: Any frontend page (triggered via
template_redirecthook). - Trigger Parameter: A GET parameter named
gdsih-csp-reportorgdsih-xxp-report. - Payload Delivery: A POST request with a JSON-encoded body containing the report data.
- Vulnerable Hook:
template_redirectcallinggdsih_component_xxp::log()orgdsih_component_csp::log(). - Authentication: None (Unauthenticated).
- Preconditions: The "Log Reports" setting for either CSP or XXP must be enabled in the plugin settings.
3. Code Flow
Entry Point (Ingestion):
- The class
gdsih_component_xxp(incore/objects/core.xxp.php) registers a listener ontemplate_redirect. - The
log()method checks forisset( $_GET['gdsih-xxp-report'] ). - It reads the raw POST body using
file_get_contents( 'php://input' )and decodes it as JSON. - It passes the
xss-reportobject to theevent()method. event()appliesd4p_sanitize_basic(a likely weak sanitization function) to the array and callsgdsih_db()->xxp_report().- The data is stored in the database (likely the
{wp_prefix}gdsih_xxp_reportstable).
- The class
Sink (Display):
- When an admin views the reports at
admin.php?page=gd-security-headers-xxp-reports, the classgdsih_xxp_report_grid(incore/grids/xxp.php) processes the data. - The
column_data()method (line ~198) iterates through the keysuser_agentandrequest_body. - Vulnerable Line:
$content .= '<tr><td>' . $key . '</td><td>' . $item->$key . '</td></tr>'; - The value of
$item->request_bodyis echoed directly into the HTML table without any escaping (esc_html, etc.).
- When an admin views the reports at
4. Nonce Acquisition Strategy
No nonce is required.
The reporting endpoints are designed to receive automated browser pings (CSP/XSS violation reports), which by specification do not include WordPress nonces. The log() functions in core/objects/core.csp.php and core/objects/core.xxp.php do not perform any authentication or nonce verification.
5. Exploitation Strategy
The exploit involves sending a crafted JSON report to the XXP reporting endpoint.
- Tool:
http_request - Method:
POST - URL:
http://localhost:8080/?gdsih-xxp-report - Headers:
Content-Type: application/json - Payload:
{
"xss-report": {
"request-url": "https://example.com/vulnerable-page",
"request-body": "<img src=x onerror=alert(`XSS_EXPLOITED`)><script>console.log(document.cookie)</script>"
}
}
Step-by-step Execution:
- Enable the XXP logging feature (see Test Data Setup).
- Send the
POSTrequest using the payload above. - The server should return a
204 No Contentresponse (as perhttp_response_code( 204 )inlog()). - Log in as an administrator and navigate to the XXP reports page.
- Click on the "View All Data" link for the new entry to trigger the rendering of the
request_bodyand execute the XSS.
6. Test Data Setup
Before exploitation, logging must be enabled. Since this is an admin setting, use WP-CLI to force it:
# Enable XXP logging via WP-CLI
wp eval "gdsih_settings()->set('log', true, 'xxp'); gdsih_settings()->save('xxp');"
# Clear existing reports to ensure a clean test
wp db query "TRUNCATE TABLE \$(wp db prefix)gdsih_csp_reports"
wp db query "TRUNCATE TABLE \$(wp db prefix)gdsih_xxp_reports"
7. Expected Results
- The
http_requestto/?gdsih-xxp-reportreturns HTTP204. - A new record appears in the database in the reports table.
- When the admin views the report grid at
/wp-admin/admin.php?page=gd-security-headers-xxp-reportsand clicks "View All Data", an alert box with "XSS_EXPLOITED" appears.
8. Verification Steps
After sending the payload, verify the database entry using WP-CLI:
# Check if the payload is stored in the database
wp db query "SELECT request_body FROM \$(wp db prefix)gdsih_xxp_reports ORDER BY id DESC LIMIT 1"
The output should contain the raw <img ...> or <script> tag.
9. Alternative Approaches
If the xxp-report endpoint is disabled or patched, the CSP report endpoint is equally vulnerable:
- URL:
/?gdsih-csp-report - Payload:
{
"csp-report": {
"document-uri": "https://example.com/",
"blocked-uri": "https://evil.com/malicious.js",
"violated-directive": "script-src",
"effective-directive": "script-src",
"referrer": "<script>alert('CSP_XSS')</script>",
"original-policy": "default-src 'self'"
}
}
The referrer field is handled by column_data in core/grids/csp.php in the same unescaped manner.
Summary
The GD Security Headers plugin for WordPress is vulnerable to unauthenticated stored cross-site scripting (XSS) due to insufficient input sanitization and output escaping of browser violation reports. Attackers can forge Content Security Policy (CSP) or X-XSS-Protection (XXP) reports containing malicious scripts, which are executed when an administrator views the reports in the plugin's dashboard.
Vulnerable Code
// core/objects/core.xxp.php private function event( $csp ) { $report = array_map( 'd4p_sanitize_basic', $csp ); gdsih_db()->xxp_report( array( 'request_url' => $report['request-url'], 'request_body' => $report['request-body'], ) ); } --- // core/grids/xxp.php (around line 183) foreach ( $keys as $key ) { if ( isset( $item->$key ) ) { $content .= '<tr><td>' . $key . '</td><td>' . $item->$key . '</td></tr>'; } } --- // core/grids/csp.php (around line 284) protected function column_default( $item, $column_name ) { return $item->$column_name; }
Security Fix
Only in /home/deploy/wp-safety.org/data/plugin-versions/gd-security-headers/1.9: changelog.md @@ -1 +1 @@ -<?php $build = 65; +<?php $build = 70; @@ -216,11 +216,11 @@ $keys = array( 'referrer', 'user_agent', 'original_policy' ); if ( isset( $item->referrer ) && $item->referrer != '' ) { - $show[] = '<abbr title="' . $item->referrer . '">' . __( 'Referer', 'gd-security-headers' ) . '</abbr>'; + $show[] = '<abbr title="' . esc_attr( $item->referrer ) . '">' . __( 'Referer', 'gd-security-headers' ) . '</abbr>'; } if ( isset( $item->user_agent ) && $item->user_agent != '' ) { - $show[] = '<abbr title="' . $item->user_agent . '">' . __( 'User Agent', 'gd-security-headers' ) . '</abbr>'; + $show[] = '<abbr title="' . esc_attr( $item->user_agent ) . '">' . __( 'User Agent', 'gd-security-headers' ) . '</abbr>'; } $content = empty( $show ) ? '' : '<br/>'; @@ -235,7 +235,7 @@ foreach ( $keys as $key ) { if ( isset( $item->$key ) ) { - $content .= '<tr><td>' . $key . '</td><td>' . $item->$key . '</td></tr>'; + $content .= '<tr><td>' . esc_html( $key ) . '</td><td>' . esc_html( $item->$key ) . '</td></tr>'; } } @@ -270,7 +270,7 @@ $title .= ' ' . __( 'Your server IP.', 'gd-security-headers' ); } - $content = sprintf( '<span title="%s" class="gdsih-ip-%s">%s</span>', trim( $title ), $status, $item->ip ); + $content = sprintf( '<span title="%s" class="gdsih-ip-%s">%s</span>', trim( $title ), $status, esc_html( $item->ip ) ); return $content . $this->row_actions( $actions ); } @@ -282,7 +282,7 @@ } protected function column_default( $item, $column_name ) { - return $item->$column_name; + return esc_html( $item->$column_name ); } public function prepare_items() { @@ -304,18 +304,18 @@ $search = isset( $_GET['s'] ) && $_GET['s'] != '' ? sanitize_text_field( $_GET['s'] ) : ''; if ( $violated_directive != '0' ) { - $where[] = "l.`violated_directive` = '$violated_directive'"; + $where[] = gdsih_db()->prepare( "l.`violated_directive` = %s", $violated_directive ); } if ( $effective_directive != '0' ) { - $where[] = "l.`effective_directive` = '$effective_directive'"; + $where[] = gdsih_db()->prepare( "l.`effective_directive` = %s", $effective_directive ); } if ( ! empty( $search ) ) { $_search = array(); foreach ( $this->_search_through_fields as $field ) { - $_search[] = "`" . $field . "` LIKE '%" . $search . "%'"; + $_search[] = gdsih_db()->prepare( "`" . $field . "` LIKE %s", '%' . $search . '%' ); } $where[] = '(' . join( ' OR ', $_search ) . ')'; @@ -165,7 +165,7 @@ $keys = array( 'user_agent', 'request_body' ); if ( isset( $item->user_agent ) && $item->user_agent != '' ) { - $show[] = '<abbr title="' . $item->user_agent . '">' . __( 'User Agent', 'gd-security-headers' ) . '</abbr>'; + $show[] = '<abbr title="' . esc_attr( $item->user_agent ) . '">' . __( 'User Agent', 'gd-security-headers' ) . '</abbr>'; } $content = empty( $show ) ? '' : '<br/>'; @@ -180,7 +180,7 @@ foreach ( $keys as $key ) { if ( isset( $item->$key ) ) { - $content .= '<tr><td>' . $key . '</td><td>' . $item->$key . '</td></tr>'; + $content .= '<tr><td>' . esc_html( $key ) . '</td><td>' . esc_html( $item->$key ) . '</td></tr>'; } } @@ -215,7 +215,7 @@ $title .= ' ' . __( 'Your server IP.', 'gd-security-headers' ); } - $content = sprintf( '<span title="%s" class="gdsih-ip-%s">%s</span>', trim( $title ), $status, $item->ip ); + $content = sprintf( '<span title="%s" class="gdsih-ip-%s">%s</span>', trim( $title ), $status, esc_html( $item->ip ) ); return $content . $this->row_actions( $actions ); } @@ -227,7 +227,7 @@ } protected function column_default( $item, $column_name ) { - return $item->$column_name; + return esc_html( $item->$column_name ); } public function prepare_items() { @@ -242,14 +242,14 @@ $where = array(); - $last = isset( $_GET['filter-period'] ) && ! empty( $_GET['filter-period'] ) ? d4p_sanitize_slug( $_GET['filter-period'] ) : ''; + $last = ! empty( $_GET['filter-period'] ) ? d4p_sanitize_slug( $_GET['filter-period'] ) : ''; $search = isset( $_GET['s'] ) && $_GET['s'] != '' ? sanitize_text_field( $_GET['s'] ) : ''; if ( ! empty( $search ) ) { $_search = array(); foreach ( $this->_search_through_fields as $field ) { - $_search[] = "`" . $field . "` LIKE '%" . $search . "%'"; + $_search[] = gdsih_db()->prepare( "`" . $field . "` LIKE %s", '%' . $search . '%' ); } $where[] = '(' . join( ' OR ', $_search ) . ')'; @@ -49,11 +49,11 @@ $report = array_map( 'd4p_sanitize_basic', $csp ); gdsih_db()->csp_report( array( - 'document_uri' => $report['document-uri'], - 'blocked_uri' => $report['blocked-uri'], - 'referrer' => $report['referrer'], - 'violated_directive' => $report['violated-directive'], - 'effective_directive' => $report['effective-directive'] ?? '', + 'document_uri' => sanitize_text_field( $report['document-uri'] ), + 'blocked_uri' => sanitize_text_field( $report['blocked-uri'] ), + 'referrer' => sanitize_text_field( $report['referrer'] ), + 'violated_directive' => sanitize_text_field( $report['violated-directive'] ), + 'effective_directive' => sanitize_text_field( $report['effective-directive'] ?? '' ), 'original_policy' => gdsih_settings()->get( 'log_original_policy', 'csp' ) ? $report['original-policy'] : '', ) ); } @@ -68,8 +68,8 @@ $report = array_map( 'd4p_sanitize_basic', $csp ); gdsih_db()->xxp_report( array( - 'request_url' => $report['request-url'], - 'request_body' => $report['request-body'], + 'request_url' => sanitize_text_field( $report['request-url'] ), + 'request_body' => sanitize_text_field( $report['request-body'] ), ) ); } }
Exploit Outline
The exploit involves sending a crafted JSON report to the unauthenticated report ingestion endpoints of the GD Security Headers plugin. By sending a POST request to the site root with either the `gdsih-csp-report` or `gdsih-xxp-report` query parameter, an attacker can provide a JSON body containing a malicious script in fields like `referrer` or `request-body`. If the plugin has logging enabled for CSP or XXP violations, it will store this data in the database with insufficient sanitization. When a WordPress administrator navigates to the plugin's report logs in the admin dashboard (e.g., `admin.php?page=gd-security-headers-xxp-reports`) and views the entry, the malicious script is rendered directly into the HTML without escaping, executing in the administrator's context.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.