Entrada e saída
Validação de entrada, codificação de saída e serialização segura — as três que respondem pela maioria das falhas exploradas.
A maior parte das falhas exploradas na prática cabe em uma frase: dado de fora tratado como código. As três defesas são independentes e nenhuma substitui as outras.
Validar na entrada#
Validação responde este valor é aceitável? — e a resposta certa a um valor inaceitável é recusar, não consertar. Consertar cria o segundo problema: duas versões do mesmo dado, e a conferência feita sobre a errada.
| Conferir | A função | O que passa despercebido sem ela |
|---|---|---|
| número e faixa | Seg.numero_seguro | ?pagina=-1, ?tamanho=999999999 |
| JSON | Seg.json_seguro | dez mil níveis de aninhamento |
| URL de fora | Seg.url_segura | SSRF para 169.254.169.254 |
| caminho de arquivo | Seg.caminho_seguro | ../../etc/passwd, e o link simbólico |
| nome de arquivo | Seg.nome_de_arquivo_seguro | CON.txt, e a marca que inverte a leitura |
| destino de redirecionamento | Seg.redirecionamento_seguro | //banco-falso.exemplo |
| texto | Seg.sem_controle | caracteres invisíveis de direção de escrita |
| esquema de formulário | Kiln.validar, Lavra.validar | campo ausente tratado como vazio |
adopt Arcane.Seguranca as Seg
// Travessia: o 'realpath' antes de comparar e o que fecha o buraco.
// Sem ele, um link simbolico dentro da pasta aponta para fora e a
// comparacao de texto aprova.
monitor:
Seg.caminho_seguro("/tmp/uploads", "../../etc/passwd")
assert no
handle UnsafeInputError as e:
out "travessia recusada"
// E o byte nulo, que e a forma classica de truncar o nome DEPOIS
// da conferencia de extensao.
monitor:
Seg.caminho_seguro("/tmp/uploads", "foto.png\u0000.php")
assert no
handle Error as e:
out "byte nulo recusado"
// Nome de arquivo: '..' some, e o nome reservado do Windows ganha
// prefixo — criar 'CON.txt' falha la e funciona aqui.
assert Seg.nome_de_arquivo_seguro("../../etc/passwd") is "passwd"
assert Seg.nome_de_arquivo_seguro("CON.txt") is "_CON.txt"Codificar na saída#
Codificação responde para onde este texto vai? — e a resposta muda a função. O que protege uma página HTML não protege uma linha de shell, e o que protege shell estraga um CSV.
| Destino | Função | O que ela fecha |
|---|---|---|
| corpo HTML | escapar_html | XSS refletido e armazenado |
| atributo HTML | escapar_atributo | atributo sem aspas, onde o espaço é o fim do valor |
dentro de <script> | escapar_js | </script> no meio de uma string fechando a tag |
| URL | escapar_url | parâmetro que vira outro parâmetro |
| linha de shell | escapar_shell | injeção de comando |
LIKE do SQL | escapar_sql_like | % do usuário virando curinga (não é defesa contra injeção) |
| célula de planilha | escapar_csv | injeção de fórmula |
| cabeçalho HTTP | escapar_cabecalho | injeção de cabeçalho por CRLF |
| linha de log | escapar_log | log forjado |
| expressão regular | escapar_regex | padrão do usuário virando metacaractere |
adopt Arcane.Seguranca as Seg
// 1. O caso que mais engana: o texto do usuario vai DENTRO de um
// <script>. Escapar aspas nao resolve — '</script>' fecha a tag
// antes de o JavaScript ser lido, e o resto da pagina vira codigo.
perigoso := "</script><script>roubar()</script>"
assert "</script>" not in Seg.escapar_js(perigoso)
// 2. A injecao que quase ninguem escapa: o Excel EXECUTA a celula
// que comeca com '=', '+', '-' ou '@'.
assert Seg.escapar_csv("=HYPERLINK(\"http://mau\")")[0:1] is "'"
assert Seg.escapar_csv("Ana Souza") is "Ana Souza"
// 3. O log forjado: um '\n' num campo acrescenta uma LINHA inteira,
// e a investigacao seguinte le um evento que nunca aconteceu.
forjado := "ana\n2026-01-01 INFO admin apagou tudo"
assert "\n" not in Seg.escapar_log(forjado)
// 4. E o cabecalho, onde o CRLF injeta outro cabecalho.
assert Seg.escapar_cabecalho("/painel\r\nSet-Cookie: admin=1") is "/painelSet-Cookie: admin=1"
out "quatro destinos, quatro funcoes"Serializar e desserializar#
Desserialização insegura é a falha que mais surpreende, porque a operação parece passiva. Em linguagens onde o formato carrega tipos — pickle do Python, a serialização nativa do Java —, ler um arquivo é executar código.
O que continua sendo sua responsabilidade é a forma do que chegou: JSON válido não quer dizer JSON esperado.
adopt Arcane.Seguranca as Seg
adopt Arcane.Objetos as Obj
record Pedido:
id: Integer
total: Integer
// 1. Teto de tamanho, profundidade e numero de chaves ANTES de ler.
// Um '[[[[[...]]]]]' de dez mil niveis estoura a pilha de quem le,
// e o leitor padrao aceita: e negacao de servico com 50 KB.
cru := Seg.json_seguro("{\"id\": 7, \"total\": 199}")
// 2. O dado NUNCA escolhe o tipo. A lista de tipos e argumento,
// e as invariantes sao conferidas na chegada.
pedido := Obj.de_vault(cru, [Pedido])
assert pedido.id is 7
// 3. E o inverso, para gravar.
assert Obj.para_vault(pedido)["total"] is 199
out "o formato nao executa, e a forma e conferida"| Regra | Porque |
|---|---|
| o dado nunca escolhe a classe | Objetos.de_vault exige a lista de tipos como argumento |
setup não roda na desserialização | construtor é código; a chegada não o invoca |
| as invariantes são conferidas na chegada | um objeto inválido nunca existe |
| tamanho e profundidade antes de interpretar | a defesa tem de vir antes do trabalho |