Durante uma pesquisa no plugin miniOrange OTP Verification para WordPress, encontrei um comportamento interessante: a interface devolvia nomes de parâmetros GET para a resposta HTML. O detalhe é que esses nomes, quando inseridos sem escape no contexto de marcação, também podem se tornar uma entrada para XSS refletido.
O que mais chamou minha atenção não foi apenas a reflexão. Foi o caminho entre o navegador e o PHP. Eu precisava entender o que acontecia com os caracteres do payload quando eles eram usados como nome do parâmetro, e não como valor.
Identificação e divulgação responsável
A vulnerabilidade que reportei recebeu o identificador CVE-2026-61957. A Patchstack registrou meu crédito como Tarcísio Luchesi (Poystick), com reporte em 12 de junho de 2026 e publicação em 27 de julho de 2026. O problema afeta o miniOrange OTP Verification até a versão 5.5.1; a correção está disponível a partir da 5.5.2.
O registro classifica a falha como CWE-79, XSS refletido não autenticado, com pontuação CVSS 3.1 de 7.1 (alta) e necessidade de interação da vítima para exploração. A descrição oficial é resumida; o mecanismo detalhado abaixo é apresentado com base na minha análise técnica.
Referências públicas: Patchstack · CVE Record.
O ponto de entrada: a chave GET
Em testes mais comuns de XSS, normalmente controlamos um valor, como ?q=teste. Neste caso, a hipótese era diferente: o nome do parâmetro poderia chegar ao HTML sem codificação contextual adequada. Por isso, olhar apenas para $_GET['q'] não era suficiente; eu precisava observar como a aplicação recuperava e exibia as próprias chaves.
GET /pagina-de-teste?chave_controlada=1 HTTP/1.1
Host: laboratorio.example
Se chave_controlada aparece em uma resposta HTML, o próximo passo é verificar onde ela aparece: em texto, em um atributo, dentro de JavaScript ou em outro contexto. É essa posição que determina se a reflexão é perigosa e qual forma de escape seria necessária.
O obstáculo: a normalização de nomes no PHP
O PHP possui um comportamento específico ao interpretar nomes de parâmetros externos. Caracteres como pontos e espaços podem ser convertidos para underscores. Colchetes, por sua vez, participam da sintaxe de arrays e também afetam como a chave é interpretada.
// Exemplo conceitual de interpretação de chaves:
?nome.campo=1 → nome_campo
?nome%20campo=1 → nome_campo
Isso afeta diretamente expressões JavaScript que contêm pontos. Se eu colocar uma expressão como document.cookie em uma chave que passa pela normalização do PHP, o ponto pode deixar de existir. O JavaScript que chega ao navegador já não é a mesma expressão que eu havia planejado.
Esse detalhe também exige cuidado na análise: nem toda aplicação usa a chave normalizada para gerar a resposta. É necessário observar o HTML efetivamente devolvido, porque um sistema também pode refletir a query string original.
Minha abordagem: payload sem pontos (dotless)
Em vez de insistir na notação tradicional de propriedades do JavaScript, trabalhei com uma representação codificada. A ideia foi manter a expressão original dentro de uma string Base64 e reconstruí-la apenas no navegador, com atob() e o construtor Function.
Assim, a expressão sensível aos caracteres do nome do parâmetro deixa de aparecer diretamente na chave GET. O navegador faz a decodificação depois que a resposta já foi interpretada como código JavaScript.
PoC simplificada para compreender a técnica
Para mostrar o mecanismo sem publicar uma rotina de coleta de sessão, este é um exemplo autocontido e inofensivo, adequado a um console ou laboratório próprio:
Function(atob('YWxlcnQoMSk'))()
A string decodificada corresponde a:
alert(1)
O Base64 desse exemplo foi escrito sem o caractere final de padding =; a função atob() aceita essa forma neste caso. O objetivo não é dizer que todo payload desse tipo funciona em qualquer aplicação, mas demonstrar por que uma representação codificada pode evitar certos caracteres na superfície de entrada.
document.cookie. Nesta publicação, preservei o mecanismo técnico, mas substituí a rotina de exfiltração por uma demonstração local. A publicação não inclui domínio real de teste, sessão ou identificador de Webhook.Por que isso pode ultrapassar uma filtragem?
Uma filtragem que dependa de detectar padrões literais como certas propriedades do JavaScript pode não enxergar o conteúdo depois de codificado. O mesmo raciocínio vale para regras de WAF que tratam alguns caracteres ou assinaturas isoladamente. No entanto, um payload ter uma forma diferente não comprova, por si só, um bypass universal de WAF.
Para afirmar que contornei uma regra específica, preciso comparar as respostas do ambiente: o bloqueio do payload original, a aceitação da variação e, principalmente, a execução real no navegador. É a evidência, não apenas a string, que determina a conclusão.
Impacto e limites do que a PoC demonstra
Uma XSS refletida explorável permite executar JavaScript na origem da aplicação quando a vítima acessa uma URL preparada. Isso pode viabilizar interações indevidas com a interface e acesso a informações que estejam disponíveis ao JavaScript naquela origem.
No cenário descrito no relatório, a leitura de document.cookie foi considerada como possibilidade de impacto. Porém, cookies protegidos com HttpOnly não ficam acessíveis por essa API. Além disso, ler um cookie não equivale automaticamente a demonstrar Account Takeover. Uma conclusão desse tipo requer evidências adicionais do mecanismo de sessão e do contexto de exploração.
Outra distinção importante: se a reflexão ocorre em uma tela administrativa, o alcance do ataque depende de quem pode acessar essa tela, de como a requisição é construída e das proteções efetivas da aplicação, incluindo CSP.
Como corrigir o problema
A correção deve acontecer no contexto de saída, impedindo que um nome de parâmetro controlado seja interpretado como HTML ou JavaScript. Em WordPress, eu começaria pelos pontos que renderizam os dados:
esc_html()para conteúdo exibido como texto HTML;esc_attr()quando o dado precisa aparecer em um atributo HTML;esc_url()quando a saída é uma URL;- validação explícita dos nomes de parâmetros esperados, em vez de refletir chaves arbitrárias.
// Exemplo para exibição de texto HTML em WordPress
echo esc_html($nome_do_parametro);
Uma Content Security Policy restritiva pode acrescentar defesa em profundidade, mas não substitui escape contextual e validação na aplicação.
O que levei dessa pesquisa
O que eu achei mais interessante nesse caso foi perceber que o nome de um parâmetro também pode ser uma superfície de ataque. A investigação não terminou quando um payload tradicional deixou de funcionar. Eu precisei seguir a transformação da entrada pelo PHP, entender quais caracteres se perdiam e pensar em uma maneira de preservar a lógica JavaScript até a etapa de execução.
Esse é um aprendizado que tento aplicar nos meus testes: antes de mudar aleatoriamente um payload, entender o parser, a representação dos dados e o contexto onde eles serão usados. Foi essa diferença que tornou a análise interessante para mim.