DesenvolvedorWordPress WhatsApp

Segurança no código WordPress: sanitização, escaping e nonces

A maioria das vulnerabilidades em plugins e temas não vem do núcleo do WordPress — vem de código que confia em dados sem tratá-los. Este guia mostra, com exemplos, as quatro defesas essenciais: sanitizar a entrada, escapar a saída, validar com nonces e checar permissões.

Precisa de uma revisão de segurança no seu código?

Atualizado em 19/06/2026

Programar para WordPress com segurança não exige um arsenal de bibliotecas externas — exige disciplina com um punhado de funções do próprio núcleo e um princípio que resume quase tudo: "sanitize early, escape late". Sanitize a entrada assim que ela chega; escape a saída no momento exato em que você imprime. Some a isso nonces contra CSRF, capabilities antes de ações sensíveis e $wpdb->prepare() em qualquer SQL, e você cobre as vulnerabilidades mais comuns (XSS, CSRF, SQL Injection, escalonamento de privilégio).

O princípio: sanitize early, escape late

Os dois verbos não são sinônimos e não se substituem:

Por que escapar mesmo o que já foi sanitizado? Porque o dado pode ter sido gravado por uma versão antiga do seu plugin, por outro plugin, ou em um contexto diferente do de saída. Escapar na saída é a última linha de defesa — barata e infalível quando se usa a função certa.

1. Sanitizando a entrada

Nunca confie em $_POST, $_GET ou $_REQUEST. Para cada campo, escolha a função de sanitização adequada ao tipo de dado esperado:

<?php
// Tratando os campos de um formulário recebido via $_POST.
// Use o operador ?? para evitar avisos de índice indefinido.
$nome      = isset( $_POST['nome'] )    ? sanitize_text_field( wp_unslash( $_POST['nome'] ) )  : '';
$email     = isset( $_POST['email'] )   ? sanitize_email( wp_unslash( $_POST['email'] ) )      : '';
$user_id   = isset( $_POST['user_id'] ) ? absint( $_POST['user_id'] )                          : 0;
$cor_meta  = isset( $_POST['cor'] )     ? sanitize_hex_color( wp_unslash( $_POST['cor'] ) )          : '';
$descricao = isset( $_POST['bio'] )     ? wp_kses_post( wp_unslash( $_POST['bio'] ) )          : '';

if ( ! is_email( $email ) ) {
    wp_die( esc_html__( 'E-mail inválido.', 'meu-plugin' ) );
}

Detalhes que importam:

2. Escapando a saída por contexto

Aqui está o ponto que mais gera XSS: imprimir um valor cru. A função de escaping depende do contexto onde o dado será inserido:

<?php
$titulo  = get_post_meta( $post_id, 'titulo', true );
$cor     = get_post_meta( $post_id, 'cor', true );
$site    = get_post_meta( $post_id, 'site', true );
$html    = get_post_meta( $post_id, 'bio_html', true );
?>
<!-- Conteúdo de texto dentro de HTML -->
<h2><?php echo esc_html( $titulo ); ?></h2>

<!-- Dentro de um atributo -->
<div style="color: <?php echo esc_attr( $cor ); ?>">...</div>

<!-- Em um href/src (URL) -->
<a href="<?php echo esc_url( $site ); ?>">Visitar</a>

<!-- HTML controlado que pode ter marcação legítima -->
<div class="bio"><?php echo wp_kses_post( $html ); ?></div>

Regra de ouro: escape o mais tarde possível, idealmente no próprio echo. Para strings traduzíveis, existem variantes que escapam e traduzem de uma vez: esc_html__(), esc_attr__(), esc_html_e(). Não imprima o retorno de __() sem escapar.

Contexto de saídaFunção correta
Texto dentro de HTMLesc_html()
Valor de atributo HTMLesc_attr()
URL em href/srcesc_url()
URL para salvar/redirecionar (banco, header)esc_url_raw()
Conteúdo de <textarea>esc_textarea()
Valor dentro de bloco <script> (JS)esc_js() ou wp_json_encode()
HTML controlado (post, comentário)wp_kses_post() / wp_kses()
String traduzível em HTMLesc_html__() / esc_html_e()

3. Nonces: barrando CSRF

Um nonce prova que a requisição saiu da sua própria tela, e não de um site malicioso que enganou o usuário logado (ataque CSRF). Gere o campo no formulário e verifique no handler:

<?php
// No formulário (admin ou front-end): adiciona um campo escondido com o nonce.
// Args: ( $action, $name ). Use uma "action" descritiva e única.
?>
<form method="post" action="">
    <?php wp_nonce_field( 'salvar_perfil_meu_plugin', 'meu_plugin_nonce' ); ?>
    <input type="text" name="nome">
    <button type="submit">Salvar</button>
</form>

<?php
// No handler que processa o $_POST:
function meu_plugin_processar() {

    // 1) Confere o nonce (CSRF). check_admin_referer encerra com erro se falhar.
    check_admin_referer( 'salvar_perfil_meu_plugin', 'meu_plugin_nonce' );

    // 2) Confere a permissão (ver seção 4).
    if ( ! current_user_can( 'edit_posts' ) ) {
        wp_die( esc_html__( 'Sem permissão.', 'meu-plugin' ) );
    }

    // 3) Só agora sanitiza e usa os dados.
    $nome = sanitize_text_field( wp_unslash( $_POST['nome'] ?? '' ) );
    // ... salvar com segurança ...
}

Para requisições AJAX/REST, em vez de check_admin_referer() você costuma usar wp_create_nonce() ao gerar o token e wp_verify_nonce() (ou check_ajax_referer()) para validá-lo. A regra é a mesma: verifique o nonce antes de qualquer efeito colateral.

O nonce não autoriza ninguém — ele só prova a origem da requisição. Por isso ele nunca dispensa o current_user_can(). Tratar nonce como controle de acesso é um erro clássico que leva a escalonamento de privilégio.

4. Capabilities: quem pode fazer o quê

Antes de qualquer ação sensível (salvar, excluir, alterar opções), verifique a capability do usuário com current_user_can(). Prefira sempre a capability ao papel (role): cheque 'manage_options', não 'administrator'.

<?php
// Errado: confiar só no fato de "estar logado" ou no papel.
// Certo: checar a capability específica para a ação.
if ( ! current_user_can( 'manage_options' ) ) {
    wp_die( esc_html__( 'Acesso negado.', 'meu-plugin' ), '', array( 'response' => 403 ) );
}

// Para ações sobre um objeto específico, passe o ID — assim o WordPress
// avalia permissões por post (ex.: o usuário pode editar ESTE post?).
if ( ! current_user_can( 'edit_post', $post_id ) ) {
    return;
}

Combine sempre as três camadas em ações de escrita: nonce (a requisição é legítima?), capability (o usuário pode?) e sanitização (o dado está limpo?). Falhar em qualquer uma delas abre uma porta. Lembre que callbacks de actions e filters que processam entrada do usuário precisam exatamente das mesmas verificações.

5. SQL seguro com $wpdb->prepare()

Para a esmagadora maioria dos casos, prefira as APIs de alto nível — WP_Query, get_posts(), get_post_meta() — que já tratam o SQL por você. Quando precisar mesmo de uma consulta direta com $wpdb, nunca concatene variáveis: use prepare() com placeholders.

<?php
global $wpdb;

$status   = sanitize_key( $_GET['status'] ?? 'publish' );
$autor_id = absint( $_GET['autor'] ?? 0 );

// ERRADO — SQL Injection: variável interpolada direto na query.
// $sql = "SELECT ID FROM {$wpdb->posts} WHERE post_status = '$status'";

// CERTO — placeholders: %s (string), %d (inteiro), %f (float).
$sql = $wpdb->prepare(
    "SELECT ID, post_title
       FROM {$wpdb->posts}
      WHERE post_status = %s
        AND post_author = %d
      LIMIT %d",
    $status,
    $autor_id,
    20
);

$linhas = $wpdb->get_results( $sql );

Pontos de atenção:

Checklist rápido por tipo de risco

RiscoDefesaFunção-chave
XSS (Cross-Site Scripting)Escapar a saída por contextoesc_html, esc_attr, esc_url, wp_kses_post
Dado de entrada inválidoSanitizar na entradasanitize_text_field, absint, sanitize_email
CSRFNonce no form + verificação no handlerwp_nonce_field, check_admin_referer, wp_verify_nonce
Escalonamento de privilégioChecar a capabilitycurrent_user_can
SQL InjectionConsulta parametrizada$wpdb->prepare

Adotar esses hábitos desde o primeiro arquivo poupa retrabalho e evita o pior cenário: uma invasão por malware originada do seu próprio código. Se você está estruturando uma extensão, vale ler também como criar um plugin do zero com esses cuidados embutidos desde o início.

Quer uma auditoria de segurança no código do seu plugin ou tema?

Reviso sanitização, escaping, nonces e queries seguindo os padrões oficiais do WordPress, e corrijo vulnerabilidades antes que virem incidente. Me chame no WhatsApp.

WhatsApp: (43) 99932-9697

ou e-mail: [email protected]

Perguntas frequentes

Qual a diferença entre sanitizar e escapar?

Sanitizar é limpar um dado que entra no sistema (um $_POST, um parâmetro de URL, uma chamada de API), removendo ou normalizando o que não deveria estar ali, antes de processar ou salvar. Escapar é tratar um dado na hora de imprimi-lo, neutralizando caracteres que teriam significado especial no contexto de saída (HTML, atributo, URL, JavaScript). O princípio do WordPress é "sanitize early, escape late": limpe na entrada, escape o mais perto possível do echo.

Preciso escapar mesmo um dado que eu já sanitizei na entrada?

Sim. Sanitizar e escapar resolvem problemas diferentes. Um dado pode ter sido sanitizado corretamente para um propósito e ainda assim ser perigoso em outro contexto de saída. Além disso, o valor pode ter vindo do banco, de outro plugin ou de uma versão antiga do seu código sem garantia de limpeza. Por isso a regra é escapar sempre na saída, com a função correta para o contexto — é a sua última linha de defesa contra XSS.

O que é um nonce e por que ele não substitui a verificação de permissão?

Um nonce (number used once) é um token com tempo de vida que comprova que a requisição partiu da sua tela, protegendo contra CSRF — alguém induzir o usuário logado a disparar uma ação sem querer. Ele não diz se o usuário pode executar a ação. Para isso existe o current_user_can(), que checa a capability. Ações sensíveis precisam das duas verificações: o nonce (a requisição é legítima?) e a capability (este usuário tem permissão?).

Por que não posso concatenar uma variável direto na query do $wpdb?

Porque abre brecha para SQL Injection: um valor controlado pelo usuário pode alterar a estrutura da consulta e ler ou destruir dados. A forma segura é usar $wpdb->prepare() com placeholders (%s, %d, %f), que escapa os valores no contexto SQL. Nunca interpole variáveis dentro da string SQL — mesmo que pareçam "seguras", como um ID.

wp_kses_post serve para entrada ou para saída?

Para os dois, dependendo do caso. Na entrada, use wp_kses_post() (ou wp_kses() com uma lista própria) para guardar HTML controlado, permitindo só as tags seguras de um post. Na saída, use wp_kses_post() quando precisar imprimir HTML que pode conter marcação legítima — é o caminho correto em vez de jogar esc_html() e perder a formatação, ou imprimir cru e arriscar XSS.

Existe uma função para sanitizar números inteiros?

Para inteiros não negativos (IDs de post, de usuário, quantidades), use absint(), que converte para inteiro e aplica valor absoluto. Para inteiros que podem ser negativos, use o cast (int) ou intval(). Já sanitize_key() serve para chaves/slugs internos (minúsculas, números, - e _), úteis para nomes de meta keys e identificadores.

Referências oficiais

  1. WordPress.org — Security (Common APIs)
  2. WordPress.org — esc_html() (Code Reference)
  3. WordPress.org — wp_verify_nonce() (Code Reference)
  4. WordPress.org — wpdb::prepare() (Code Reference)
  5. WordPress.org — Classe wpdb (Code Reference)
Tem alguma dúvida? Ficarei feliz em ajudar. Clique Aqui