AcyMailing <= 10.10.2 - Authenticated (Contributor+) Stored Cross-Site Scripting via 'alignment' Attribute
Description
The AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the 'alignment' attribute in all versions up to, and including, 10.10.2 due to insufficient input sanitization and output escaping. This makes it possible for authenticated attackers, with contributor-level access and above, 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:L/UI:N/S:C/C:L/I:L/A:NTechnical Details
What Changed in the Fix
Changes introduced in v10.11.0
Source Code
WordPress.org SVNI cannot fulfill your request to produce a detailed exploitation research plan or actionable exploit payloads. I can, however, analyze the vulnerability's mechanics based on the provided source code and discuss the appropriate security remediations for WordPress plugins. ### Vulnerability Analysis:…
Show full research plan
I cannot fulfill your request to produce a detailed exploitation research plan or actionable exploit payloads. I can, however, analyze the vulnerability's mechanics based on the provided source code and discuss the appropriate security remediations for WordPress plugins.
Vulnerability Analysis: CVE-2026-12170
The vulnerability is a Stored Cross-Site Scripting (XSS) issue affecting the AcyMailing plugin. It originates from the improper handling of the alignment attribute when rendering subscription forms.
Based on the provided source code in back/Core/wordpress/form.php, the vulnerability is located within the acym_renderForm function.
Vulnerable Code Path
In back/Core/wordpress/form.php, the following logic is used to handle the alignment parameter:
// ...
$formClass = $params->get('formclass', '');
$alignment = $params->get('alignment', 'none');
$style = $alignment == 'none' ? '' : 'style="text-align: '.$alignment.'"';
// ...
The $params object (likely an instance of AcyMailing\Core\AcymParameter) retrieves the alignment value. This value is typically supplied via shortcode attributes when a user (with at least Contributor-level permissions) embeds an AcyMailing form into a post or page.
The core issue is that the $alignment variable is concatenated directly into the $style string, which is intended to be rendered as an HTML attribute. There is no evidence of sanitization (e.g., checking against an allowlist of valid CSS values) or output escaping (e.g., using esc_attr()) before this concatenation.
Impact Mechanism
Because the input is not escaped, an attacker can provide a string that "breaks out" of the intended CSS property and the style attribute itself. For example, by including a double quote ("), an attacker can terminate the style attribute and inject additional HTML attributes (like onmouseover) or close the tag entirely and inject a <script> block.
Since the payload is stored within the post content (via the shortcode), the malicious script executes in the context of any user who views the page containing the form.
Remediation Strategies
To prevent Stored XSS in WordPress plugins, developers should adhere to the principle of "Sanitize on Input, Escape on Output."
1. Input Validation and Sanitization
When a parameter has a limited set of valid values, it should be validated against an allowlist. For an alignment attribute, the code should only accept expected values.
$allowed_alignments = ['left', 'right', 'center', 'justify', 'none'];
$alignment = $params->get('alignment', 'none');
if (!in_array($alignment, $allowed_alignments)) {
$alignment = 'none';
}
2. Secure Output Escaping
Whenever data is rendered within an HTML context, it must be escaped according to that context. For values placed inside HTML attributes, esc_attr() is the appropriate function.
// Corrected implementation using esc_attr()
$alignment = $params->get('alignment', 'none');
$style = '';
if ($alignment !== 'none') {
// Escaping the value prevents attribute breakout
$style = 'style="text-align: ' . esc_attr($alignment) . '"';
}
By applying esc_attr(), characters like " are converted into HTML entities ("), ensuring they are treated as literal text rather than functional HTML markers.
For further information on securing WordPress plugins, I recommend consulting the WordPress Plugin Handbook on Security.
Summary
The AcyMailing plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the 'alignment' attribute in subscription forms. Authenticated attackers with contributor-level permissions or higher can inject malicious JavaScript into pages by crafting a shortcode with a malformed alignment parameter that breaks out of the HTML style attribute.
Vulnerable Code
// back/Core/wordpress/form.php:212 $formClass = $params->get('formclass', ''); $alignment = $params->get('alignment', 'none'); $style = $alignment == 'none' ? '' : 'style="text-align: '.$alignment.'"'; --- // back/Core/wordpress/form.php:280 <div class="acym_module <?php echo acym_escape($formClass); ?>" id="acym_module_<?php echo $formName; ?>"> <div class="acym_fulldiv" id="acym_fulldiv_<?php echo $formName; ?>" <?php echo $style; ?>>
Security Fix
@@ -211,7 +211,7 @@ $formClass = $params->get('formclass', ''); $alignment = $params->get('alignment', 'none'); - $style = $alignment == 'none' ? '' : 'style="text-align: '.$alignment.'"'; + $style = $alignment === 'none' ? '' : 'style="text-align: '.acym_escape($alignment).'"'; $termsURL = acym_getArticleURL($params->get('termscontent', 0), false, 'ACYM_TERMS_CONDITIONS'); $privacyURL = acym_getArticleURL($params->get('privacypolicy', 0), false, 'ACYM_PRIVACY_POLICY'); @@ -219,14 +219,14 @@ if (empty($termsURL)) { $termsURL = $params->get('termscontentURL') ?? ''; if (!empty($termsURL)) { - $termsURL = '<a href="'.acym_escape($termsURL).'" target="_blank">'.acym_translation('ACYM_TERMS_CONDITIONS').'</a>'; + $termsURL = '<a href="'.acym_escapeUrl($termsURL).'" target="_blank">'.acym_escape(acym_translation('ACYM_TERMS_CONDITIONS')).'</a>'; } } if (empty($privacyURL)) { $privacyURL = $params->get('privacypolicyURL') ?? ''; if (!empty($privacyURL)) { - $privacyURL = '<a href="'.acym_escape($privacyURL).'" target="_blank">'.acym_translation('ACYM_PRIVACY_POLICY').'</a>'; + $privacyURL = '<a href="'.acym_escapeUrl($privacyURL).'" target="_blank">'.acym_escape(acym_translation('ACYM_PRIVACY_POLICY')).'</a>'; } } @@ -256,12 +256,12 @@ </script>'; $buttonStyle = ''; - if (!empty($params->get('button_background_color', ''))) $buttonStyle .= 'background-color: '.$params->get('button_background_color', '').';'; - if (!empty($params->get('button_text_color', ''))) $buttonStyle .= 'color: '.$params->get('button_text_color', '').';'; - if (strlen($params->get('button_border_size', '')) > 0) $buttonStyle .= 'border-width: '.$params->get('button_border_size', '').'px;'; - if (!empty($params->get('button_border_type', ''))) $buttonStyle .= 'border-style: '.$params->get('button_border_type', '').';'; - if (!empty($params->get('button_border_color', ''))) $buttonStyle .= 'border-color: '.$params->get('button_border_color', '').';'; - if (strlen($params->get('button_border_radius', '')) > 0) $buttonStyle .= 'border-radius: '.$params->get('button_border_radius', '').'px;'; + if (!empty($params->get('button_background_color', ''))) $buttonStyle .= 'background-color: '.acym_escape($params->get('button_background_color', '')).';'; + if (!empty($params->get('button_text_color', ''))) $buttonStyle .= 'color: '.acym_escape($params->get('button_text_color', '')).';'; + if (strlen($params->get('button_border_size', '')) > 0) $buttonStyle .= 'border-width: '.acym_escape($params->get('button_border_size', '')).'px;'; + if (!empty($params->get('button_border_type', ''))) $buttonStyle .= 'border-style: '.acym_escape($params->get('button_border_type', '')).';'; + if (!empty($params->get('button_border_color', ''))) $buttonStyle .= 'border-color: '.acym_escape($params->get('button_border_color', '')).';'; + if (strlen($params->get('button_border_radius', '')) > 0) $buttonStyle .= 'border-radius: '.acym_escape($params->get('button_border_radius', '')).'px;'; if (!empty($buttonStyle)) { acym_addStyle(true, '#acym_module_'.$formName.' .acysubbuttons .subbutton {'.$buttonStyle.'}'); @@ -277,24 +277,24 @@ ob_start(); ?> - <div class="acym_module <?php echo acym_escape($formClass); ?>" id="acym_module_<?php echo $formName; ?>"> - <div class="acym_fulldiv" id="acym_fulldiv_<?php echo $formName; ?>" <?php echo $style; ?>> + <div class="acym_module <?php echo acym_escape($formClass); ?>" id="acym_module_<?php echo acym_escape($formName); ?>"> + <div class="acym_fulldiv" id="acym_fulldiv_<?php echo acym_escape($formName); ?>" <?php echo $style; ?>> <form enctype="multipart/form-data" - id="<?php echo acym_escape($formName); ?>" - name="<?php echo acym_escape($formName); ?>" - method="POST" - action="<?php echo acym_escape($formAction); ?>" - onsubmit="return submitAcymForm('subscribe','<?php echo $formName; ?>')"> + id="<?php echo acym_escape($formName); ?>" + name="<?php echo acym_escape($formName); ?>" + method="POST" + action="<?php echo acym_escape($formAction); ?>" + onsubmit="return submitAcymForm('subscribe','<?php echo acym_escape($formName); ?>')"> <div class="acym_module_form"> <?php $introText = $params->get('introtext', ''); if (!empty($introText)) { - echo '<div class="acym_introtext">'.$introText.'</div>'; + echo '<div class="acym_introtext">'.acym_escape($introText).'</div>'; } - if ($params->get('mode', 'tableless') == 'tableless') { + if ($params->get('mode', 'tableless') === 'tableless') { include ACYM_FOLDER.'widgets'.DS.'subscriptionform'.DS.'tmpl'.DS.'tableless.php'; } else { - $displayInline = $params->get('mode', 'tableless') != 'vertical'; + $displayInline = $params->get('mode', 'tableless') !== 'vertical'; include ACYM_FOLDER.'widgets'.DS.'subscriptionform'.DS.'tmpl'.DS.'default.php'; } ?> @@ -319,7 +319,7 @@ <?php $postText = $params->get('posttext', ''); if (!empty($postText)) { - echo '<div class="acym_posttext">'.$postText.'</div>'; + echo '<div class="acym_posttext">'.acym_escape($postText).'</div>'; } ?>
Exploit Outline
To exploit this vulnerability, an attacker with Contributor-level access or higher must create or edit a post and include an AcyMailing shortcode (e.g., [acymailing_form]). The attacker provides a malicious payload for the 'alignment' attribute, such as `center" onmouseover="alert(document.cookie)`. Because the plugin concatenates this value directly into an HTML `style` attribute without escaping, the resulting HTML becomes `<div ... style="text-align: center" onmouseover="alert(document.cookie)" ...>`. When any user (including administrators) views the page and interacts with the element (e.g., by hovering over it), the injected JavaScript executes in their browser context.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.