Sanitização e validação de dados em metaboxes personalizados: guia completo

Aprenda a proteger metaboxes no WordPress com sanitização, validação e nonces. Evite falhas de segurança e garanta integridade dos dados salvos no banco.

Imagine que você acabou de entregar um site para um cliente. Dias depois, ele relata que campos personalizados do tipo “URL do portfólio” estão salvando scripts maliciosos ou simplesmente apagando o conteúdo ao editar o post. O culpado? Um metabox que aceita qualquer coisa vinda do $_POST sem checagem. Esse cenário é mais comum do que parece e expõe o site a XSS persistente, corrupção de dados e falhas de integridade que só aparecem em produção.

A sanitização e validação não são detalhes de implementação — são a barreira entre um CMS confiável e uma brecha explorável. No WordPress, a responsabilidade é do desenvolvedor: o core não sanitiza automaticamente o que vem de metaboxes personalizados. Cada campo exige tratamento explícito, e a ordem importa: primeiro autenticação (nonce + permissões), depois validação de regras de negócio, por fim sanitização contextual antes de gravar no banco.

Neste guia, você verá o fluxo completo desde a renderização do campo até o gancho de salvamento, com código pronto para adaptar. Cobriremos nonces, capacidades, autosaves, revisões, arrays de campos repetíveis e feedback de erro ao usuário. No final, terá um checklist para revisar seus metaboxes atuais e evitar surpresas.

O fluxo de segurança: do formulário ao banco de dados

Todo metabox seguro passa por três portões obrigatórios antes de tocar update_post_meta(). O primeiro é a autenticação da requisição: confirmar que o pedido veio do painel admin legítimo e não de um formulário externo forjado. O segundo é a autorização: garantir que o usuário logado tem direito de editar aquele post específico. O terceiro é a integridade do dado: validar regras de negócio e sanitizar conforme o contexto de saída (HTML, URL, JS, SQL).

Pular qualquer um desses portões abre vetores de ataque. Um nonce válido sem checagem de current_user_can() permite que um editor malicioso edite posts de administradores via CSRF. Sanitização sem validação prévia aceita “javascript:alert(1)” como URL válida. Validação sem sanitização deixa passar HTML quebrado que quebra o layout do admin. A ordem correta é inegociável.

No WordPress, o gancho padrão para salvamento é save_post (ou save_post_{post_type}). É nele que concentrarmos toda a lógica de defesa. O callback do metabox (add_meta_box()) só deve renderizar HTML e o campo wp_nonce_field(). Separar renderização de processamento mantém o código testável e evita duplicação de lógica.

Nonces: identificando a origem da requisição

O wp_nonce_field() gera um token único atrelado à ação, ao usuário e a uma janela de tempo (12-24h). No metabox, use um nome de ação específico: wp_nonce_field('salvar_metabox_portfolio', 'portfolio_nonce'). No save_post, recupere com isset($_POST['portfolio_nonce']) e valide com wp_verify_nonce($_POST['portfolio_nonce'], 'salvar_metabox_portfolio'). Se falhar, retorne cedo com return $post_id;.

Erros comuns: usar o mesmo nonce para múltiplos metaboxes (um falho invalida todos), omitir o parâmetro action (torna o nonce genérico e fraco), ou verificar o nonce fora do save_post (ex: no admin_init), onde o $post_id ainda não está disponível de forma confiável. Um nonce por metabox, ação nomeada, verificação no local correto.

Para metaboxes em tipos de post customizados, o sufixo do hook ajuda a organizar: add_action('save_post_meu_cpt', 'meu_cpt_salvar_metabox'). Isso evita rodar sua lógica em posts, páginas ou outros CPTs desnecessariamente, economizando ciclos de CPU e reduzindo superfície de erro.

Permissões, autosaves e revisões: os filtros invisíveis

Antes de processar qualquer $_POST, aplique três guardas que evitam processamento indevido e perda de dados:

if ( wp_is_post_autosave($post_id) || wp_is_post_revision($post_id) ) {
    return $post_id;
}
if ( ! current_user_can('edit_post', $post_id) ) {
    return $post_id;
}

wp_is_post_autosave() protege contra salvamento automático do editor (Gutenberg ou Classic) que dispara save_post sem intenção do usuário. wp_is_post_revision() evita duplicar meta ao criar revisões. current_user_can('edit_post', $post_id) é a checagem contextual de permissão — mais precisa que edit_posts genérica, pois respeita locks de post, status privado e capacidades filtradas por plugins como Members ou PublishPress Capabilities.

Esses três ifs devem vir antes da verificação de nonce. Por quê? Porque um usuário sem permissão não deve nem gastar CPU verificando criptografia. Ordem: autosave/revision → permissão → nonce → dados. Essa sequência também evita falsos positivos em logs de auditoria.

Sanitização contextual: a função certa para cada tipo

O WordPress oferece dezenas de funções de sanitização. Escolher a errada é tão perigoso quanto não sanitizar. A tabela abaixo resume as mais usadas em metaboxes:

Tipo de campo Função recomendada Observação
Input text (simples) sanitize_text_field() Remove tags, entidades, espaços extras
Textarea (sem HTML) sanitize_textarea_field() Preserva quebras de linha, remove tags
URL esc_url_raw() Para armazenamento; use esc_url() na saída
E-mail sanitize_email() Remove caracteres inválidos, normaliza
Inteiro (ID, quantidade) absint() ou intval() absint() garante >= 0
Float (preço, nota) floatval() + validação de range Cuidado com separador decimal
Slug / chave sanitize_title() ou sanitize_key() sanitize_key() para chaves alfanuméricas
HTML controlado wp_kses($input, $allowed_tags) Defina array de tags/atributos permitidos

Exemplo prático para um campo “Link do projeto”:

$url_bruta = isset($_POST['portfolio_url']) ? $_POST['portfolio_url'] : '';
$url_limpa = esc_url_raw($url_bruta);
if ( $url_limpa !== $url_bruta ) {
    // Opcional: registrar que a entrada foi alterada
    error_log('Portfolio URL sanitizada: ' . $url_bruta . ' -> ' . $url_limpa);
}
update_post_meta($post_id, '_portfolio_url', $url_limpa);

Note que esc_url_raw() não valida se a URL existe ou é acessível — só garante que é uma string segura para o banco. A validação de “URL acessível” é regra de negócio, não sanitização.

Validação de regras de negócio: além da formatação

Sanitização garante formato; validação garante sentido. Exemplos de validação pós-sanitização:

  1. Campo obrigatório: if (empty($url_limpa)) { /* erro */ }
  2. Domínio permitido: if (strpos($url_limpa, 'meudominio.com') === false) { /* erro */ }
  3. Range numérico: if ($nota < 0 || $nota > 10) { /* erro */ }
  4. Formato de data: DateTime::createFromFormat('Y-m-d', $data) !== false
  5. Unicidade: consultar get_posts() com meta_query para evitar duplicatas

Quando a validação falha, não salve o meta e informe o usuário. O WordPress não tem API nativa de “flash messages” no save_post, mas você pode usar set_transient('portfolio_error_' . $post_id, $msg, 30) e ler no callback do metabox com get_transient(), exibindo um <div class="notice notice-error">. Alternativamente, add_settings_error() funciona se você registrar a seção via Settings API, mas em metaboxes puros o transient é mais simples.

Evite wp_die() ou wp_redirect() no save_post: eles quebram o fluxo de salvamento do editor (especialmente no Gutenberg, que espera resposta JSON da REST API). Falhe silenciosamente no banco, mas avise na interface.

Campos repetíveis (repeaters) e arrays complexos

Muitos metaboxes modernos usam campos repetíveis (ex: lista de integrantes, galeria de URLs). O $_POST chega como array multidimensional. A sanitização deve iterar com segurança:

$membros_brutos = isset($_POST['equipe_membros']) && is_array($_POST['equipe_membros']) 
    ? $_POST['equipe_membros'] 
    : [];

$membros_limpos = [];
foreach ($membros_brutos as $membro) {
    if ( ! is_array($membro) ) continue;
    $nome  = isset($membro['nome'])  ? sanitize_text_field($membro['nome'])  : '';
    $funcao = isset($membro['funcao']) ? sanitize_text_field($membro['funcao']) : '';
    if ( $nome !== '' ) {
        $membros_limpos[] = compact('nome', 'funcao');
    }
}
// Limite de segurança
$membros_limpos = array_slice($membros_limpos, 0, 50);
update_post_meta($post_id, '_equipe_membros', $membros_limpos);

Pontos-chave: valide is_array() antes de iterar, sanitize cada sub-campo individualmente, descarte itens vazios ou malformados, e limite a quantidade (DoS via array gigante é real). Nunca faça update_post_meta($post_id, 'chave', $_POST['array']) direto.

Para metaboxes complexos, considere mover a lógica de sanitização para uma class dedicada (ex: Portfolio_Metabox_Sanitizer) com métodos por tipo de campo. Isso facilita testes unitários e reuso entre CPTs. Veja como estruturar plugins profissionais em Gerenciamento de dependências Composer em plugins profissionais.

Feedback ao usuário e debug em produção

Um metabox silencioso que apaga dados inválidos sem avisar gera tickets de suporte “meus dados sumiram”. Implemente dois níveis de feedback:

  1. Admin notice via transient (como descrito acima) para erros de validação esperados (URL inválida, campo obrigatório).
  2. Error log estruturado para anomalias: error_log(json_encode(['metabox' => 'portfolio', 'post_id' => $post_id, 'input' => $_POST['portfolio_url'], 'sanitized' => $url_limpa, 'user' => get_current_user_id()]));

Em produção, monitore logs com ferramentas como Query Monitor ou enviando para Sentry / Loggly. Padrões suspeitos (muitas sanitizações alterando entrada, nonces falhando, permissões negadas) indicam ataques ou bugs de frontend.

Para depurar em desenvolvimento, adicione temporariamente no save_post:

if ( defined('WP_DEBUG') && WP_DEBUG ) {
    error_log('SAVE_POST Portfolio: ' . print_r($_POST, true));
}

Remova antes do deploy. Nunca logue nonces ou senhas.

Boas práticas avançadas e armadilhas comuns

  • Prefira update_post_meta() a add_post_meta(): update cria se não existe, evita duplicatas.
  • Prefixe chaves de meta com _ (underscore) para ocultar da UI de campos personalizados nativa e evitar conflitos.
  • Não use sanitize_text_field() em campos que aceitam HTML (ex: biografia rica). Use wp_kses() com lista branca restrita.
  • Campos do tipo select, radio, checkbox: valide contra a lista de valores permitidos (in_array($valor, $opcoes_validas, true)). Não confie no value enviado pelo browser.
  • Gutenberg: se o metabox aparece no painel lateral do editor de blocos, o fluxo save_post continua valendo. Mas para blocos nativos, use register_meta() com sanitize_callback e auth_callback — mais declarativo e integrado à REST API. Aprenda a criar blocos dinâmicos em Como criar blocos Gutenberg dinâmicos com React.
  • Performance: sanitização é barata. O gargalo costuma ser validações que fazem queries (meta_query para unicidade). Cacheie resultados ou use índices de meta customizados via register_meta() com 'show_in_rest' => true.

A hierarquia de templates também influencia onde seus metaboxes aparecem. Entenda o contexto em Hierarquia de templates para temas block-based modernos.

Conclusão prática

Sanitização e validação em metaboxes não são opcionais — são o contrato de confiança entre seu código e o banco de dados. Implemente o trio autenticação → autorização → integridade em todo save_post, use a função de sanitização específica para cada tipo de dado, valide regras de negócio após limpar a entrada, e sempre devolva feedback claro ao editor. Revise seus metaboxes existentes com o checklist acima e durma tranquilo sabendo que “script>alert(1)</script” nunca vai virar meta_value no seu site.

Para aprofundar em performance de assets que carregam no admin, veja Eliminando recursos bloqueantes de renderização no WordPress. E se você integra metaboxes com a REST API para apps headless, leia Autenticação via Application Passwords na REST API.

Checklist prático

0 de 9 concluídos

Perguntas frequentes

Qual a diferença entre sanitização e validação em metaboxes?

Sanitização limpa e formata o dado para torná-lo seguro (ex: remover tags HTML), enquanto validação verifica se o dado atende a regras de negócio (ex: e-mail válido, número positivo). Ambas devem ser usadas juntas: valide a intenção, sanitize a entrada.

Onde devo colocar a verificação de nonce: no callback do metabox ou no hook de salvamento?

A verificação de nonce deve ocorrer no hook de salvamento (`save_post`), antes de qualquer processamento dos dados. O callback do metabox apenas renderiza o campo oculto com `wp_nonce_field()`. Separar renderização de verificação evita processamento de requisições forjadas.

Posso usar `sanitize_text_field()` para todos os tipos de campo?

Não. `sanitize_text_field()` remove tags e entidades, ideal para inputs simples. Para URLs use `esc_url_raw()`, para e-mails `sanitize_email()`, para inteiros `absint()` ou `intval()`, e para áreas de texto sem HTML `sanitize_textarea_field()`. Escolha a função específica ao tipo de dado esperado.

Como lidar com campos repetíveis (repeaters) na sanitização?

Itere sobre o array `$_POST['meu_campo']` com `array_map()` aplicando a função de sanitização a cada item. Valide a estrutura do array antes (ex: `is_array()`), limite a quantidade de itens permitidos e descarte chaves inesperadas. Nunca confie na estrutura enviada pelo navegador.

É necessário verificar `current_user_can()` no salvamento do metabox?

Sim. Mesmo com nonce válido, verifique `current_user_can('edit_post', $post_id)` para garantir que o usuário tem permissão de editar aquele post específico. Isso previne escalação de privilégios se um nonce for vazado ou reutilizado indevidamente.

Como debugar falhas de sanitização que apagam dados legítimos?

Registre o `$_POST` bruto e o resultado pós-sanitização no log de erro com `error_log(print_r($dados, true))`. Compare a entrada e saída para identificar qual função removeu caracteres esperados. Use `wp_die()` temporariamente em ambiente de desenvolvimento para inspecionar o fluxo.