WordPress trae una lista de control de acceso completa y casi nadie la usa. El fallo de permisos que más vemos en sitios de clientes no es un plugin sin parchear, es un redactor de contenido con el rol de Administrador porque alguien necesitó que reordenara un menú una vez. Esta guía cubre lo que la API de roles y capacidades hace de verdad a nivel de código fuente: dónde viven los datos, por qué add_role() ignora en silencio tu segunda llamada, a qué se resuelve realmente una comprobación de nombre de rol, y qué capacidades son más amplias de lo que su nombre sugiere.
Descubre más sobre servicios de seguridad WordPress en WPPoland.
Un solo cambio hace casi todo el trabajo: deja de nombrar roles en tus condicionales y nombra la capacidad que el código necesita. El resto de esta guía es lo que eso cuesta en la práctica, y es la base del trabajo de cualquier desarrollador WordPress que se tome en serio la seguridad.
1. Conceptos: rol vs capacidad
El sistema de permisos de WordPress se basa en dos conceptos que conviene separar antes de escribir cualquier lógica de control de acceso.
Capacidad (cap)
Un permiso específico para hacer una cosa. Las capacidades son las unidades atómicas del sistema. Ejemplos: edit_posts, publish_pages, install_plugins, manage_options y edit_theme_options.
Las capacidades pueden ser primitivas (almacenadas directamente en la fila de roles) o meta capacidades (resueltas en tiempo de ejecución por map_meta_cap() según el contexto). Por ejemplo, edit_post es una meta capacidad que se resuelve a edit_posts o edit_others_posts según si el usuario es el autor de la publicación.
Rol
Una colección de capacidades agrupadas bajo un nombre. WordPress almacena el conjunto y evalúa la cadena.
Los conjuntos por defecto son más pequeños de lo que se asume. Según la documentación de roles y capacidades, Editor tiene 26 capacidades y Autor siete: read, upload_files, edit_posts, edit_published_posts, publish_posts, delete_posts, delete_published_posts. Colaborador tiene tres: read, edit_posts, delete_posts. Un Colaborador no puede subir un archivo, que es por lo que “hazle Colaborador y ya” acaba en un ticket sobre la biblioteca de medios.
Editor es el caso interesante. En una instalación de sitio único lleva unfiltered_html, es decir que un Editor puede guardar una etiqueta <script> en el contenido. En multisitio esa capacidad se deniega a todos menos a los super administradores, como muestra map_meta_cap():
if ( defined( 'DISALLOW_UNFILTERED_HTML' ) && DISALLOW_UNFILTERED_HTML ) {
$caps[] = 'do_not_allow';
} elseif ( is_multisite() && ! is_super_admin( $user_id ) ) {
$caps[] = 'do_not_allow';
} else {
$caps[] = 'unfiltered_html';
}Lo que Editor no lleva: edit_theme_options, list_users, edit_users, manage_options ni nada relacionado con plugins.
Regla de oro
Verifica capacidades, nunca nombres de rol.
// INCORRECTO - nunca hagas esto
if ( current_user_can( 'administrator' ) ) { ... }
// CORRECTO - siempre verifica capacidades
if ( current_user_can( 'manage_options' ) ) { ... }La primera línea no es un error de sintaxis, es algo peor: devuelve la respuesta correcta justo en el caso que probaste. El motivo es una fusión al final de WP_User::get_role_caps():
$this->allcaps = array();
foreach ( (array) $this->roles as $role ) {
$the_role = $wp_roles->get_role( $role );
$this->allcaps = array_merge( (array) $this->allcaps, (array) $the_role->capabilities );
}
$this->allcaps = array_merge( (array) $this->allcaps, (array) $this->caps );$this->caps es el meta de usuario wp_capabilities en bruto, y sus claves son nombres de rol. Por eso administrator => true aterriza en allcaps junto a capacidades reales y pasa la comprobación por accidente. La documentación de current_user_can() lo dice así: comprobar roles concretos en lugar de una capacidad está soportado solo en parte y se desaconseja, porque puede producir resultados poco fiables.
Tres formas en que eso muerde, las tres vistas en sitios reales:
- Creas un rol personalizado con
manage_optionspara que un cliente llegue a tu página de ajustes.current_user_can('administrator')esfalsepara él, y tu propia página devuelve 403 al usuario para el que se hizo. - Una capacidad concedida directamente sobre el objeto usuario, sin rol asociado, pasa cualquier comprobación de capacidad y falla cualquier comprobación de rol.
- En multisitio,
WP_User::has_cap()corta el circuito para super administradores antes de mirarallcapsy devuelvetruepara todo lo que no seado_not_allow. Ahí una comprobación de rol no discrimina nada.
2. Creando un rol personalizado
Antes de escribir uno, comprueba si la plataforma ya lo trae. WooCommerce añade shop_manager y customer al instalarse y, según su documentación de roles, Gerente de tienda ya puede gestionar productos, pedidos, cupones y cuentas de clientes. Construir un “gerente de tienda” paralelo duplica un rol que mantiene el ciclo de versiones de otro.
Cuando de verdad necesitas uno, esta es la forma:
add_role(
'editorial_lead',
'Responsable editorial',
[
'read' => true,
'upload_files' => true,
'edit_posts' => true,
'edit_others_posts' => true,
'edit_published_posts' => true,
'publish_posts' => true,
'delete_posts' => true,
'moderate_comments' => true,
]
);Dos cosas de esa llamada no se deducen de la firma.
Escribe una vez y luego te ignora. El manual de plugins indica que tras la primera llamada el rol y sus capacidades quedan guardados en la base de datos, y que las llamadas sucesivas no hacen nada, incluido modificar la lista de capacidades. Editas el array, despliegas, y no cambia nada. Cada cambio de capacidades necesita por tanto una migración explícita:
const EDITORIAL_LEAD_VERSION = 3;
function wppoland_sync_editorial_lead() {
if ( (int) get_option( 'wppoland_editorial_lead_version' ) === EDITORIAL_LEAD_VERSION ) {
return;
}
remove_role( 'editorial_lead' );
add_role( 'editorial_lead', 'Responsable editorial', [ /* ... */ ] );
update_option( 'wppoland_editorial_lead_version', EDITORIAL_LEAD_VERSION );
}
register_activation_hook( __FILE__, 'wppoland_sync_editorial_lead' );
add_action( 'admin_init', 'wppoland_sync_editorial_lead' );La copia en admin_init importa en multisitio y en despliegues que nunca disparan un hook de activación. La comprobación de versión es lo que mantiene el código fuera de la ruta caliente, y el manual avisa de que eliminar y volver a crear el rol sin condición degrada el rendimiento de forma considerable.
remove_role() no toca a los usuarios. Elimina la clave de la fila de opciones. Cada usuario cuyo meta wp_capabilities siga diciendo editorial_lead => true conserva esa clave, WP_Roles::is_role() ya devuelve false para ella, y por tanto nunca se resuelve en capacidades. El usuario se queda con una cadena que no concede nada. Reasigna a los usuarios antes de eliminar un rol, o inmediatamente después.
Dónde está realmente la fila. WP_Roles::for_site() construye la clave como $wpdb->get_blog_prefix( $this->site_id ) . 'user_roles'. En un sitio único con el prefijo por defecto eso es wp_user_roles. En el subsitio 3 de una red es wp_3_user_roles, y por eso un rol añadido en un subsitio es invisible en el siguiente. Si quieres los roles definidos en código y nunca escritos en la base de datos, define la global $wp_user_roles en wp-config.php: WordPress la usa entonces y, en palabras de la referencia del core, la opción de roles no se actualiza ni se utiliza.
Roles multiidioma
Si tu sitio WordPress es multiidioma, asegúrate de que las etiquetas de los roles estén traducidas correctamente. Los nombres internos de los roles (slugs) deben permanecer en inglés para compatibilidad, pero las etiquetas mostradas al usuario pueden localizarse usando las funciones de internacionalización de WordPress.
3. Agregando capacidades a roles existentes
get_role() más add_cap() es el cambio más pequeño cuando un rol por defecto casi encaja:
$role = get_role( 'editor' );
if ( $role ) {
$role->add_cap( 'edit_theme_options' );
}Aplica la misma comprobación de versión de la sección 2. WP_Role::add_cap() acepta exactamente dos parámetros, add_cap( $cap, $grant = true ), y el segundo decide si la capacidad se concede o se deniega explícitamente, no si la fila se escribe. No hay un tercer parámetro aquí. La forma de tres argumentos pertenece a WP_Roles::add_cap( $role, $cap, $grant ), otra clase, y es el método al que WP_Role::add_cap() delega. Ese método asigna la capacidad y llama a update_option() siempre que use_db sea true, en cada llamada. Ningún argumento suprime la escritura, así que ejecutar add_cap() en init sin guardia significa un intento de escritura de opción en cada petición.
Consideraciones importantes
El problema mayor de esa línea concreta es el alcance. edit_theme_options es el arreglo clásico de “que el Editor gestione los menús”, y en un tema clásico hace más o menos eso: Apariencia para widgets, menús, personalizador y cabecera. En un tema de bloques es otra capacidad. La documentación del core la nombra ahora como la capacidad principal que WordPress comprueba al decidir si un usuario puede acceder a las plantillas y gestionarlas desde el editor del sitio, y avisa de que no se limita en exclusiva al editor del sitio y puede dar acceso a otras funciones administrativas relacionadas con el tema.
Es decir, el Editor al que querías dejar reordenar un menú tiene ahora el editor del sitio: plantillas, partes de plantilla, estilos globales y navegación. Es una concesión más amplia que switch_themes, que solo expone Apariencia y Apariencia > Temas.
Si lo que quieres de verdad es un rol para el editor del sitio, la documentación lista lo que hace falta: edit_theme_options para el acceso y el menú de navegación, más edit_posts, edit_pages y edit_others_posts para gestionar plantillas, read para previsualizar y upload_files para medios.
El modo de fallo aquí es silencioso, porque el editor del sitio habla con la API REST. Los endpoints de plantillas y partes de plantilla comprueban edit_theme_options; los de entradas y páginas hacen sus propias comprobaciones. Un rol que puede abrir el editor del sitio pero no tiene edit_others_posts obtiene un panel que carga y luego falla al guardar. Lee la pestaña de red buscando respuestas 401 y 403 antes de añadir capacidades a ciegas: el código de error REST nombra el permiso que falló.
Capacidades condicionales
En escenarios avanzados puedes necesitar capacidades que dependan del contexto. Por ejemplo, un editor que solo puede publicar en ciertas categorías, o un gestor que solo puede ver pedidos de su región. WordPress permite implementarlo con el filtro user_has_cap, que da control sobre la evaluación de capacidades en tiempo de ejecución.
add_filter('user_has_cap', function($allcaps, $caps, $args) {
// Logica personalizada de verificación de capacidades
return $allcaps;
}, 10, 3);4. Recuperación ante desastres: restableciendo roles
La versión de este script que circula, y que ediciones anteriores de este artículo incluían, es una escalada de privilegios remota:
// No despliegues esto.
function wppoland_reset_roles() {
if ( ! isset( $_GET['reset_roles_secret_key'] ) ) return;
require_once( ABSPATH . 'wp-admin/includes/schema.php' );
populate_roles();
}
add_action( 'init', 'wppoland_reset_roles' );isset() comprueba que el parámetro esté presente, no que sea correcto. Cualquier visitante sin autenticar que añada ?reset_roles_secret_key a cualquier URL del sitio restaura las capacidades por defecto, lo que incluye devolver todo lo que hubieras quitado a propósito a Editor o Autor.
Usa WP-CLI en su lugar
wp role reset corre sobre la misma función del core y está autenticado por el acceso a la shell:
wp role reset --all
wp role reset editorHace además más que populate_roles() por su cuenta, y la diferencia importa al depurar. populate_roles() llama a populate_roles_160() hasta populate_roles_300(), y esas funciones solo llaman a add_role() y add_cap(). No hay ningún remove_cap() en ellas. Llamada directamente, restaura capacidades eliminadas y deja todas las que un plugin haya añadido. WP-CLI compara además contra una instalación limpia, y por eso su salida se lee así:
Restored 1 capability to and removed 0 capabilities from 'administrator' role.Los roles personalizados quedan intactos por cualquiera de las dos vías. Si el problema del sitio es un rol personalizado mal hecho, un reset no lo arregla.
Inspecciona antes de restablecer
El estado que estás depurando es una fila de opciones:
wp option get wp_user_roles --format=json | jq '.editor.capabilities'
wp cap list editor
wp user list --field=user_login --role=administratorEse último es el comando que merece la pena correr en cualquier sitio que heredas. Un recuento de administradores que te sorprende es el hallazgo.
5. Mejores prácticas de seguridad 2026
A. Poda la lista de administradores
El mínimo privilegio no es un rol nuevo, es una lista de administradores más corta. Ejecuta wp user list --role=administrator en los sitios que mantienes. Cuentas de agencia, un freelance que se fue y el acceso de soporte de un plugin de hace dos años son los residentes habituales.
Sobre el consejo clásico de no llamar ‘admin’ al usuario: el instalador pide nombre de usuario desde WordPress 3.0, así que el hueco que queda es la enumeración. El endpoint REST de usuarios devuelve id, name, url, description, link, slug y avatar_urls sin autenticación. roles y capabilities van en contexto edit y no se exponen, pero slug es el user_nicename, que WordPress deriva del login salvo que alguien lo cambie. Eso es una lista de nombres de usuario, no una brecha, y limitar la tasa del formulario de acceso es el control que pesa más.
B. Cierra las rutas de archivo con constantes, no con capacidades
Quitar edit_themes a Administrador es un cambio de capacidad que alguien puede deshacer. Una constante en wp-config.php se comprueba dentro de map_meta_cap() y no se puede conceder por encima:
define( 'DISALLOW_FILE_EDIT', true );DISALLOW_FILE_EDIT mapea edit_files, edit_plugins y edit_themes a do_not_allow. DISALLOW_FILE_MODS va más lejos y bloquea la instalación y actualización de plugins y temas desde el escritorio, y desactiva el editor de archivos como efecto secundario. En un sitio desplegado desde Git, DISALLOW_FILE_MODS es el ajuste honesto, porque las actualizaciones llegan por la tubería de despliegue.
DISALLOW_UNFILTERED_HTML funciona igual y merece la pena en cualquier sitio editorial donde los Editores no necesiten pegar etiquetas de script.
C. Mapea meta capacidades en tipos de contenido personalizados
register_post_type() pone map_meta_cap a null por defecto, no a false. WP_Post_Type::set_props() convierte ese null en true cuando capabilities está vacío y capability_type es post o page, por compatibilidad con el comportamiento de 3.0 (ticket del core #14122), y cae a false en cualquier otro caso. Un capability_type propio como el de abajo aterriza por tanto en false salvo que lo digas. Indica ambos argumentos:
register_post_type( 'book', [
'capability_type' => [ 'book', 'books' ],
'map_meta_cap' => true,
] );La forma de array no es decoración. Con una cadena simple WordPress añade una s para construir el plural, lo que da storys para story. La referencia de register_post_type() nombra el array como la manera de pasar un plural alternativo.
capability_type => 'book' genera las meta capacidades edit_book, read_book, delete_book y las primitivas edit_books, edit_others_books, delete_books, publish_books, read_private_books. Otras seis, read, delete_private_books, delete_published_books, delete_others_books, edit_private_books y edit_published_books, solo se generan cuando map_meta_cap es true, porque solo map_meta_cap() las consume. Esa es la razón de existir del flag: sin él nunca se crean las distinciones granulares de edición y borrado, así que un usuario con edit_books puede editar los libros de cualquiera.
Otro valor por defecto a vigilar: create_posts se mapea a edit_books salvo que lo definas. Si quieres un rol que edite libros existentes pero nunca cree uno, tienes que decirlo en el array capabilities.
D. Comprueba la capacidad en el punto de decisión
La comprobación va junto a la operación, no en el menú. add_menu_page() recibe una capacidad y con ella oculta un enlace, pero no protege el manejador. Cada manejador de admin-post, cada callback AJAX y cada ruta REST necesita su propio current_user_can(), y las rutas REST lo necesitan en permission_callback y no dentro del manejador, o WordPress registra un aviso _doing_it_wrong().
Para cualquier cosa que actúe sobre un objeto concreto, pasa el objeto: current_user_can( 'edit_post', $post_id ) pasa por map_meta_cap() y resuelve la propiedad. current_user_can( 'edit_posts' ) no lo hace, y responde a una pregunta sobre el tipo de contenido en lugar de sobre esa entrada.
E. Auditoría de accesos y separación de entornos
Un registro de auditoría que siga inicios de sesión, cambios de rol y acciones sensibles te dice quién concedió qué y cuándo. Plugins como WP Activity Log cubren esa función, y también puedes implementarlo con los hooks de WordPress.
Los roles en desarrollo, staging y producción se revisan por separado. Un rol razonable en desarrollo, con permisos extra para depurar, puede ser peligroso en producción.
6. Gestión avanzada de capacidades
Capacidades de red en Multisite
En instalaciones WordPress Multisite las capacidades funcionan a dos niveles: sitio individual y red. El super administrador tiene capacidades de red que los administradores de sitios individuales no tienen, y WP_User::has_cap() atiende al super administrador antes de consultar allcaps. Entender esa distinción es crítico para configurar permisos en multisitio.
Integración con plugins populares
Plugins como WooCommerce, BuddyPress y bbPress agregan sus propias capacidades al sistema. Cuando planificas la arquitectura de roles para un sitio que usa estos plugins, debes considerar tanto las capacidades nativas de WordPress como las capacidades agregadas por los plugins.
Herramientas de depuración
Los plugins Members y User Role Editor proporcionan interfaces gráficas para gestionar roles y capacidades. Aun así, para desarrollo profesional es preferible gestionar los roles por código versionado, de modo que los cambios de permisos pasen por la misma revisión que cualquier otro cambio.
Resumen
Los roles son almacenamiento. Las capacidades son la decisión. Todo lo anterior se deriva de esa separación:
- Verifica capacidades, nunca nombres de rol:
current_user_can('administrator')se resuelve por una fusión de claves de meta de usuario y devuelve false para cada rol personalizado que crees. - Trata
add_role()yadd_cap()como migraciones contra una fila de base de datos, con comprobación de versión, no como configuración que editas y redespliegas. - Lee el alcance real de una capacidad antes de concederla.
edit_theme_optionssignifica el editor del sitio en un tema de bloques, no solo la pantalla de menús. - Recupera con
wp role reset --all, nunca con un parámetro de consulta sin autenticar, y recuerda que deja los roles personalizados intactos. - Pon
map_meta_cap => trueen cada tipo de contenido personalizado que tenga más de un autor.
Consulta también nuestros servicios de mantenimiento WordPress para mantener tu sitio seguro y optimizado.






