Programação defensiva

Da Wikipédia, a enciclopédia livre
Ir para a navegação Saltar para pesquisar

A programação defensiva é uma forma de design defensivo destinada a desenvolver programas capazes de detectar possíveis anormalidades de segurança e dar respostas predeterminadas. [1] Garante o funcionamento contínuo de um software em circunstâncias imprevistas. As práticas de programação defensiva são frequentemente usadas onde é necessária alta disponibilidade , segurança ou proteção .

A programação defensiva é uma abordagem para melhorar o software e o código-fonte , em termos de:

  • Qualidade geral – reduzindo o número de bugs e problemas de software .
  • Tornar o código-fonte compreensível – o código-fonte deve ser legível e compreensível para que seja aprovado em uma auditoria de código .
  • Fazer o software se comportar de maneira previsível, apesar de entradas inesperadas ou ações do usuário.

A programação excessivamente defensiva, no entanto, pode proteger contra erros que nunca serão encontrados, incorrendo assim em tempo de execução e custos de manutenção. Há também o risco de que as interceptações de código evitem muitas exceções , resultando potencialmente em resultados incorretos e despercebidos.

Programação segura

A programação segura é o subconjunto da programação defensiva relacionada à segurança do computador . Segurança é a preocupação, não necessariamente segurança ou disponibilidade (o software pode falhar de certas maneiras). Como acontece com todos os tipos de programação defensiva, evitar bugs é o objetivo principal; no entanto, a motivação não é tanto para reduzir a probabilidade de falha na operação normal (como se a segurança fosse a preocupação), mas para reduzir a superfície de ataque – o programador deve assumir que o software pode ser mal utilizado ativamente para revelar bugs, e que bugs podem ser explorados maliciosamente.

int risky_programming ( char * input ) {   
  char str [ 1000 ]; // ... strcpy ( str , input ); // Copia a entrada. // ... }  
  
  
  
     
  
  

A função resultará em comportamento indefinido quando a entrada tiver mais de 1.000 caracteres. Alguns programadores podem não achar que isso é um problema, supondo que nenhum usuário entrará com uma entrada tão longa. Este bug em particular demonstra uma vulnerabilidade que permite explorações de estouro de buffer . Aqui está uma solução para este exemplo:

int secure_programming ( char * input ) {   
  caracter str [ 1000 + 1 ]; // Mais um para o caractere nulo.   

  // ...

  // Copia a entrada sem exceder o comprimento do destino. 
strncpy ( str , input , sizeof ( str ));    

  // Se strlen(input) >= sizeof(str) então strncpy não terminará com null. 
// Nós contrariamos isso sempre definindo o último caractere no buffer como NUL, // cortando efetivamente a string para o comprimento máximo que podemos manipular. // Pode-se também decidir abortar explicitamente o programa se strlen(input) for // muito longo. str [ sizeof ( str ) - 1 ] = '\0' ;  
  
  
  
      

  // ... 
}

Programação ofensiva

A programação ofensiva é uma categoria de programação defensiva, com a ênfase adicional de que certos erros não devem ser tratados defensivamente . Nesta prática, apenas erros de fora do controle do programa devem ser tratados (como entrada do usuário); o próprio software, bem como os dados de dentro da linha de defesa do programa, devem ser confiáveis ​​nesta metodologia .

Confiando na validade dos dados internos

Programação excessivamente defensiva
const char * trafficlight_colorname ( enum traffic_light_color c ) {     
    interruptor ( c ) {  
        case TRAFFICLIGHT_RED : return "vermelho" ;      
        case TRAFFICLIGHT_YELLOW : return "amarelo" ;   
        case TRAFFICLIGHT_GREEN : return "verde" ;    
    }
    retorne "preto" ; // Para ser tratado como um semáforo morto. // Aviso: Esta última instrução 'return' será descartada por um // compilador de otimização se todos os valores possíveis de 'traffic_light_color' estiverem listados // na instrução 'switch' anterior... }  
    
    
    

Programação ofensiva
const char * trafficlight_colorname ( enum traffic_light_color c ) {     
    interruptor ( c ) {  
        case TRAFFICLIGHT_RED : return "vermelho" ;      
        case TRAFFICLIGHT_YELLOW : return "amarelo" ;   
        case TRAFFICLIGHT_GREEN : return "verde" ;    
    }
    afirmar ( 0 ); // Afirma que esta seção é inacessível. // Aviso: esta chamada de função 'assert' será descartada por um compilador // otimizado se todos os valores possíveis de 'traffic_light_color' estiverem listados // na instrução 'switch' anterior... } 
    
    
    

Confiando em componentes de software

Programação excessivamente defensiva
if ( is_legacy_compatível ( user_config )) {  
    // Estratégia: Não confie que o novo código se comporte do mesmo 
old_code ( user_config );    
} mais {  
    // Fallback: Não confie que o novo código trata os mesmos casos 
if ( new_code ( user_config ) != OK ) {        
        código_antigo ( user_config );
    }
}
Programação ofensiva
// Espera que o novo código não tenha novos bugs 
if ( new_code ( user_config ) != OK ) {    
    // Reportar em voz alta e encerrar o programa abruptamente para obter a devida atenção 
report_error ( "Algo deu muito errado" );    
    saída ( -1 );
}

Técnicas

Aqui estão algumas técnicas de programação defensivas:

Reutilização inteligente de código fonte

Se o código existente é testado e funciona, reutilizá-lo pode reduzir a chance de erros serem introduzidos.

No entanto, reutilizar código nem sempre é uma boa prática. A reutilização de código existente, especialmente quando amplamente distribuído, pode permitir a criação de exploits que visam um público mais amplo do que seria possível e traz consigo toda a segurança e vulnerabilidades do código reutilizado.

Ao considerar o uso de código-fonte existente, uma rápida revisão dos módulos (subseções como classes ou funções) ajudará a eliminar ou tornar o desenvolvedor ciente de quaisquer vulnerabilidades em potencial e garantir que seja adequado para uso no projeto. [ citação necessária ]

Problemas de legado

Antes de reutilizar código-fonte antigo, bibliotecas, APIs, configurações e assim por diante, deve-se considerar se o trabalho antigo é válido para reutilização ou se é propenso a problemas legados .

Problemas de legado são problemas inerentes quando se espera que projetos antigos funcionem com os requisitos atuais, especialmente quando os projetos antigos não foram desenvolvidos ou testados com esses requisitos em mente.

Muitos produtos de software tiveram problemas com código-fonte legado antigo; por exemplo:

  • O código legado pode não ter sido projetado sob uma iniciativa de programação defensiva e, portanto, pode ser de qualidade muito inferior ao código-fonte recém-projetado.
  • O código legado pode ter sido escrito e testado em condições que não se aplicam mais. Os antigos testes de garantia de qualidade podem não ter mais validade.
    • Exemplo 1 : o código legado pode ter sido projetado para entrada ASCII, mas agora a entrada é UTF-8.
    • Exemplo 2 : código legado pode ter sido compilado e testado em arquiteturas de 32 bits, mas quando compilado em arquiteturas de 64 bits, novos problemas aritméticos podem ocorrer (por exemplo, testes de assinatura inválidos, conversões de tipo inválidas, etc.).
    • Exemplo 3 : o código legado pode ter sido direcionado para máquinas offline, mas torna-se vulnerável quando a conectividade de rede é adicionada.
  • O código legado não é escrito com novos problemas em mente. Por exemplo, o código-fonte escrito em 1990 provavelmente está sujeito a muitas vulnerabilidades de injeção de código , porque a maioria desses problemas não era amplamente compreendida naquela época.

Exemplos notáveis ​​do problema legado:

  • O BIND 9 , apresentado por Paul Vixie e David Conrad como "BINDv9 é uma reescrita completa ", "Segurança foi uma consideração chave no design", [2] nomeando segurança, robustez, escalabilidade e novos protocolos como principais preocupações para reescrever código legado antigo.
  • O Microsoft Windows sofreu com "a" vulnerabilidade do Windows Metafile e outras explorações relacionadas ao formato WMF. O Microsoft Security Response Center descreve os recursos WMF como "Por volta de 1990, o suporte WMF foi adicionado... Este era um momento diferente no cenário de segurança... todos eram totalmente confiáveis" , [3] não sendo desenvolvido sob as iniciativas de segurança em Microsoft.
  • A Oracle está combatendo problemas legados, como código-fonte antigo escrito sem abordar preocupações de injeção de SQL e escalonamento de privilégios , resultando em muitas vulnerabilidades de segurança que levaram tempo para corrigir e também geraram correções incompletas. Isso deu origem a fortes críticas de especialistas em segurança como David Litchfield , Alexander Kornbrust , Cesar Cerrudo . [4] [5] [6] Uma crítica adicional é que as instalações padrão (em grande parte um legado de versões antigas) não estão alinhadas com suas próprias recomendações de segurança, como o Oracle Database Security Checklist, que é difícil de corrigir, pois muitos aplicativos exigem que as configurações herdadas menos seguras funcionem corretamente.

Canonização

Usuários mal-intencionados provavelmente inventarão novos tipos de representações de dados incorretos. Por exemplo, se um programa tentar rejeitar o acesso ao arquivo "/etc/ passwd ", um cracker pode passar outra variante desse nome de arquivo, como "/etc/./passwd". Bibliotecas de canonização podem ser empregadas para evitar bugs devido a entrada não canônica .

Baixa tolerância contra bugs "potenciais"

Suponha que as construções de código que parecem ser propensas a problemas (semelhantes a vulnerabilidades conhecidas, etc.) sejam bugs e possíveis falhas de segurança. A regra básica é: "Não estou ciente de todos os tipos de explorações de segurança . Devo me proteger contra aqueles que conheço e, então, devo ser proativo!".

Outras dicas para proteger seu código

  • Um dos problemas mais comuns é o uso não verificado de estruturas de tamanho constante ou pré-alocadas para dados de tamanho dinâmico, como entradas para o programa (o problema de estouro de buffer ). Isso é especialmente comum para dados de string em C . As funções da biblioteca C como getsnunca devem ser usadas, pois o tamanho máximo do buffer de entrada não é passado como argumento. As funções da biblioteca C como scanfpodem ser usadas com segurança, mas exigem que o programador tome cuidado com a seleção de strings de formato seguro, limpando-as antes de usá-las.
  • Criptografe/autentique todos os dados importantes transmitidos pelas redes. Não tente implementar seu próprio esquema de criptografia, use um já comprovado . A verificação de mensagens com CRC ou tecnologia semelhante também ajudará a proteger os dados enviados por uma rede.

As 3 Regras de Segurança de Dados

* Todos os dados são importantes até prova em contrário.
 * Todos os dados estão corrompidos até prova em contrário.
 * Todo código é inseguro até prova em contrário.
    • Você não pode provar a segurança de nenhum código em userland , ou, mais comumente conhecido como: "nunca confie no cliente" .

Estas três regras sobre segurança de dados descrevem como lidar com quaisquer dados, de origem interna ou externa:

Todos os dados são importantes até prova em contrário - significa que todos os dados devem ser verificados como lixo antes de serem destruídos.

Todos os dados são contaminados até prova em contrário - significa que todos os dados devem ser tratados de uma forma que não exponha o restante do ambiente de tempo de execução sem verificar a integridade.

Todo código é inseguro até que se prove o contrário - embora seja um nome um pouco impróprio, faz um bom trabalho nos lembrando de nunca assumir que nosso código é seguro, pois bugs ou comportamento indefinido podem expor o projeto ou sistema a ataques como ataques comuns de injeção de SQL .

Mais informações

  • Se os dados devem ser verificados quanto à exatidão, verifique se estão corretos, não se estão incorretos.
  • Projeto por contrato
  • Asserções (também chamadas de programação assertiva )
  • Prefira exceções a códigos de retorno
    • De um modo geral, é preferível lançar mensagens de exceção que apliquem parte do seu contrato de API e orientem o desenvolvedor em vez de retornar valores de código de erro que não apontam para onde a exceção ocorreu ou como a pilha do programa parecia. aumente a robustez e a segurança do seu software, minimizando o estresse do desenvolvedor.

Veja também

Referências

  1. Boulanger, Jean-Louis (2016-01-01), Boulanger, Jean-Louis (ed.), "6 - Técnica para gerenciar a segurança do software" , Certified Software Applications 1 , Elsevier, pp. 125–156, ISBN 978-1-78548-117-8, recuperado 2022-09-02
  2. ^ "arquivo fogo: Paul Vixie e David Conrad em BINDv9 e Internet Security por Gerald Oskoboiny <[email protected]>" . impressionante.net . Recuperado em 27/10/2018 .
  3. ^ "Olhando para a questão WMF, como ele chegou lá?" . MSRC . Arquivado do original em 2006-03-24 . Recuperado em 27/10/2018 .
  4. ^ Litchfield, David. "Bugtraq: Oracle, onde estão os patches???" . seclists.org . Recuperado em 27/10/2018 .
  5. ^ Alexandre, Kornbrust. "Bugtraq: RE: Oracle, onde estão os patches???" . seclists.org . Recuperado em 27/10/2018 .
  6. ^ Cerrudo, César. "Bugtraq: Re: [divulgação completa] RE: Oracle, onde estão os patches???" . seclists.org . Recuperado em 27/10/2018 .

Links externos