
# Docker-лаборатория для воспроизведения CVE-2025-11262 — неаутентифицированное сохранённое слепое XSS в бесплатном плагине Link Whisper для WordPress Включает уязвимую и пропатченную цели, PoC-скрипт и анализ первопричины.
Этот репозиторий содержит локальный Docker-стенд для воспроизведения CVE-2025-11262 — проблемы неаутентифицированного сохранённого межсайтового скриптинга (stored XSS), затрагивающей плагин WordPress Link Whisper Free.
Стенд сравнивает две версии плагина:
| Service | Plugin version | Purpose | URL |
|---|---|---|---|
vuln | 0.9.0 | Vulnerable target | http://127.0.0.1:8081 |
patched | 0.9.1 | Patched comparison target | http://127.0.0.1:8082 |
Демонстрируемая цепочка уязвимости:
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
Атакующему не нужно входить в систему, чтобы внедрить сохранённую полезную нагрузку. JavaScript выполняется позже, когда привилегированный пользователь WordPress открывает затронутую страницу административной панели.
Этот стенд предназначен только для контролируемого локального исследования, понимания на уровне исходного кода и демонстрации в портфолио.
| Claim | Evidence | How 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. |
Это раздел Root Cause Summary, который можно подставить в README. Он безопасен для публикации, не упоминает внутренние файлы вроде vuln_detail.txt и опирается на стенд/исходный код, который вы используете сейчас.
CVE-2025-11262 вызвана цепочкой сохранённой инъекции JavaScript в Link Whisper Free 0.9.0.
Проблема не в одном пропущенном вызове экранирования. Это цепочка нескольких небезопасных действий:
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
Link Whisper Free регистрирует REST-эндпоинт аутентификации AI в REST-пространстве имён плагина:
const REST_SLUG = 'link-whisper';
const AI_AUTH = 'ai-auth';
Эндпоинт регистрируется как маршрут POST:
register_rest_route(self::REST_SLUG, self::AI_AUTH, [
'methods' => 'POST',
'callback' => [
$this,
'ai_auth_handler'
],
'permission_callback' => "__return_true",
'show_in_index' => false
]);
Поскольку callback проверки прав — __return_true, эндпоинт доступен без аутентификации.
В лабораторном стенде фактический эндпоинт:
/wp-json/link-whisper/ai-auth
Это означает, что неаутентифицированный атакующий может отправить запрос к эндпоинту без сессии WordPress, nonce или учётной записи администратора.
В Link Whisper Free 0.9.0 обработчик читает управляемые атакующим параметры из REST-запроса:
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 ]);
}
Уязвимое поведение обусловлено слабым условием проверки:
if(!empty($token) && false !== strpos($token, 'ai-')){
Оно лишь проверяет, содержит ли переданный токен доступа строку ai-.
Строгая проверка user_id перед сохранением отсутствует:
update_option('wpil_ai_access_user_id', $user_id);
В результате управляемый атакующим JavaScript может быть сохранён в таблице параметров (options) WordPress.
Управляемое атакующим значение user_id сохраняется в параметре WordPress:
wpil_ai_access_user_id
В этом стенде PoC отправляет следующую локальную полезную нагрузку:
</script><script>alert("CVE-2025-11262-LAB")</script>
На уязвимом сервисе полезная нагрузка сохраняется как значение wpil_ai_access_user_id.
Атакующему не нужно входить в систему, чтобы внедрить полезную нагрузку. Она внедряется через неаутентифицированный REST-эндпоинт.
Сохранённое значение позже извлекается через логику настроек плагина:
public static function get_linkwhisper_ai_user_id(){
return get_option('wpil_ai_access_user_id', '');
}
Значение присваивается переменной $ai_id и выводится на страницу AI Subscription в административной панели.
В Link Whisper Free 0.9.0 значение вставляется непосредственно в строку JavaScript:
body: JSON.stringify({
ai_id: "<?php echo $ai_id;?>",
subscription_id: "<?php echo ((!empty($sub)) && isset($sub->subscription_id)) ? $sub->subscription_id: null;?>"
})
Поскольку $ai_id не экранируется перед вставкой в контекст JavaScript, сохранённая полезная нагрузка может выйти за пределы предполагаемой строки и выполнить JavaScript при открытии страницы административной панели.
С лабораторной полезной нагрузкой уязвимый выводимый результат становится эквивалентен:
body: JSON.stringify({
ai_id: "</script><script>alert("CVE-2025-11262-LAB")</script>",
subscription_id: ""
})
В браузере внедрённый закрывающий тег </script> завершает исходный блок скрипта, а внедрённый блок <script> выполняется.
Полезная нагрузка внедряется неаутентифицированным атакующим, но для выполнения требуется, чтобы привилегированный пользователь WordPress открыл затронутую страницу административной панели:
/wp-admin/admin.php?page=link_whisper_ai_subscription
В этом стенде затронутая страница открывается под учётной записью администратора WordPress, чтобы вызвать диалоговое окно alert.
Это делает проблему неаутентифицированным сохранённым XSS, направленным на аутентифицированных администраторов WordPress или привилегированных пользователей, которые могут открывать страницу Link Whisper AI Subscription в административной панели.
Link Whisper Free 0.9.1 добавляет более строгую проверку перед сохранением значений аутентификации AI.