devly
Voltar para Secure Coding

Escrevendo Código que Resiste: Input Validation, Output Encoding e Mais

Técnicas de codificação defensiva que você pode aplicar HOJE: Input Validation, Output Encoding, Parameterized Queries, Princípio do Menor Privilégio e Defesa em Profundidade.

Atualizado em: 4 de Junho, 2025
Notas do Autor

E aí, Dev Defensor(a)! Na aula passada, encaramos os vilões do OWASP Top 10. Agora, é hora de arregaçar as mangas e aprender a construir fortalezas de código. Esta aula é sobre as técnicas de codificação defensiva que são o seu escudo e espada contra os ataques mais comuns.

Pense nestes princípios não como regras chatas, mas como boas práticas de engenharia que tornam seu software mais robusto, confiável e, claro, seguro. São os fundamentos que separam o código vulnerável daquele que resiste bravamente às tentativas de invasão.

Vamos ver como validar entradas como um segurança de boate, codificar saídas para não dar brecha pra XSS, e usar queries parametrizadas para dar um chega pra lá no SQL Injection. Preparado(a) para blindar seu código?


1. Input Validation: NUNCA Confie em Dados Externos 🛡️

Este é o mantra número um da segurança: NUNCA, JAMAIS, EM HIPÓTESE ALGUMA, confie em dados que vêm de fora da sua aplicação. Isso inclui input do usuário, parâmetros de URL, dados de APIs de terceiros, arquivos enviados, etc. Input validation é o processo de garantir que esses dados estejam no formato, tipo, tamanho e intervalo esperados ANTES de usá-los.

O que validar?

Tipo de dado (string, número, booleano, data).
Tamanho/Comprimento (mínimo e máximo).
Formato (ex: email, CEP, data no formato YYYY-MM-DD).
Intervalo de valores (ex: idade entre 18 e 99).
Caracteres permitidos (ex: apenas alfanuméricos, sem caracteres especiais que podem ser usados em injections).
Conformidade com regras de negócio específicas.

Exemplo (Node.js com uma biblioteca como Joi ou Zod - conceitual):

javascript
import Joi from 'joi';

// Esquema de validação para um payload de cadastro
const registerSchema = Joi.object({
  username: Joi.string().alphanum().min(3).max(30).required(),
  email: Joi.string().email().required(),
  password: Joi.string().min(8).required(), // Idealmente, validar complexidade também
  birthYear: Joi.number().integer().min(1900).max(new Date().getFullYear() - 18)
});

app.post('/register', (req, res) => {
  const { error, value } = registerSchema.validate(req.body);

  if (error) {
    return res.status(400).json({ message: error.details[0].message });
  }

  // Se chegou aqui, 'value' contém os dados validados e sanitizados (dependendo da config)
  // Prossiga com o cadastro usando 'value'
});

Warning

Sanitização vs. Validação: Validar é checar. Sanitizar é tentar 'limpar' ou modificar o input para torná-lo seguro (ex: remover tags HTML). A abordagem mais segura é validar primeiro e ser rigoroso. Se o dado não passa na validação, rejeite-o. Use sanitização com cautela e apenas quando apropriado.

2. Output Encoding: Exibindo Dados de Forma Segura 🖼️

Se o Input Validation é sobre proteger sua aplicação de dados ruins que entram, o Output Encoding é sobre proteger seus usuários de dados que saem da sua aplicação de forma perigosa. Especificamente, é a principal defesa contra Cross-Site Scripting (XSS).

A ideia é tratar qualquer dado que será inserido em uma página HTML como 'texto puro', e não como HTML ou script. Isso é feito 'escapando' caracteres especiais que têm significado em HTML (como <, >, &, ", ') para suas respectivas entidades HTML (&lt;, &gt;, &amp;, &quot;, &#39;).

Exemplo (JavaScript puro e template literals - conceitual):

javascript
function escapeHTML(unsafe) {
  return unsafe
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#039;");
}

// Suponha que userInput venha de uma fonte não confiável
const userInput = "<script>alert('XSS!')</script>";

// VULNERÁVEL:
// document.getElementById('comment').innerHTML = `Comentário: ${userInput}`;

// SEGURO (usando a função de escape):
// document.getElementById('comment').innerHTML = `Comentário: ${escapeHTML(userInput)}`;

// MELHOR AINDA (quando possível, use textContent para inserir apenas texto):
document.getElementById('comment').textContent = `Comentário: ${userInput}`; // userInput será tratado como texto literal

Success

Muitos frameworks e template engines modernos (React, Angular, Vue, Jinja2, EJS, etc.) fazem output encoding por padrão quando você insere dados em templates. No entanto, é crucial entender o que eles fazem e NUNCA desabilitar essa proteção ou usar métodos "raw HTML" (como dangerouslySetInnerHTML no React) sem entender 100% os riscos e sanitizar o conteúdo rigorosamente.

3. Parameterized Queries (Prepared Statements): Adeus, SQL Injection! 🚫💉

Já vimos o perigo do SQL Injection. A forma mais eficaz de preveni-lo é usando Parameterized Queries (também conhecidas como Prepared Statements).

Funciona assim:

Primeiro, você define a estrutura da query SQL com placeholders (como ?, $, ou :name) no lugar dos dados do usuário.
Depois, você envia os dados do usuário separadamente da query.
O banco de dados trata os dados do usuário como LITERAIS, não como parte do comando SQL. Isso impede que caracteres maliciosos alterem a lógica da query.

Exemplo (Node.js com a biblioteca \`pg\` para PostgreSQL):

javascript
const { Client } = require('pg');
const client = new Client(/* ... config ... */);
client.connect();

const userInputId = "1 OR 1=1"; // Input malicioso

// VULNERÁVEL (Concatenação de String - NÃO FAÇA!):
// const queryTextVulnerable = `SELECT * FROM users WHERE id = '${userInputId}'`;
// client.query(queryTextVulnerable, (err, res) => { /* ... */ });


// SEGURO (Parameterized Query):
const queryTextSafe = 'SELECT * FROM users WHERE id = $1';
const values = [userInputId]; // Os valores são passados separadamente

client.query(queryTextSafe, values, (err, res) => {
  if (err) {
    console.error(err.stack);
  } else {
    // Mesmo com input malicioso, a query busca apenas pelo ID literal "1 OR 1=1" (que não existirá)
    console.log(res.rows);
  }
  client.end();
});

Info

Quase todas as bibliotecas de acesso a banco de dados modernas suportam parameterized queries. Use-as SEMPRE. ORMs (Object-Relational Mappers) também costumam usar prepared statements por baixo dos panos, mas é bom confirmar.

4. Princípio do Menor Privilégio (PoLP) 🗝️

O Princípio do Menor Privilégio (Principle of Least Privilege - PoLP) é simples: um usuário ou componente do sistema só deve ter as permissões estritamente necessárias para realizar suas tarefas legítimas, e nada mais.

Por que isso é importante? Se uma conta de usuário ou um componente do sistema é comprometido, o dano que um atacante pode causar é limitado pelas permissões que aquela conta/componente possui.

Usuários de banco de dados: Sua aplicação web não precisa de permissões de `DROP TABLE` no banco de dados de produção. Crie usuários com permissões granulares (SELECT, INSERT, UPDATE, DELETE apenas nas tabelas necessárias).
Contas de serviço: Processos rodando no servidor devem usar contas com privilégios mínimos, não como `root` ou `Administrator`.
Aplicações e APIs: Não conceda acesso de administrador a todas as APIs internas se uma funcionalidade específica só precisa ler dados de um endpoint.
Tokens de API: Gere tokens com escopos limitados (ex: um token que só permite ler dados de produtos, não modificar pedidos).

Success

PoLP é uma mentalidade: antes de conceder uma permissão, pergunte-se: "Essa permissão é REALMENTE necessária para esta tarefa específica?".

5. Defesa em Profundidade (Defense in Depth) 🏰

Defesa em Profundidade não é uma técnica única, mas uma estratégia de segurança que envolve a aplicação de múltiplas camadas de controles de segurança. A ideia é que, se uma camada de defesa falhar, outras camadas ainda estarão lá para proteger seus ativos.

Pense em um castelo medieval: ele tem muralhas altas, um fosso, portões fortificados, arqueiros, guardas internos, etc. Cada um é uma camada de defesa.

Para uma aplicação web, isso pode incluir:

Web Application Firewall (WAF) na borda.
Input validation rigoroso no backend.
Output encoding em todas as saídas.
Parameterized queries para acesso a dados.
Controles de acesso fortes (autenticação e autorização).
Princípio do Menor Privilégio para todas as contas e processos.
Logging e monitoramento de segurança.
Scans de vulnerabilidade regulares.
Patches de segurança aplicados em tempo hábil.

Info

Nenhuma camada de segurança é perfeita. A Defesa em Profundidade aumenta a resiliência do seu sistema, tornando muito mais difícil para um atacante ser bem-sucedido.

💊 Pílula Devly

Dominar Input Validation, Output Encoding, Parameterized Queries, PoLP e pensar em Defesa em Profundidade são os superpoderes do dev seguro. Essas não são apenas técnicas, são a base para construir software que não só funciona, mas que protege seus usuários e sua empresa. Incorpore esses hábitos no seu dia a dia e veja seu código (e sua carreira) se fortalecerem!

Anterior

OWASP Top 10 (Versão Dev)

Voltar

Próximo

Segredos no Cofre: Lidando com Senhas, Tokens e HTTPS

Continuar