Segurança é um aspecto crucial no desenvolvimento de aplicações web, e PHP, sendo uma das linguagens mais utilizadas para desenvolvimento web, exige atenção especial. Nesta aula, vamos explorar as principais ameaças que suas aplicações podem enfrentar, baseadas no projeto OWASP, e discutir estratégias para mitigá-las. Você aprenderá sobre validação de dados, o conceito de defesa em profundidade e a importância de adotar um mindset voltado à segurança.

A segurança não é um recurso que se adiciona no final do desenvolvimento; é uma prática que deve ser incorporada desde a concepção do projeto. Vamos ver como aplicar isso no contexto PHP, com exemplos práticos e boas práticas que você pode adotar imediatamente.

Principais ameaças (OWASP, visão geral)

A OWASP (Open Web Application Security Project) mantém uma lista atualizada dos riscos mais críticos para aplicações web, conhecida como OWASP Top 10. Essa lista é uma referência essencial para entender onde focar seus esforços de segurança. Para aplicações PHP, as ameaças mais relevantes incluem:

  • Injeção (SQL, LDAP, OS) - quando dados não confiáveis são interpretados como parte de um comando ou consulta.
  • Quebra de autenticação - falhas em mecanismos de login e gerenciamento de sessão.
  • Exposição de dados sensíveis - falta de criptografia adequada para proteger informações.
  • XML External Entities (XXE) - processamento inseguro de XML.
  • Controle de acesso quebrado - permissões e restrições mal configuradas.
  • Configurações de segurança incorretas - servidores e frameworks com defaults inseguros.
  • Cross-Site Scripting (XSS) - execução de scripts maliciosos no navegador do usuário.
  • Deserialização insegura - manipulação de dados serializados.
  • Componentes vulneráveis - uso de bibliotecas e frameworks com conhecidas vulnerabilidades.
  • Logging e monitoramento insuficientes - falta de rastreamento de eventos de segurança.

Como desenvolvedor PHP, você deve estar ciente dessas ameaças e implementar contramedidas. Por exemplo, para prevenir injeção SQL, use prepared statements com PDO ou MySQLi. Para mitigar XSS, sempre escape a saída com htmlspecialchars(). Vamos detalhar essas práticas nos próximos subtópicos.

Validação

A validação de dados é a primeira linha de defesa contra muitos ataques. Ela garante que apenas dados esperados sejam aceitos pela aplicação. Em PHP, você deve validar tanto no lado do cliente (JavaScript) quanto no servidor, mas a validação no servidor é a mais crítica, pois o cliente pode ser contornado. Existem várias abordagens:

Filtros de entrada: Use funções como filter_var() para validar dados de entrada. Por exemplo, para validar um email, você pode fazer:

$email = $_POST['email'];
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
    // email inválido
    exit('Email inválido');
}

Validação de tipos: Verifique se os dados são do tipo esperado. Por exemplo, para um campo numérico, use is_numeric() ou ctype_digit().

Validação de comprimento e formato: Use strlen() para verificar comprimentos e expressões regulares para formatos específicos. Por exemplo, para validar um CPF, você pode usar uma regex.

Sanitização: Além de validar, você deve sanitizar os dados para remover qualquer conteúdo perigoso. Para evitar XSS, use htmlspecialchars() ao exibir dados. Para evitar SQL Injection, use prepared statements (que veremos em breve).

Aqui está um exemplo de validação combinada:

function validar_dados($dados) {
    $erros = [];
    // Validar nome
    if (empty($dados['nome']) || strlen($dados['nome']) < 3) {
        $erros[] = 'Nome deve ter pelo menos 3 caracteres';
    }
    // Validar email
    if (!filter_var($dados['email'], FILTER_VALIDATE_EMAIL)) {
        $erros[] = 'Email inválido';
    }
    // Validar idade
    $idade = filter_var($dados['idade'], FILTER_VALIDATE_INT, ['options' => ['min_range' => 18]]);
    if ($idade === false) {
        $erros[] = 'Idade deve ser um número inteiro maior ou igual a 18';
    }
    return $erros;
}

Lembre-se: validação é sobre aceitar ou rejeitar dados, enquanto sanitização é sobre modificar dados para torná-los seguros. Ambos são importantes.

Defesa em profundidade

Defesa em profundidade é uma estratégia que consiste em aplicar múltiplas camadas de segurança, de modo que, se uma falha for explorada, outras camadas ainda protejam o sistema. No contexto PHP, isso significa combinar várias técnicas de segurança:

  • Validação de entrada: A primeira camada, como vimos.
  • Prepared statements: Para prevenir SQL Injection, use sempre prepared statements com PDO ou MySQLi. Nunca concatene diretamente valores em consultas SQL.
  • Escape de saída: Use htmlspecialchars() ao exibir dados do usuário para prevenir XSS.
  • CSRF Protection: Implemente tokens CSRF em formulários para evitar ataques de cross-site request forgery.
  • Autenticação e autorização: Use senhas com hash (por exemplo, password_hash()) e verifique permissões em cada ação.
  • Configuração do servidor: Desative exibição de erros em produção, use HTTPS, configure cabeçalhos HTTP de segurança (por exemplo, Content-Security-Policy).
  • Atualização de componentes: Mantenha o PHP e bibliotecas sempre atualizadas para corrigir vulnerabilidades conhecidas.

Exemplo de prepared statement com PDO:

$pdo = new PDO('mysql:host=localhost;dbname=test', $user, $pass);
$stmt = $pdo->prepare('SELECT * FROM usuarios WHERE email = :email');
$stmt->execute(['email' => $email]);
$result = $stmt->fetch();

Exemplo de proteção CSRF:

session_start();
if ($_POST) {
    if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
        die('Token CSRF inválido');
    }
}
$token = bin2hex(random_bytes(32));
$_SESSION['csrf_token'] = $token;

Na view, você incluiria: <input type="hidden" name="csrf_token" value="<?php echo $token; ?>">

Essas camadas funcionam juntas para proteger a aplicação mesmo se uma delas falhar.

Mindset

Adotar um mindset de segurança significa pensar como um atacante e sempre considerar as implicações de segurança em cada decisão de código. Isso envolve:

  • Nunca confie em entrada do usuário: Todo dado vindo de fora deve ser tratado como potencialmente malicioso.
  • Menor privilégio: Dê apenas as permissões necessárias para cada usuário e processo.
  • Fail securely: Em caso de erro, falhe de forma segura (por exemplo, não revele informações internas).
  • Segurança por padrão: Configure opções seguras por padrão, como usar password_hash() em vez de MD5.
  • Revisão de código: Revise seu código regularmente em busca de vulnerabilidades.
  • Aprenda com os erros: Quando uma vulnerabilidade for encontrada, documente e evite repetir.

Um exemplo prático: ao armazenar senhas, nunca use MD5 ou SHA1. Sempre use password_hash() com o algoritmo bcrypt:

$hash = password_hash($senha, PASSWORD_DEFAULT);
if (password_verify($senha, $hash)) {
    // senha correta
}

Além disso, implemente logs de segurança para monitorar tentativas de ataque. Use funções como error_log() para registrar eventos relevantes.

Pensar em segurança desde o início economiza tempo e evita dores de cabeça futuras. Faça da segurança um hábito, não um pensamento tardio.

Boas práticas e observações finais

Para concluir, aqui estão algumas boas práticas que você deve adotar em seus projetos PHP:

  • Use um framework moderno (Laravel, Symfony) que já implementa muitas proteções.
  • Mantenha o PHP atualizado (use versões suportadas).
  • Use HTTPS em produção.
  • Configure o php.ini adequadamente (ex.: expose_php = Off, display_errors = Off em produção).
  • Faça backups regulares.
  • Realize testes de segurança, como pentests.

Lembre-se: segurança é um processo contínuo. Sempre esteja atento a novas ameaças e atualize suas práticas.

Referências

Exercícios

  1. Exercício 1: Explique por que a validação no servidor é mais importante que a validação no cliente. Dê um exemplo de um ataque que pode ocorrer se você confiar apenas na validação do cliente.

    ✓ Resposta: A validação no servidor é crucial porque o cliente pode ser facilmente manipulado. Um atacante pode desativar o JavaScript ou enviar requisições diretamente para o servidor, contornando a validação do cliente. Por exemplo, se um formulário de login valida apenas no cliente que o email tem formato correto, um atacante pode enviar manualmente uma requisição POST com um email malicioso, como admin' OR '1'='1, que pode causar injeção SQL se não for tratado no servidor.
  2. Exercício 2: Escreva um trecho de código PHP que valide um campo de telefone (formato brasileiro, ex.: (11) 91234-5678) usando expressão regular e que rejeite entradas inválidas.

    ✓ Resposta:
    $telefone = '(11) 91234-5678';
    $padrao = '/^\(\d{2}\) \d{4,5}-\d{4}$/';
    if (preg_match($padrao, $telefone)) {
        echo 'Telefone válido';
    } else {
        echo 'Telefone inválido';
    }
    
  3. Exercício 3: Explique o que é defesa em profundidade e liste três camadas de segurança que você implementaria em uma aplicação PHP.

    ✓ Resposta: Defesa em profundidade é uma estratégia que aplica múltiplas camadas de segurança para que, se uma for comprometida, outras ainda protejam o sistema. Três camadas: 1) Validação de entrada (filtros e sanitização), 2) Prepared statements para prevenir SQL Injection, 3) Escape de saída com htmlspecialchars() para prevenir XSS.
  4. Exercício 4: Implemente um token CSRF em um formulário PHP. Mostre o código do formulário e a verificação no servidor.

    ✓ Resposta:
    // No servidor, antes de exibir o formulário
    session_start();
    $token = bin2hex(random_bytes(32));
    $_SESSION['csrf_token'] = $token;
    // No formulário:
    <form method="post">
        <input type="hidden" name="csrf_token" value="<?php echo $token; ?>">
        <!-- outros campos -->
        <button type="submit">Enviar</button>
    </form>
    
    // No processamento do POST:
    if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
        die('Token CSRF inválido');
    }
    
  5. Exercício 5: Qual é a função PHP recomendada para armazenar senhas de forma segura? Por que você não deve usar MD5 ou SHA1?

    ✓ Resposta: A função recomendada é password_hash() com o algoritmo PASSWORD_DEFAULT (bcrypt). MD5 e SHA1 são rápidos e sem salt, tornando-as vulneráveis a ataques de força bruta e rainbow tables. password_hash() gera um hash com salt automático e é computacionalmente caro, dificultando ataques.