Clickjacking — também chamado de UI redressing — é um ataque em que a vítima acha que está clicando em uma coisa, mas está clicando em outra. O atacante carrega a aplicação legítima dentro de um iframe invisível, posiciona esse iframe exatamente sobre um elemento atraente da sua própria página (um botão “Ganhe um prêmio”, por exemplo) e captura o clique. Para o navegador, o clique é totalmente legítimo: ele acontece dentro da sessão autenticada da vítima, com os cookies dela, na origem real da aplicação.

O resultado é que o usuário executa uma ação sensível — trocar e-mail de recuperação, autorizar um pagamento, dar permissão a um app OAuth, excluir a conta — sem nunca ter a intenção de fazê-lo.

Como funciona, na prática

O ataque depende de três ingredientes:

  1. A aplicação-alvo permite ser carregada em um iframe (não envia cabeçalhos que proíbam isso).
  2. A vítima tem uma sessão ativa na aplicação-alvo.
  3. O atacante consegue atrair a vítima para uma página que ele controla.

A página maliciosa monta o iframe da vítima por cima da sua isca e o torna transparente:

<style>
  /* O iframe da vítima fica por cima, invisível, captando o clique real */
  iframe {
    position: absolute;
    top: 0;
    left: 0;
    width: 800px;
    height: 600px;
    opacity: 0.0;        /* totalmente transparente */
    z-index: 2;
  }
  /* A isca fica embaixo, visível, alinhada ao botão real do alvo */
  #lure {
    position: absolute;
    top: 410px;          /* posição do botão "Confirmar" dentro do iframe */
    left: 300px;
    z-index: 1;
  }
</style>

<button id="lure">🎁 Clique para resgatar seu prêmio</button>
<iframe src="https://app.exemplo.com/configuracoes/excluir-conta"></iframe>

O usuário vê o botão de prêmio, clica nele, mas o clique atravessa para o botão “Confirmar exclusão” do iframe transparente. Durante o desenvolvimento do ataque, o invasor inverte a lógica (opacity: 0.5) para alinhar a isca com precisão de pixel e só depois zera a opacidade.

Variações que vão além do clique simples

  • Likejacking: sequestro de cliques em botões de “curtir”/“seguir” de redes sociais para inflar engajamento.
  • Cursorjacking: o atacante esconde o cursor real e desenha um cursor falso deslocado, fazendo a vítima clicar em um ponto diferente do que ela enxerga.
  • Drag-and-drop / fill jacking: combinando iframes e eventos de arrastar, é possível induzir a vítima a preencher e arrastar dados sensíveis para campos controlados pelo atacante.
  • Double clickjacking: técnica mais recente que abusa da janela de tempo entre dois cliques para burlar proteções que dependem de interação do usuário (como telas de consentimento OAuth).

Por que “frame busting” em JavaScript não basta

Durante anos, a defesa padrão foi o frame busting: um script que detecta estar dentro de um frame e quebra para fora.

// Frame busting clássico — frágil, NÃO confie nisso como única defesa
if (top !== self) {
  top.location = self.location;
}

Esse padrão falha de várias formas:

  • O atributo sandbox do iframe pode bloquear a navegação do top (sandbox="allow-forms allow-scripts" sem allow-top-navigation), neutralizando o top.location.
  • Navegadores antigos e handlers de onbeforeunload podem cancelar o redirecionamento.
  • race conditions explorables entre o carregamento do script e o clique.

Frame busting é, na melhor das hipóteses, uma camada extra — nunca a defesa principal.

A defesa correta: cabeçalhos de resposta

A proteção sólida contra clickjacking é dizer ao navegador, no servidor, quem pode (ou não) embutir suas páginas.

1. CSP frame-ancestors (recomendado, moderno)

A diretiva frame-ancestors da Content Security Policy é o padrão atual e o mais flexível. Para proibir totalmente o embutimento:

Content-Security-Policy: frame-ancestors 'none';

Para permitir apenas a própria origem (e nada de terceiros):

Content-Security-Policy: frame-ancestors 'self';

Para permitir um parceiro específico:

Content-Security-Policy: frame-ancestors 'self' https://parceiro-confiavel.com;

frame-ancestors suporta múltiplas origens, curinga de esquema e porta, e é respeitado por todos os navegadores modernos. É a única opção que cobre cenários de allowlist com mais de um domínio. Para conferir como o seu site responde hoje, cole os headers no Analisador de Security Headers e veja a nota e o fix por stack.

2. X-Frame-Options (legado, ainda útil)

Cabeçalho mais antigo, com apenas dois valores práticos:

X-Frame-Options: DENY
X-Frame-Options: SAMEORIGIN

O valor ALLOW-FROM uri foi descontinuado e é ignorado pela maioria dos navegadores. Se você precisa de allowlist, use frame-ancestors.

Mantenha X-Frame-Options por compatibilidade com user agents antigos, mas trate frame-ancestors como a fonte da verdade. Quando os dois discordam, o CSP prevalece nos navegadores modernos.

3. Cookies SameSite em ações sensíveis

Mesmo com o embutimento bloqueado, defina cookies de sessão como SameSite=Lax (ou Strict para áreas críticas). Isso garante que, em um contexto de terceiros, o cookie não acompanhe requisições que mudam estado — uma camada que reforça também a proteção contra CSRF.

Set-Cookie: session=...; HttpOnly; Secure; SameSite=Lax

4. Confirmação explícita para ações irreversíveis

Para operações de altíssimo impacto (excluir conta, transferir valores), exija uma confirmação que não possa ser “clicada por engano”: reautenticação, digitação de uma palavra, ou um desafio que o overlay invisível não consiga reproduzir.

Como testamos clickjacking em um pentest

No nosso processo de teste, a verificação segue uma linha objetiva:

  1. Mapear endpoints que mudam estado com um único clique (formulários POST, toggles, fluxos de consentimento).
  2. Inspecionar os cabeçalhos de resposta procurando Content-Security-Policy: frame-ancestors e/ou X-Frame-Options. A ausência em qualquer rota sensível já é um achado.
  3. Construir uma PoC real: embutir a página em um iframe controlado e confirmar que ela renderiza e é operável.
  4. Avaliar o impacto combinando com a função sensível — clickjacking que só “curte” é baixo risco; clickjacking sobre “autorizar app OAuth” é alto.

A falha mais comum que encontramos não é a ausência total de proteção, e sim a proteção inconsistente: a home e o login enviam os cabeçalhos, mas o painel administrativo ou um endpoint de API embutível, não.

Checklist de mitigação

  • Content-Security-Policy: frame-ancestors 'none' (ou allowlist explícita) em todas as respostas, inclusive APIs e páginas autenticadas.
  • X-Frame-Options: DENY/SAMEORIGIN como reforço para navegadores legados.
  • Cookies de sessão com HttpOnly; Secure; SameSite=Lax (ou Strict).
  • Reautenticação/confirmação forte em ações irreversíveis.
  • Cabeçalhos aplicados de forma centralizada (middleware/edge), não rota a rota.
  • Frame busting apenas como camada extra, nunca como única defesa.

Clickjacking é barato de explorar e barato de corrigir — o que o torna um achado quase imperdoável quando aparece em um relatório. Dois cabeçalhos de resposta, aplicados de forma consistente, eliminam a classe inteira do problema.