O WordPress traz uma lista de controlo de acesso completa e quase ninguém a usa. A falha de permissões que mais vemos em sites de clientes não é um plugin por atualizar, é um redator de conteúdos com a função de Administrador porque alguém precisou que ele reordenasse um menu uma vez. Este guia cobre o que a API de funções e capacidades faz realmente ao nível do código-fonte: onde vivem os dados, porque é que add_role() ignora em silêncio a sua segunda chamada, a que se resolve realmente uma verificação por nome de função e que capacidades são mais amplas do que o nome sugere.
Saiba mais sobre os serviços de segurança WordPress da WPPoland.
Uma única mudança faz quase todo o trabalho: deixe de nomear funções nas suas condições e nomeie a capacidade de que o código precisa. O resto deste guia é o que isso custa na prática, e é a base do trabalho de qualquer programador WordPress que leve a segurança a sério.
1. Diferença entre função e capacidade no WordPress
O sistema de permissões do WordPress assenta em dois conceitos que convém separar antes de escrever qualquer lógica de controlo de acesso.
Capacidade (cap)
Uma permissão específica para fazer uma coisa. As capacidades são as unidades atómicas do sistema. Exemplos: edit_posts, publish_pages, install_plugins, manage_options e edit_theme_options.
As capacidades podem ser primitivas (guardadas diretamente na linha das funções) ou meta capacidades (resolvidas em tempo de execução por map_meta_cap() consoante o contexto). Por exemplo, edit_post é uma meta capacidade que se resolve em edit_posts ou edit_others_posts consoante o utilizador seja ou não o autor do artigo.
Função (role)
Uma coleção de capacidades agrupadas sob um nome. O WordPress guarda o conjunto e avalia a cadeia.
Os conjuntos predefinidos são mais pequenos do que se assume. Segundo a documentação de funções e capacidades, o Editor tem 26 capacidades e o Autor sete: read, upload_files, edit_posts, edit_published_posts, publish_posts, delete_posts, delete_published_posts. O Colaborador tem três: read, edit_posts, delete_posts. Um Colaborador não consegue carregar um ficheiro, e é por isso que “basta torná-lo Colaborador” acaba num pedido de suporte sobre a biblioteca multimédia.
O Editor é o caso interessante. Numa instalação de site único tem unfiltered_html, ou seja, um Editor pode guardar uma etiqueta <script> no conteúdo. Em multisite essa capacidade é negada a todos exceto aos super administradores, como mostra 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';
}O que o Editor não tem: edit_theme_options, list_users, edit_users, manage_options nem nada relacionado com plugins.
Verificar capacidades em vez de funções
Verifique capacidades, nunca nomes de funções.
// ERRADO - nunca faça isto
if ( current_user_can( 'administrator' ) ) { ... }
// CERTO - verifique sempre capacidades
if ( current_user_can( 'manage_options' ) ) { ... }A primeira linha não é um erro de sintaxe, é algo pior: devolve a resposta certa exatamente no caso que testou. O motivo é uma fusão no fim 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 é o meta de utilizador wp_capabilities em bruto, e as suas chaves são nomes de funções. Por isso administrator => true aterra em allcaps ao lado de capacidades reais e passa a verificação por acidente. A documentação de current_user_can() diz isto mesmo: verificar funções concretas em vez de uma capacidade é suportado apenas em parte e desaconselhado, porque pode produzir resultados pouco fiáveis.
Três formas de isto correr mal, todas vistas em sites reais:
- Cria uma função personalizada com
manage_optionspara que um cliente chegue à sua página de definições.current_user_can('administrator')éfalsepara ele, e a sua própria página devolve 403 ao utilizador para quem foi feita. - Uma capacidade concedida diretamente no objeto utilizador, sem função associada, passa qualquer verificação de capacidade e falha qualquer verificação de função.
- Em multisite,
WP_User::has_cap()faz curto-circuito para super administradores antes de olhar paraallcapse devolvetruepara tudo o que não sejado_not_allow. Aí uma verificação de função não distingue nada.
2. Como criar uma função personalizada no WordPress
Antes de escrever uma, verifique se a plataforma já a traz. O WooCommerce acrescenta shop_manager e customer ao ser instalado e, segundo a sua documentação de funções, o Gestor de loja já pode gerir produtos, encomendas, cupões e contas de clientes. Construir um “gestor de loja” paralelo duplica uma função que outra equipa mantém ao ritmo das suas versões.
Quando precisa mesmo de uma, esta é a forma:
add_role(
'editorial_lead',
'Responsável 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,
]
);Há duas coisas nesta chamada que não se deduzem da assinatura.
Escreve uma vez e depois ignora-o. O manual de plugins indica que, após a primeira chamada, a função e as suas capacidades ficam guardadas na base de dados e que as chamadas seguintes não fazem nada, incluindo alterar a lista de capacidades. Edita o array, faz o deploy e nada muda. Cada alteração de capacidades precisa, por isso, de uma migração 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', 'Responsável 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' );A cópia em admin_init importa em multisite e em deploys que nunca disparam um hook de ativação. A verificação de versão é o que mantém o código fora do caminho crítico, e o manual avisa que remover e voltar a criar a função sem condição degrada consideravelmente o desempenho.
remove_role() não toca nos utilizadores. Remove a chave da linha de opções. Cada utilizador cujo meta wp_capabilities continue a dizer editorial_lead => true mantém essa chave, WP_Roles::is_role() já devolve false para ela e, por isso, nunca se resolve em capacidades. O utilizador fica com uma cadeia que não concede nada. Reatribua os utilizadores antes de remover uma função, ou imediatamente depois.
Onde está realmente a linha. WP_Roles::for_site() constrói a chave como $wpdb->get_blog_prefix( $this->site_id ) . 'user_roles'. Num site único com o prefixo predefinido isso é wp_user_roles. No subsite 3 de uma rede é wp_3_user_roles, e é por isso que uma função acrescentada num subsite é invisível no seguinte. Se quiser as funções definidas em código e nunca escritas na base de dados, defina a global $wp_user_roles em wp-config.php: o WordPress passa a usá-la e, segundo a referência do core, a opção de funções deixa de ser atualizada e utilizada.
Etiquetas de funções em sites multilingues
O segundo argumento de add_role() é a etiqueta visível, e WP_Roles::add_role() guarda-a na linha de opções tal como chega. Se a envolver em __(), o que fica guardado é a cadeia no idioma ativo no momento da ativação, e não no idioma de quem abre depois o ecrã de utilizadores. Ao mostrá-la, wp_dropdown_roles() passa a etiqueta por translate_user_role(), que traduz com o contexto User role e o domínio de texto default. Isso cobre as funções do core, cujas cadeias estão no catálogo do WordPress, e não cobre a etiqueta do seu plugin. Num site multilingue guarde o slug e a etiqueta em inglês, e não construa lógica sobre a etiqueta: o slug é o identificador estável.
3. Como adicionar capacidades a uma função existente
get_role() mais add_cap() é a mudança mais pequena quando uma função predefinida quase serve:
$role = get_role( 'editor' );
if ( $role ) {
$role->add_cap( 'edit_theme_options' );
}Aplique a mesma verificação de versão da secção 2. WP_Role::add_cap() aceita exatamente dois parâmetros, add_cap( $cap, $grant = true ), e o segundo decide se a capacidade é concedida ou negada explicitamente, e não se a linha é escrita. Não há aqui um terceiro parâmetro. A forma com três argumentos pertence a WP_Roles::add_cap( $role, $cap, $grant ), outra classe, e é o método para o qual WP_Role::add_cap() delega. Esse método atribui a capacidade e chama update_option() sempre que use_db é true, em cada chamada. Nenhum argumento suprime a escrita, por isso executar add_cap() em init sem guarda significa uma tentativa de escrita de opção em cada pedido.
Alcance de edit_theme_options em temas de blocos
O problema maior desta linha concreta é o alcance. edit_theme_options é a solução clássica para “o Editor gerir os menus” e, num tema clássico, faz mais ou menos isso: Aparência para widgets, menus, personalizador e cabeçalho. Num tema de blocos é outra capacidade. A documentação do core apresenta-a agora como a capacidade principal que o WordPress verifica para decidir se um utilizador pode aceder aos modelos e geri-los a partir do editor do site, e avisa que não se limita exclusivamente ao editor do site e pode dar acesso a outras funcionalidades administrativas relacionadas com o tema.
Ou seja, o Editor a quem queria deixar reordenar um menu tem agora o editor do site: modelos, partes de modelo, estilos globais e navegação. É uma concessão mais ampla do que switch_themes, que só expõe Aparência e Aparência > Temas.
Se o que quer é realmente uma função para o editor do site, a documentação lista o que é necessário: edit_theme_options para o acesso e o menu de navegação, mais edit_posts, edit_pages e edit_others_posts para gerir modelos, read para pré-visualizar e upload_files para multimédia.
O modo de falha aqui é silencioso, porque o editor do site fala com a API REST. Os endpoints de modelos e partes de modelo verificam edit_theme_options; os de artigos e páginas fazem as suas próprias verificações. Uma função que consegue abrir o editor do site mas não tem edit_others_posts recebe um painel que carrega e depois falha ao guardar. Leia o separador de rede à procura de respostas 401 e 403 antes de acrescentar capacidades às cegas: o código de erro REST nomeia a permissão que falhou.
Capacidades condicionais com user_has_cap
Algumas regras não cabem numa função porque dependem do objeto: um Editor que não deve mexer nos artigos de uma categoria concreta, por exemplo. É para isso que existe o filtro user_has_cap. WP_User::has_cap() aplica-o depois de map_meta_cap(), com quatro argumentos: $allcaps (o que o utilizador tem), $caps (as primitivas que a verificação exige), $args (a capacidade pedida em $args[0], o ID do utilizador em $args[1] e, normalmente, o ID do objeto em $args[2]) e o objeto WP_User.
add_filter( 'user_has_cap', function ( $allcaps, $caps, $args, $user ) {
if ( 'edit_post' !== ( $args[0] ?? '' ) || empty( $args[2] ) ) {
return $allcaps;
}
if ( has_term( 'legal', 'category', $args[2] ) && empty( $allcaps['manage_options'] ) ) {
foreach ( $caps as $cap ) {
$allcaps[ $cap ] = false;
}
}
return $allcaps;
}, 10, 4 );Três detalhes do código-fonte decidem se isto funciona. O filtro só vê verificações que levam o ID do objeto, por isso current_user_can( 'edit_posts' ) passa por ele sem que a regra atue. Corre em cada verificação de capacidades, de modo que a saída antecipada da primeira linha não é opcional. E em multisite não chega aos super administradores: has_cap() devolve-lhes true antes de aplicar o filtro, como se vê na secção 6.
4. Como repor as funções do WordPress
A versão deste script que circula é uma escalada de privilégios remota:
// Não implemente isto em produção.
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() verifica que o parâmetro está presente, não que está correto. Qualquer visitante não autenticado que acrescente ?reset_roles_secret_key a qualquer URL do site restaura as capacidades predefinidas, o que inclui devolver tudo o que tivesse retirado de propósito ao Editor ou ao Autor.
Como repor funções com wp role reset
O wp role reset corre sobre a mesma função do core e está autenticado pelo acesso à shell:
wp role reset --all
wp role reset editorFaz também mais do que populate_roles() por si só, e a diferença importa ao depurar. populate_roles() chama populate_roles_160() até populate_roles_300(), e essas funções só chamam add_role() e add_cap(). Não há nelas nenhum remove_cap(). Chamada diretamente, restaura as capacidades removidas e deixa todas as que um plugin tenha acrescentado. O WP-CLI compara ainda com uma instalação limpa, e é por isso que a sua saída se lê assim:
Restored 1 capability to and removed 0 capabilities from 'administrator' role.As funções personalizadas ficam intactas por qualquer uma das duas vias. Se o problema do site é uma função personalizada mal feita, uma reposição não o resolve.
Como inspecionar as funções com WP-CLI antes de as repor
O estado que está a depurar é uma linha de opções:
wp option get wp_user_roles --format=json | jq '.editor.capabilities'
wp cap list editor
wp user list --field=user_login --role=administratorO último é o comando que vale a pena correr em qualquer site que herde. Um número de administradores que o surpreenda é a descoberta.
5. Como proteger as funções do WordPress
A. Como rever as contas de administrador no WordPress
O privilégio mínimo não é uma função nova, é uma lista de administradores mais curta. Execute wp user list --role=administrator nos sites que mantém. Contas de agência, um freelancer que saiu e o acesso de suporte de um plugin de há dois anos são os residentes habituais.
Quanto ao conselho clássico de não chamar ‘admin’ ao utilizador: o instalador pede o nome de utilizador desde o WordPress 3.0, por isso a lacuna que resta é a enumeração. O endpoint REST de utilizadores devolve id, name, url, description, link, slug e avatar_urls sem autenticação. roles e capabilities estão no contexto edit e não são expostos, mas slug é o user_nicename, que o WordPress deriva do login a não ser que alguém o altere. Isso é uma lista de nomes de utilizador, não uma brecha, e limitar a taxa de pedidos do formulário de login é o controlo que mais pesa.
B. Como desativar a edição de ficheiros com DISALLOW_FILE_EDIT
Retirar edit_themes ao Administrador é uma alteração de capacidade que alguém pode desfazer. Uma constante em wp-config.php é verificada dentro de map_meta_cap() e não pode ser sobreposta por uma concessão:
define( 'DISALLOW_FILE_EDIT', true );DISALLOW_FILE_EDIT mapeia edit_files, edit_plugins e edit_themes para do_not_allow. DISALLOW_FILE_MODS vai mais longe e bloqueia a instalação e atualização de plugins e temas a partir do painel, e desativa o editor de ficheiros como efeito secundário. Num site implementado a partir do Git, DISALLOW_FILE_MODS é a definição honesta, porque as atualizações chegam pelo pipeline de deploy.
DISALLOW_UNFILTERED_HTML funciona da mesma forma e vale a pena em qualquer site editorial onde os Editores não precisem de colar etiquetas de script.
C. Como mapear meta capacidades em tipos de conteúdo personalizados
register_post_type() define map_meta_cap como null por omissão, não como false. WP_Post_Type::set_props() converte esse null em true quando capabilities está vazio e capability_type é post ou page, por compatibilidade com o comportamento da versão 3.0 (ticket do core #14122), e passa a false em qualquer outro caso. Um capability_type próprio como o de baixo fica, por isso, em false, a não ser que o indique. Defina ambos os argumentos:
register_post_type( 'book', [
'capability_type' => [ 'book', 'books' ],
'map_meta_cap' => true,
] );A forma de array não é decoração. Com uma cadeia simples, o WordPress acrescenta um s para construir o plural, o que dá storys para story. A referência de register_post_type() indica o array como a forma de passar um plural alternativo.
capability_type => 'book' gera as meta capacidades edit_book, read_book, delete_book e as primitivas edit_books, edit_others_books, delete_books, publish_books, read_private_books. Outras seis, read, delete_private_books, delete_published_books, delete_others_books, edit_private_books e edit_published_books, só são geradas quando map_meta_cap é true, porque só map_meta_cap() as consome. É essa a razão de ser da flag. Sem ela, map_meta_cap() resolve a meta capacidade edit_book na própria cadeia edit_book, sem olhar para quem é o autor nem para se o livro está publicado, por isso quem tiver essa capacidade edita os livros de qualquer pessoa.
Outro valor predefinido a vigiar: create_posts é mapeado para edit_books, a não ser que o defina. Se quiser uma função que edite livros existentes mas nunca crie nenhum, tem de o indicar no array capabilities.
D. Onde verificar current_user_can em handlers e rotas REST
A verificação fica junto à operação, não no menu. add_menu_page() recebe uma capacidade e com ela esconde uma ligação, mas não protege o handler. Cada handler de admin-post, cada callback AJAX e cada rota REST precisa do seu próprio current_user_can(), e as rotas REST precisam dele em permission_callback e não dentro do handler. Desde o WordPress 5.5, register_rest_route() regista um aviso _doing_it_wrong() quando falta permission_callback, e para uma rota verdadeiramente pública o valor documentado é __return_true.
Para tudo o que atue sobre um objeto concreto, passe o objeto: current_user_can( 'edit_post', $post_id ) passa por map_meta_cap() e resolve a autoria. current_user_can( 'edit_posts' ) não o faz, e responde a uma pergunta sobre o tipo de conteúdo em vez de sobre esse artigo.
E. Auditoria de acessos e separação de ambientes
Um registo de auditoria que acompanhe inícios de sessão, alterações de funções e ações sensíveis diz-lhe quem concedeu o quê e quando. Plugins como o WP Activity Log cobrem essa função, e também a pode implementar com os hooks do WordPress.
As funções em desenvolvimento, staging e produção reveem-se em separado. Uma função razoável em desenvolvimento, com permissões extra para depurar, pode ser perigosa em produção.
6. Capacidades do WordPress em multisite e plugins
Capacidades de rede em Multisite
Em instalações WordPress Multisite as capacidades funcionam a dois níveis: site individual e rede. Segundo a documentação de funções e capacidades, estas dez existem apenas para o super administrador: create_sites, delete_sites, manage_network, manage_sites, manage_network_users, manage_network_plugins, manage_network_themes, manage_network_options, upgrade_network e setup_network.
A parte que surpreende vai no sentido contrário. O Administrador de um subsite perde capacidades que tem num site único. A mesma documentação lista update_core, update_plugins, update_themes, install_plugins, install_themes, delete_themes, delete_plugins, edit_plugins, edit_themes, edit_files, edit_users, add_users, create_users, delete_users e unfiltered_html como capacidades que em multisite só o super administrador tem. Não são retiradas da linha da função: map_meta_cap() converte-as em do_not_allow com a condição is_multisite() && ! is_super_admin( $user_id ), a mesma que viu na secção 1 para unfiltered_html. É por isso que acrescentar install_plugins à função Administrador de um subsite não muda nada.
WP_User::has_cap() trata o super administrador antes de consultar allcaps e antes de aplicar o filtro user_has_cap: se o resultado de map_meta_cap() não contiver do_not_allow, devolve true. Para retirar algo a um super administrador é preciso filtrar map_meta_cap e acrescentar do_not_allow, e não user_has_cap.
Para verificar uma permissão noutro site da rede, o WordPress 6.7 introduziu current_user_can_for_site( $site_id, $capability ), que substitui current_user_can_for_blog(), agora em deprecated.php. O manual de plugins ainda mostra o nome antigo.
Capacidades do WooCommerce, BuddyPress e bbPress
Plugins como o WooCommerce, o BuddyPress e o bbPress acrescentam as suas próprias capacidades ao sistema. Ao planear a arquitetura de funções para um site que usa estes plugins, tem de considerar tanto as capacidades nativas do WordPress como as acrescentadas pelos plugins.
Members e User Role Editor para gerir funções
Os plugins Members e User Role Editor oferecem interfaces gráficas para gerir funções e capacidades. Ainda assim, em desenvolvimento profissional é preferível gerir as funções através de código versionado, para que as alterações de permissões passem pela mesma revisão que qualquer outra alteração.
Resumo
As funções são armazenamento. As capacidades são a decisão. Tudo o que está acima decorre dessa separação:
- Verifique capacidades, nunca nomes de funções:
current_user_can('administrator')resolve-se por uma fusão de chaves do meta de utilizador e devolve false para cada função personalizada que criar. - Trate
add_role()eadd_cap()como migrações sobre uma linha da base de dados, com verificação de versão, e não como configuração que edita e volta a implementar. - Leia o alcance real de uma capacidade antes de a conceder.
edit_theme_optionssignifica o editor do site num tema de blocos, não só o ecrã de menus. - Recupere com
wp role reset --all, nunca com um parâmetro de consulta não autenticado, e lembre-se de que deixa as funções personalizadas intactas. - Defina
map_meta_cap => trueem cada tipo de conteúdo personalizado que tenha mais de um autor.
Veja também os nossos serviços de manutenção WordPress para manter o seu site seguro e otimizado.







