Boas Práticas na Implementação de um Formulário de Contacto num Website

Duarte Spínola  |  2026-07-11

Hosting & CMS · Segurança · Forms · Acessibilidade · LGPD

Duarte Spínola · 11 de Julho de 2026

Um formulário de contacto é o ponto de entrada mais explotável de um website. Sem protecção adequada, pode ser usado para enviar spam (através do servidor de email), injectar headers SMTP, executar XSS Stored, fazer brute-force de credenciais, ou violar o RGPD/LGPD por recolher dados sem consentimento. Este artigo cobre as boas práticas de implementação — desde a validação de input no frontend e backend, protecção anti-spam com reCAPTCHA ou Cloudflare Turnstile, envio seguro via SMTP com PHPMailer, até à acessibilidade WCAG 2.1 e conformidade legal com o RGPD.

⚠ **Atenção

** Um formulário sem validação server-side é uma porta aberta para ataques. A validação no frontend (JavaScript) é apenas para experiência de utilizador — pode ser facilmente bypassada. Toda a validação crítica tem de acontecer no servidor.

1. O que Está em Risco — Ameaças a um Formulário Desprotegido

Ameaça Como funciona Consequência
Email Header Injection Atacante injecta \r\n no campo “From” para enviar email para milhares de destinatários Servidor usado para spam → IP em blacklists → email legítimo bloqueado
XSS Stored Atacante insere <script> num campo que é exibido noutra página Roubo de cookies de sessão de admins que visualizam as mensagens
SQL Injection Se as mensagens são guardadas em BD sem prepared statements Acesso à base de dados completa do website
Spam de bots Bots preenchem o formulário automaticamente com links publicitários Caixa de email inundada com spam, perda de tempo a filtrar
CSRF Atacante enganar um utilizador autenticado a submeter um formulário não intencional Acções executadas em nome do utilizador sem consentimento
Brute-force Se o formulário tem login ou recuperação de password Comprometimento de contas de utilizador
Violação de privacidade Recolher dados pessoais sem consentimento ou política de privacidade Coima RGPD até €20M ou 4% do volume de negócios

2. Validação de Input — A Primeira Linha de Defesa

2.1 Princípio: never trust user input

Toda a informação recebida do formulário deve ser tratada como potencialmente maliciosa até provado o contrário. Isto aplica-se a todos os campos — nome, email, assunto, mensagem, campos ocultos, e até cookies e headers HTTP.

2.2 Validação no Frontend (experiência de utilizador)

A validação no frontend dá feedback imediato ao utilizador, mas não substitui a validação no servidor. Usar atributos HTML5 nativos:

<form method="POST" action="/contacto-processar.php">
<label for="nome">Nome *</label>
<input type="text" id="nome" name="nome" required maxlength="100"
pattern="[A-Za-zÀ-ÿ\s]+"
oninvalid="this.setCustomValidity('Use apenas letras')"
oninput="this.setCustomValidity('')">
<label for="email">Email *</label>
<input type="email" id="email" name="email" required maxlength="254">
<label for="assunto">Assunto *</label>
<input type="text" id="assunto" name="assunto" required maxlength="200">
<label for="mensagem">Mensagem *</label>
<textarea id="mensagem" name="mensagem" required maxlength="5000"
minlength="10"></textarea>
<button type="submit">Enviar</button>
</form>

Atributos-chave:

  • required — campo obrigatório
  • maxlength — limite máximo de caracteres (previne inputs absurdamente longos)
  • pattern — expressão regular para validar formato
  • type="email" — valida automaticamente formato de email

Referência: MDN — HTML form element.

2.3 Validação no Backend (segurança real)

A validação no servidor é obrigatória. Em PHP:

<?php
// Sanitizar e validar cada campo
$nome = filter_input(INPUT_POST, 'nome', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
$email = filter_input(INPUT_POST, 'email', FILTER_SANITIZE_EMAIL);
$assunto = filter_input(INPUT_POST, 'assunto', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
$mensagem = filter_input(INPUT_POST, 'mensagem', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
// Validar email
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
die('Email inválido');
}
// Validar comprimento (defesa contra inputs excessivamente longos)
if (strlen($nome) > 100 || strlen($assunto) > 200 || strlen($mensagem) > 5000) {
die('Campo excede o tamanho permitido');
}
// Validar nome (apenas letras, espaços e acentos)
if (!preg_match('/^[\p{L}\s\-]+$/u', $nome)) {
die('Nome contém caracteres inválidos');
}
// Validar mensagem (mínimo 10 caracteres)
if (strlen(trim($mensagem)) < 10) {
die('Mensagem demasiado curta');
}
?>

⚠ **Atenção

** Nunca usar FILTER_SANITIZE_FULL_SPECIAL_CHARS como única defesa contra XSS. Sanitizar na entrada e escapar na saída (output encoding). Ver secção 6.

2.4 Prevenir Email Header Injection

O ataque mais comum em formulários de email é a injecção de headers SMTP. Se o campo “email” do utilizador é colocado directamente no header From: sem validação, um atacante pode injectar quebras de linha (\r\n) para adicionar BCC, CC, ou mesmo um corpo de email completamente diferente.

Vulnerável:

// NUNCA fazer isto
mail($para, $assunto, $mensagem, "From: $email_do_utilizador");

Se $email_do_utilizador contiver [email protected]\r\nBcc: [email protected], [email protected], ..., o servidor envia spam para milhares de destinatários.

Seguro:

<?php
// Validar email rigorosamente antes de usar em headers
$email = filter_var($email, FILTER_VALIDATE_EMAIL);
if ($email === false) {
die('Email inválido');
}
// Verificar que não há quebras de linha
if (preg_match('/[\r\n]/', $email) || preg_match('/[\r\n]/', $assunto)) {
die('Input inválido');
}
// Usar o email validado no header
$headers = "From: website@dominio.pt\r\n";
$headers .= "Reply-To: $email\r\n";
?>

Referência: OWASP Input Validation Cheat Sheet.

3. Anti-Spam — Bloquear Bots sem Frustrar Utilizadores

3.1 Honeypot (anti-spam invisível)

Um campo honeypot é um campo oculto no formulário que utilizadores reais nunca preenchem, mas bots preenchem automaticamente. Se o campo tem valor, o formulário é rejeitado.

<div style="position:absolute;left:-9999px;top:-9999px;" aria-hidden="true">
<label for="website">Não preencher este campo</label>
<input type="text" id="website" name="website" tabindex="-1"
autocomplete="off" aria-hidden="true">