Campos de entrada são a porta de entrada para dados fornecidos pelo usuário. Se não forem tratados adequadamente, podem se tornar vetores de ataques como injeção de SQL, Cross-Site Scripting (XSS), injeção de comandos e outros. Nesta aula, vamos explorar quatro pilares fundamentais para proteger esses campos: validação, normalização, codificação e escape. Compreender e aplicar essas técnicas é essencial para construir aplicações seguras e robustas.

Vamos começar com uma visão geral de cada conceito e depois aprofundar em cada um, com exemplos práticos e boas práticas. O foco será em linguagens de programação comuns, mas os princípios são universais.

Validação

Validação é o processo de verificar se os dados fornecidos pelo usuário atendem aos critérios esperados antes de aceitá-los. Isso inclui verificar tipo, formato, comprimento, faixa de valores e outras regras de negócio. A validação pode ser feita no cliente (JavaScript) e no servidor, mas a validação no servidor é imprescindível, pois o cliente pode ser burlado.

Por exemplo, se um campo espera um e-mail, devemos verificar se o valor contém um formato de e-mail válido. Se espera uma idade, deve ser um número inteiro entre 0 e 120. A validação não apenas previne ataques, mas também melhora a experiência do usuário, fornecendo feedback imediato.

Exemplo em Python com Flask (usando WTForms para validação):

from flask_wtf import FlaskForm
from wtforms import StringField, IntegerField, validators

class UserForm(FlaskForm):
    email = StringField('Email', [validators.DataRequired(), validators.Email()])
    age = IntegerField('Idade', [validators.NumberRange(min=0, max=120)])
    username = StringField('Username', [validators.Length(min=3, max=20)])

Neste exemplo, o formulário valida se o e-mail é obrigatório e tem formato válido, se a idade está entre 0 e 120, e se o username tem entre 3 e 20 caracteres. Se a validação falhar, o formulário não é processado e erros são exibidos.

Normalização

Normalização é o processo de transformar os dados em uma forma canônica ou padrão antes de processá-los. Isso é importante porque dados podem ser representados de várias maneiras, e diferentes representações podem causar problemas de segurança. Por exemplo, em Unicode, caracteres podem ser representados de formas diferentes (composição vs. decomposição). Um atacante pode explorar isso para contornar filtros.

Normalizar envolve, por exemplo, converter todas as letras para minúsculas, remover espaços extras, ou converter codificações. Em segurança, é crucial normalizar antes de aplicar validação ou armazenamento, para evitar bypasses.

Um exemplo comum é a normalização de caminhos de arquivos para evitar ataques de path traversal. Em Python, podemos usar os.path.normpath para normalizar um caminho:

import os
user_input = "../../etc/passwd"
normalized = os.path.normpath(user_input)
if normalized.startswith("/safe/directory"):
    print("Acesso permitido")
else:
    print("Acesso negado")

Outro exemplo é a normalização de strings Unicode usando unicodedata.normalize em Python, para evitar ataques de homógrafos ou caracteres invisíveis:

import unicodedata
s = "café"  # pode ter duas representações
nfc = unicodedata.normalize('NFC', s)
nfd = unicodedata.normalize('NFD', s)
print(nfc, nfd)

Encoding

Codificação (encoding) é o processo de converter dados de uma representação para outra, geralmente para transmissão ou armazenamento seguro. No contexto de segurança, é importante codificar dados de saída para evitar injeção. Por exemplo, ao exibir dados do usuário em HTML, devemos codificar caracteres especiais como <, >, &, etc., para que sejam interpretados como texto, não como HTML.

Também é importante codificar dados ao enviar para outros sistemas, como URLs, cabeçalhos HTTP, etc. Por exemplo, ao incluir dados em uma URL, devemos usar percent-encoding.

Em HTML, podemos usar bibliotecas como html.escape em Python:

import html
user_input = ""
escaped = html.escape(user_input)
print(escaped)  # <script>alert('xss')</script>

Em JavaScript, podemos usar encodeURIComponent para codificar dados em URLs:

let userInput = "foo bar&baz";
let encoded = encodeURIComponent(userInput);
console.log(encoded); // foo%20bar%26baz

Escaping

Escaping é um caso específico de codificação que visa neutralizar caracteres que têm significado especial em um contexto particular. Por exemplo, em SQL, aspas simples podem ser usadas para injetar código. Escaping envolve adicionar uma barra invertida (ou outro caractere) para indicar que o caractere deve ser tratado literalmente.

No entanto, a prática moderna recomenda usar prepared statements (consultas parametrizadas) em vez de escapar manualmente, pois é mais seguro e evita erros.

Exemplo em PHP com PDO (prepared statement):

$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email');
$stmt->execute(['email' => $email]);

Se você precisar escapar manualmente, em SQL, você pode usar mysqli_real_escape_string (mas não é recomendado):

$safe = mysqli_real_escape_string($conn, $input);
$query = "SELECT * FROM users WHERE username = '$safe'";

Em HTML, escapar é essencial para prevenir XSS. Já vimos o uso de html.escape. Em JavaScript, para inserir texto seguro no DOM, use textContent em vez de innerHTML:

element.textContent = userInput; // seguro
element.innerHTML = userInput; // perigoso

Boas Práticas e Observações Finais

Além de aplicar essas técnicas, é importante seguir algumas boas práticas:

  • Defesa em profundidade: Não confie em uma única camada de proteção. Combine validação, normalização, codificação e escape.
  • Valide sempre no servidor: A validação no cliente é apenas para UX, nunca para segurança.
  • Use bibliotecas testadas: Não tente implementar sua própria codificação ou escape; use funções e bibliotecas amplamente testadas.
  • Mantenha-se atualizado: Vulnerabilidades evoluem; acompanhe as melhores práticas e atualizações de segurança.
  • Teste com entradas maliciosas: Inclua casos de teste com payloads conhecidos (XSS, SQLi, etc.) para garantir que sua aplicação está protegida.

Lembre-se: a segurança de campos de entrada é uma responsabilidade contínua, não uma tarefa única.

Referências

Exercícios

  1. Exercício 1: Explique por que a validação no servidor é mais importante do que a validação no cliente. Dê um exemplo de ataque que poderia ocorrer se apenas a validação no cliente fosse usada.
  2. ✓ Resposta: A validação no servidor é crucial porque o cliente pode ser facilmente contornado: um atacante pode desativar JavaScript, usar ferramentas como curl, ou interceptar e modificar requisições. Sem validação no servidor, dados maliciosos podem ser processados, levando a ataques como injeção de SQL ou XSS. Por exemplo, se um campo de e-mail é validado apenas no cliente, um atacante pode enviar uma requisição direta para o servidor com um payload malicioso, e o servidor aceitará sem verificação.
  3. Exercício 2: Descreva uma situação em que a normalização de dados é necessária para evitar um ataque de path traversal. Implemente um exemplo em Python.
  4. ✓ Resposta: Em um sistema de download de arquivos, o usuário pode fornecer um nome de arquivo como "../../etc/passwd". Se o servidor simplesmente concatenar esse nome com o diretório base, o arquivo pode ser acessado fora do diretório permitido. A normalização usando os.path.normpath resolve isso, pois remove os componentes ".." e resolve o caminho. Exemplo:
    import os
    base_dir = '/var/www/uploads'
    user_input = '../../etc/passwd'
    full_path = os.path.join(base_dir, user_input)
    normalized = os.path.normpath(full_path)
    if normalized.startswith(base_dir):
        print('Acesso permitido')
    else:
        print('Acesso negado')
    
  5. Exercício 3: Qual é a diferença entre codificação e escape? Dê um exemplo de cada.
  6. ✓ Resposta: Codificação é um termo mais amplo que se refere a transformar dados de uma representação para outra, como base64 ou percent-encoding. Escaping é um tipo específico de codificação que adiciona caracteres de escape para neutralizar caracteres especiais em um contexto, como usar &lt; para < em HTML. Exemplo de codificação: encodeURIComponent em JavaScript. Exemplo de escape: html.escape em Python.
  7. Exercício 4: Considere o seguinte código PHP que usa mysqli_real_escape_string para prevenir injeção SQL. Ele é seguro? Justifique.
  8. $username = mysqli_real_escape_string($conn, $_POST['username']);
    $query = "SELECT * FROM users WHERE username = '$username'";
    

    ✓ Resposta: Esse código não é totalmente seguro. Embora mysqli_real_escape_string escape certos caracteres, ele não é infalível em todos os contextos, especialmente quando o conjunto de caracteres da conexão não é conhecido. Além disso, o uso de concatenação de strings é propenso a erros. A recomendação moderna é usar prepared statements com bind parameters, que separam dados de código, eliminando o risco de injeção SQL.
  9. Exercício 5: Escreva um exemplo em JavaScript que demonstre a diferença entre usar innerHTML e textContent ao inserir dados do usuário. Qual é seguro e por quê?
  10. ✓ Resposta: Exemplo:
    let userInput = "";
    // Perigoso: interpreta como HTML
    document.getElementById('output').innerHTML = userInput;
    // Seguro: insere como texto
    document.getElementById('output').textContent = userInput;
    
    Usar innerHTML pode executar scripts ou carregar elementos maliciosos, levando a XSS. textContent insere o conteúdo como texto puro, sem interpretar HTML, tornando-o seguro.