Criar programas C++ confiáveis e seguros
A publicação do governo dos Estados Unidos NISTIR 8397: Diretrizes sobre Padrões Mínimos para Verificação de Software para Desenvolvedores contém excelentes diretrizes sobre como criar softwares confiáveis e seguros em qualquer linguagem de programação.
Este documento segue a mesma estrutura da NISTIR 8397. Cada seção:
- resume como usar produtos de desenvolvedor da Microsoft para C++ e outras linguagens para atender às necessidades de segurança da respectiva seção e
- fornece diretrizes para você obter o maior aproveitamento em cada área.
2.1 Modelagem de ameaças
Resumo
A modelagem de ameaças é um processo importante, especialmente quando aplicado de forma a ser dimensionado para atender às suas necessidades de desenvolvimento, o que reduz o ruído.
Recomendações
A modelagem de ameaças deve ser um dos componentes do seu Ciclo de Vida de Desenvolvimento de Segurança (SDL) dinâmico. Sugerimos que, para seu produto como um todo, para um recurso específico ou para uma alteração de design ou implementação importante, você:
- Tenha um SDL sólido e dinâmico que permita um envolvimento antecipado com as equipes de desenvolvedores e um dimensionamento adequado da abordagem.
- Aplique a modelagem de ameaças de forma direcionada. Aplique a modelagem de ameaças a todos os recursos, mas comece de maneira tática com os recursos expostos, complexos ou críticos. Aplique-a regularmente, em vez disso, como parte de uma revisão de produto de cima para baixo.
- Aplique a modelagem de ameaças mais cedo (como ocorre com todos os requisitos de segurança), quando ainda houver oportunidade de alterar o design. Além disso, os modelos de ameaças atuam como fonte de informações para outros processos, como redução da superfície de ataque ou design com foco em segurança. Os modelos de ameaças criados mais tarde são, na melhor das hipóteses, "sondagens" para fins de testes de penetração (teste de caneta) ou áreas que precisam de testes de segurança, como fuzzing. Após criar um modelo de ameaças como base de referência, planeje prosseguir iterando nele à medida que a superfície de ataque mudar.
- Use a conformidade e o inventário de ativos para acompanhar adequadamente o que constitui um produto e rastrear os artefatos de segurança (incluindo os modelos de ameaças), juntamente com os ativos aos quais se aplicam. Essa abordagem permite uma melhor avaliação de risco automatizada e uma concentração dos esforços de segurança nos componentes ou recursos específicos que apresentarem alterações.
- No Azure, a Microsoft Threat Modeling Tool (Ferramenta de Modelagem de Ameaças da Microsoft) foi atualizada em 2022 para o desenvolvimento no Azure. Para obter mais informações, confira Visão Geral da Microsoft Threat Modeling Tool — Azure
Práticas e fatores comprobatórios
Para aplicar a modelagem de ameaças corretamente e evitar a subutilização ou uso excessivo, descobrimos que os conceitos básicos a seguir precisam ser abordados em primeiro lugar.
Abordagem de desenvolvimento
Para começar, entenda a abordagem de desenvolvimento da equipe. Para as equipes com fluxos de trabalho de desenvolvimento ágeis, que encaminham dezenas de alterações para a produção diariamente, não é prático nem razoável requerer uma atualização do modelo de ameaças para cada alteração funcional. Em vez disso, ao escrever os requisitos funcionais de um recurso considere, desde o início, incluir um questionário de requisitos de segurança. O questionário deve se concentrar em perguntas específicas sobre o recurso para determinar quais aspectos futuros do seu SDL irão se aplicar. Por exemplo:
- O recurso apresenta uma alteração importante no design e na forma como fornecemos o isolamento do cliente em um ambiente multilocatário? Nesse caso, pense em executar um modelo de ameaças completo.
- Um novo recurso permite uploads de arquivos? Nesse caso, talvez o mais apropriado seja uma avaliação de segurança de um aplicativo web.
- Essa alteração é basicamente apenas uma alteração funcional da interface do usuário? Nesse caso, talvez você não precise de nada além do seu conjunto de ferramentas automatizadas tradicional.
Os resultados do questionário de segurança informam quais técnicas de SDL devem ser vinculadas a qual unidade de desenvolvimento. Também informam os cronogramas do SDL do recurso aos parceiros de desenvolvimento para que possam colaborar nos momentos certos.
Inventário de produtos
Em segundo lugar, mantenha um sólido inventário de ativos para os produtos que você está encarregado de avaliar. A complexidade dos produtos está aumentando. É comum escrever software para dispositivos conectados que incluem:
- sensores (como trens de passageiros e veículos);
- redes baseadas em barramentos que se comunicam com outros componentes no veículo (como CANBUS ou PROFIBUS);
- sem fio/celular/Bluetooth para comunicação com dispositivos de clientes e back-ends de nuvem;
- aprendizado de máquina na nuvem retroalimentando o dispositivo ou um aplicativo de gerenciamento de frota.
- e mais.
Em produtos complexos como esses, a modelagem de ameaças é essencial. Ter um inventário de ativos sólido permite que você veja toda a pilha de produtos para ter uma visão do panorama completo e dos locais cruciais que precisam ser avaliados quanto à maneira como um recurso novo ou alterado afetará a segurança do produto.
Granularidade e integração
Estabeleça sistemas para medir a conformidade usando métricas claras.
- Avalie a conformidade regularmente para o desenvolvimento no nível de recurso. A conformidade dos recursos de modo geral deve ser medida com maior frequência e mais granularidade, às vezes até mesmo no sistema do desenvolvedor ou por ocasião do commit/merge do código.
- Avalie periodicamente a segurança do produto como um todo em que um recurso ou componente está sendo consumido. Avaliações mais amplas costumam ser feitas com menor frequência e menos granularidade, como durante os testes de módulos ou sistemas.
Escala
Mantenha um sistema de inventário de ativos adequado, que capture e preserve os artefatos de segurança e os resultados das revisões dos modelos de ameaças. Ter um inventário claro permite que você avalie os resultados de revisões para detectar padrões e tome decisões inteligentes sobre como refinar o programa de segurança do produto regularmente.
Tente combinar os questionários de segurança da fase de requisitos com os resultados da modelagem de ameaças, das avaliações de segurança e das ferramentas automatizadas. Combinar esses elementos permite que você automatize um ponto de vista de risco relativo de um determinado produto, idealmente na forma de um "painel de controle", para informar às suas equipes de segurança no que devem se concentrar para obter o máximo rendimento da modelagem de ameaças.
2.2 Testes automatizados
Resumo
Os testes automatizados são uma maneira importante de garantir a qualidade e a segurança do seu código. Esses testes são parte integrante do apoio às outras áreas mencionadas neste documento, como a Modelagem de Ameaças. Quando emparelhados com outras práticas seguras de codificação, ajudam a proteger contra as vulnerabilidades e bugs que podem ser introduzidos na base de código.
Atributos de chave
Os testes devem ser confiáveis, consistentes e isolados. Devem abranger o máximo de código possível. Todos os novos recursos e correções de bugs devem ter testes correspondentes, para garantir a segurança e a confiabilidade do código no longo prazo sempre que possível. Execute testes automatizados regularmente e no maior número de ambientes possível, para garantir que sejam executados e cubram todas as áreas:
- O primeiro lugar em que devem ser executados é o computador que está fazendo as alterações. A execução de testes é feita com mais facilidade dentro do IDE que está sendo usado para edição, ou como um script na linha de comando à medida que o desenvolvedor faz as alterações.
- O próximo local em que devem ser executados é como parte do processo de commit/merge de pull requests.
- O último local em que os testes devem ser executados é como parte de um pipeline de Integração Contínua e Implantação Contínua (CI/CD), ou nos seus builds candidatos ao lançamento.
O escopo dos testes deve ser ampliado a cada etapa, com a última etapa fornecendo uma cobertura completa para qualquer coisa que as outras etapas possam ter deixado passar.
Uso e manutenção contínuos
A confiabilidade do teste é uma parte importante para manter a eficácia do conjunto de testes. As falhas de testes devem ser atribuídas e investigadas, com os possíveis problemas de segurança sendo tratados com alta prioridade e atualizados dentro de um período de tempo pré-determinado e em tempo hábil. Ignorar as falhas de teste não deve ser considerado uma prática comum; ao contrário, deve exigir uma justificativa sólida e a consequente aprovação. As falhas de teste decorrentes de problemas do próprio conjunto de testes devem ser tratadas da mesma forma que as demais falhas, para evitar um lapso na cobertura em que problemas do produto possam não ser percebidos.
Tipos de testes, especialmente testes de unidade
Existem vários tipos de testes automatizados e, embora nem todos sejam aplicáveis a todos os aplicativos, um bom conjunto de testes deve conter uma seleção de vários tipos diferentes. Casos de Teste Baseados em Código, como os testes de unidade, são os mais comuns e os mais completos, sendo aplicáveis a todos os aplicativos e abrangendo, intencionalmente, o maior número possível de caminhos de código para a correção. Esses testes devem ser pequenos e rápidos e não devem afetar o estado do computador, para que o conjunto completo de testes possa ser executado frequentemente e com rapidez. Se possível, execute os testes em muitos computadores que tenham configurações de hardware diferentes, para detectar problemas que não são reproduzíveis em um único tipo de computador.
Visual Studio
O Gerenciador de Testes do Visual Studio é nativamente compatível com muitas estruturas de teste de C++ mais populares e oferece opções para instalar extensões para estruturas adicionais. Essa flexibilidade é útil para executar um subconjunto de testes abrangendo o código no qual você está trabalhando e facilita a depuração de falhas de testes à medida que aparecem. O Visual Studio também facilita a configuração de novos conjuntos de testes para projetos existentes, além de fornecer ferramentas úteis, como o CodeLens, para facilitar o gerenciamento desses testes. Para obter mais informações sobre como escrever, executar e gerenciar testes em C/C++ com o Visual Studio, confira Escrever testes de unidade para C/C++ — Visual Studio (Windows).
No Azure e no CI/CD do GitHub
Os testes que fazem uma verificação mais profunda e levam mais tempo para ser executados, como a análise estática, a detecção de componentes etc. são boas escolhas para testes de pull request ou de integração contínua. O Azure DevOps e o GitHub Actions facilitam a execução automática de validações e bloqueiam check-ins de código se uma validação falhar. A implementação automatizada ajuda a garantir que todo o código sendo verificado é seguro, com base nessas verificações mais rigorosas sendo executadas. Os recursos Azure Pipelines e a Validação de Build do Azure DevOps são descritos aqui:
- Políticas e configurações de branch do Git — Azure Repos
- Como definir a capacidade de fazer merge de pull requests | Documentos do GitHub
2.3 Análise baseada em código, ou estática
Resumo A análise de código/binária estática deve estar habilitada por padrão, para ser segura por padrão. A análise estática analisa um programa quanto às políticas de proteção e segurança obrigatórias no momento de sua criação, não de sua execução, quando uma exploração pode ocorrer no computador do cliente. A análise estática pode analisar o programa no formulário do código-fonte ou no formulário executável compilado.
Recomendações A Microsoft recomenda que você:
- Habilite a análise estática para todos os programas em C++, tanto para o código-fonte original (antes da compilação) quanto para os binários executáveis (após a compilação). "Habilitar" pode significar executar a análise durante cada build no computador do desenvolvedor, como um build separado para inspecionar o código mais tarde ou como um requisito de check-in.
- Incorpore a análise estática aos pipelines de CI como uma forma de teste.
- A análise estática vem com falsos positivos por definição, então você deve se preparar para incorporar esse fato ao seu loop de feedback de qualidade. Habilite todos os avisos de falsos positivos pouco prováveis (low-false-positive) rapidamente com antecedência. Em seguida, seja proativo para aumentar gradualmente o número de regras com relação às quais sua base de código é compilada como livre de avisos à medida que você adiciona regularmente mais regras que sinalizam bugs importantes, em detrimento de falsos positivos gradualmente mais prováveis (inicialmente, antes de também limpar a base de código para essas regras).
- Sempre use as versões com suporte do Visual Studio mais recentes e configure seu ambiente de engenharia para consumir rapidamente as versões de patch mais recentes assim que estiverem disponíveis, sem atrasar o próximo estágio/ciclo de desenvolvimento.
Ferramentas importantes Esteja ciente dos seguintes itens e certifique-se de usá-los:
- Documentação da análise de código — C++ e .NET
/analyze
— Compilador do Visual C++/W4
e/WX
— Compilador do Visual C++- Use os Verificadores das Diretrizes Principais do C++
- CodeQL | GitHub
- Guia do usuário do Binskim | GitHub
- Confira também (somente no Windows): Anotações de SAL
Observações:
/analyze
permite a análise estática do código em C++ no momento da compilação, para identificar vulnerabilidades críticas de segurança e confiabilidade do código. A habilitação deve ocorrer em toda a linha do tempo de desenvolvimento de um programa em C++. Comece habilitando pelo menos os itens "Recomendados Nativamente pela Microsoft" por padrão, como uma base de referência mínima. Em seguida, confira a documentação sobre como especificar mais regras, em especial as regras das Diretrizes Principais do C++, conforme exigido pelas políticas de engenharia. A funcionalidade de Análise Estática do código-fonte está disponível tanto no IDE do Visual C++ quanto nas Ferramentas de Build de linha de comando./W4
e/WX
devem estar habilitados sempre que possível, para garantir que você compile seu código de forma limpa e com altos níveis de aviso (W4
), além de tratar os avisos como erros que precisam ser corrigidos (WX
). Essas opções permitem localizar erros de dados não inicializados que outras ferramentas de análise estática não conseguem verificar, porque os erros só ficam visíveis após o back-end do compilador executar uma análise interprocedural e o inlining da função.- A análise de binários do BinSkim garante que os projetos habilitem uma ampla gama de recursos de segurança. O BinSkim gera PDBs e outros resultados que tornam mais fácil verificar a cadeia de custódia e reagir com eficiência aos problemas de segurança. A Microsoft recomenda executar a ferramenta BinSkim para analisar todos os binários executáveis (
.sys
,.dll
ou.exe
) produzidos para seus programas ou consumidos por eles. O Guia de Usuário do BinSkim inclui uma lista de padrões de segurança com suporte. A Microsoft recomenda que você corrija todos os problemas relatados como "erros" pela ferramenta BinSkim. Os problemas relatados como "avisos" devem ser avaliados seletivamente, porque resolvê-los pode ter implicações de desempenho ou não ser necessário.
No Azure e no CI/CD do GitHub, a Microsoft recomenda sempre habilitar a análise estática do código-fonte e dos binários em cenários de CI/CD de lançamento. Execute a análise do código-fonte imediatamente no computador local do desenvolvedor, ou pelo menos para cada commit ou pull request, de modo a capturar os bugs do código-fonte o mais cedo possível e minimizar os custos de modo geral. Bugs no nível de binário tendem a ser introduzidos mais lentamente, então pode ser suficiente executar a análise do binário em cenários de CI/CD de pré-lançamento menos frequentes (como builds noturnos ou semanais).
2.4 Rever segredos incluídos no código
Resumo
Não inclua segredos dentro do código do software. Você pode encontrar e remover os segredos do código-fonte com eficiência usando ferramentas confiáveis que podem verificar toda a base de código do seu código-fonte. Após encontrar os segredos, transfira-os para um local seguro seguindo a diretriz de armazenamento seguro e uso de segredos.
Problema
"Segredos" significa entidades que estabelecem identidade e fornecem acesso aos recursos, ou que são usadas para assinar ou criptografar dados confidenciais. Os exemplos incluem senhas, chaves de armazenamento, cadeias de conexão e chaves privadas. É tentador manter segredos no produto de software, para que possam ser prontamente obtidos pelo software quando necessário. No entanto, esses segredos incluídos no código podem resultar em incidentes de segurança graves ou catastróficos, já que são descobertos com facilidade e podem ser usados para comprometer seu serviço e seus dados.
Prevenção
Os segredos incluídos no código-fonte (como textos sem formatação ou blobs criptografados) são uma vulnerabilidade de segurança. Aqui estão as diretrizes gerais sobre como evitar segredos no código-fonte:
- Use uma ferramenta antes do check-in para verificar e capturar possíveis segredos incluídos no código antes de enviá-lo ao controle do código-fonte.
- Não coloque credenciais de texto claras nos arquivos de configuração ou do código-fonte.
- Não armazene credenciais de texto claras no SharePoint, no OneNote, em compartilhamentos de arquivos etc. Tampouco as compartilhe por email, mensagens instantâneas etc.
- Não criptografe um segredo com uma chave de descriptografia facilmente detectável. Por exemplo, não armazene um arquivo PFX junto com um arquivo que contenha a respectiva senha.
- Não criptografe um segredo com uma descriptografia fraca. Por exemplo, não criptografe um arquivo PFX com uma senha fraca ou comum.
- Evite colocar credenciais criptografadas no código do código-fonte. Em vez disso, use espaços reservados no seu código-fonte e permita que seu sistema de implantação os substitua por segredos provenientes de repositórios aprovados.
- Aplique os mesmos princípios aos segredos incluídos em ambientes como os de teste, preparo etc., como você faz nas implantações de produção. Adversários costumam visar sistemas que não são de produção, já que não são tão bem gerenciados, e, em seguida, usá-los para se transferir para a produção.
- Não compartilhe segredos entre diferentes implantações (por exemplo, teste, preparo, produção).
Embora isso não seja diretamente relacionado aos segredos incluídos no código, lembre-se também de proteger segredos para seus testes, desenvolvimento e produção:
- Rotacione seus segredos periodicamente e sempre que possam ter sido expostos. Ter uma capacidade demonstrada de rotacionar/reimplantar segredos é uma evidência de um sistema seguro. Mais notavelmente ainda, a ausência dessa funcionalidade é uma evidência ainda mais forte de uma vulnerabilidade inevitável.
- Não se renda à justificativa comum de desenvolvedores no sentido de que "minhas credenciais de teste não criam risco". Na prática, isso quase sempre acontece.
- Pense em manter uma distância definitiva de segredos (por exemplo, senhas, chaves ao portador), dando preferência às soluções de RBAC/orientadas à identidade como uma boa solução de engenharia que possa evitar totalmente uma gestão de segredos ineficiente.
Detecção
Componentes herdados do seu produto podem conter segredos ocultos incluídos no código do respectivo código-fonte. Às vezes, segredos dos computadores da área de trabalho dos desenvolvedores podem entrar no branch remoto e se mesclar ao branch de lançamento, vazando segredos involuntariamente. Para descobrir os segredos que podem estar ocultos no seu código-fonte, você pode usar ferramentas que podem verificar seu código em busca de segredos incluídos no código:
Remediação
Quando são encontradas credenciais no código-fonte, é necessário urgente e imediatamente invalidar a chave exposta e executar uma análise de risco baseada na exposição. Mesmo se o seu sistema precisar permanecer em execução, você pode habilitar um gerenciador de segredos para a correção usando as etapas a seguir:
- Se a correção permitir alternar para identidades gerenciadas ou requerer a introdução de um gerenciador de segredos como o Azure Key Vault (AKV), faça isso primeiro. Em seguida, reimplante com a identidade ou chave atualizadas.
- Invalide o segredo exposto.
- Execute a auditoria/avaliação de riscos dos possíveis danos decorrentes do comprometimento.
Para proteger chaves criptográficas e outros segredos usados por aplicativos e serviços de nuvem, use o Azure Key Vault com uma política de acesso adequada.
Se comprometer determinados dados/PII do cliente, a exposição poderá exigir outros requisitos de conformidade/notificação.
Remova os segredos agora invalidados do seu código-fonte e os substitua por métodos alternativos que não exponham os segredos diretamente no seu código-fonte. Busque oportunidades de eliminar segredos sempre que possível usando ferramentas como o Azure AD. Você pode atualizar seus métodos de autenticação para tirar proveito das identidades gerenciadas por meio do Azure Active Directory. Use apenas repositórios aprovados para armazenar e gerenciar segredos, como, por exemplo, o Azure Key Vault (AKV). Para saber mais, veja:
Azure DevOps (AzDO)
Os usuários do AzDO podem verificar seu código por meio da GitHub Advanced Security para Azure DevOps (GHAzDO). O GHAzDO também permite que usuários evitem exposições de segredos ao habilitar a Proteção contra Push em seus repositórios para capturar possíveis exposições antes que qualquer vazamento ocorra. Para obter mais informações sobre como detectar segredos incluídos no código do código-fonte no Azure DevOps, confira Verificação de Segredos no GitHub Advanced Security para Azure DevOps em cada um dos seguintes links:
- GitHub Advanced Security para Azure DevOps
- Verificação de Segredos no GitHub Advanced Security para Azure DevOps
- Microsoft Defender para DevOps Versão Prévia
No GitHub
A verificação de segredos está disponível no GitHub.com em dois formatos:
- Alertas de verificação de segredos para parceiros. É executado automaticamente em todos os repositórios públicos. As cadeias de caracteres que correspondem aos padrões fornecidos por parceiros da verificação de segredos são relatadas diretamente ao parceiro relevante.
- Alertas de verificação de segredos para usuários. Você pode habilitar e configurar uma verificação adicional para os repositórios pertencentes a organizações que usam o GitHub Enterprise Cloud e têm uma licença do GitHub Advanced Security. Essas ferramentas também são compatíveis com repositórios privados e internos.
O GitHub fornece padrões de segredos conhecidos para parceiros e usuários, que podem ser configurados para atender às suas necessidades. Para obter mais informações, confira:
Observação
O GitHub Advanced Security for Azure DevOps traz a mesma verificação de segredos, verificação de dependências e soluções de verificação de código CodeQL já disponíveis para os usuários do GitHub e as integra nativamente ao Azure DevOps para proteger seus Repositórios e Pipelines do Azure.
Recursos adicionais
- Verificação de credenciais | Microsoft Code com Guia Estratégico de Engenharia.
- Ferramenta de verificação de credenciais detect-secrets | GitHub: um módulo com o nome adequado para detectar segredos dentro de uma base de código.
- Como executar o detect-secrets no Azure Pipelines.
- Git-secrets | awslabs no GitHub: impede que você faça commit de senhas e outras informações confidenciais para um repositório do git.
- Gerenciamento de Segredos | Microsoft Code com Guia Estratégico de Engenharia: fornece diretrizes gerais sobre como os segredos devem ser gerenciados.
2.5 Executar com verificações e proteção fornecidas pelo sistema operacional e pela linguagem
Resumo
O reforço da proteção de binários é feito por meio da aplicação de controles de segurança no momento da compilação. Esses controles incluem mitigações que:
- evitam vulnerabilidades exploráveis no código;
- habilitam detecções de runtime que disparam defesas de segurança na exploração; e
- habilitam a produção e o arquivamento de dados para ajudar a limitar os danos causados por incidentes de segurança.
Os consumidores de binários precisam optar pelos recursos de segurança do Windows para obter o benefício total da proteção reforçada.
A Microsoft fornece um conjunto de instalações específico para projetos em C++, para ajudar os desenvolvedores a escrever e fornecer códigos cada vez mais seguros. Os desenvolvedores de C++ também devem aderir aos padrões de segurança comuns às linguagens que geram código executável. A Microsoft mantém o BinSkim, um software de código aberto público verificador de binários que ajuda a implementar o uso de várias das proteções descritas nesta seção. Para obter mais informações sobre o BinSkim, confira o Guia do Usuário do Binskim | GitHub
Os controles em nível de binário diferem conforme o local em que são aplicados dentro do processo de engenharia. Você deve distinguir entre as opções de compilador e vinculador que: sejam estritamente aplicadas no momento da compilação, alterem a geração de código com sobrecarga no runtime e alterem a geração de código para atingir a compatibilidade com as proteções do sistema operacional.
As configurações do desenvolvedor devem preferir habilitar o máximo possível de análise estática, habilitar a produção de dados privados para acelerar a depuração etc. Os builds de lançamento devem ser ajustados para uma combinação apropriada entre segurança, desempenho e outros motivos de preocupação relacionados à geração de código. Os processos de lançamento precisam ser configurados para gerar e gerenciar corretamente os dados de build consumidos em caráter público ou privado (por exemplo, símbolos públicos ou privados).
Mantenha-se atualizado: sempre use ferramentas e compiladores atualizados
Compile todos os códigos com conjuntos de ferramentas atualizados para se beneficiar de um suporte à linguagem, análise estática, geração de código e controles de segurança atualizados. Devido ao fato de que os compiladores afetam cada componente gerado, a possibilidade de regressão na atualização da ferramenta é relativamente alto. O uso de compiladores desatualizados cria um risco específico para a ação corretiva ao responder a um incidente de segurança, porque as equipes podem não ter tempo suficiente para atualizar os compiladores. A Microsoft recomenda que as equipes desenvolvam uma instalação que atualize e teste as atualizações do compilador regularmente.
Usar versões de linguagem, estruturas/APIs e métodos de desenvolvimento seguros
O código deve utilizar metodologias de desenvolvimento, versões de linguagem, estrutura, APIs etc., que minimizem o risco ao promover a segurança e a simplicidade no C++, incluindo:
- Confira Biblioteca de Suporte às Diretrizes (GSL) das Diretrizes Principais do C++ para obter diretrizes sobre como escrever um código em C++ moderno, seguro e consistente que siga as boas práticas e evite as armadilhas comuns.
- Confira Implementação da GSL da Microsoft para obter informações sobre as funções e tipos que as Diretrizes Principais do C++ sugerem que você use.
- Contêineres em C++ seguros para recursos, proteções contra estouro de memória da biblioteca de runtime em C (CRT): prefira
std::vector
estd::string
, que são seguros para recursos. Se precisar usar dados em C, use versões seguras das funções de CRT, que foram projetadas para ajudar a evitar a corrupção da memória devido ao uso indevido de buffers e comportamentos de linguagem indefinidos. - A biblioteca SafeInt protege contra o estouro de número inteiro em operações matemáticas e de comparação.
Consuma dependências seguras
Os binários não devem se vincular a bibliotecas e dependências que não sejam seguras. As equipes de desenvolvimento devem acompanhar todas as dependências externas e resolver as CVES/vulnerabilidades de segurança identificadas nesses componentes ao atualizá-los para versões mais seguras quando estiverem sujeitas a essas vulnerabilidades.
Maximizar as garantias de procedência de código e a eficiência da resposta de segurança
A compilação deve habilitar garantias reforçadas de procedência do código para ajudar a detectar e impedir a introdução de backdoors e outros códigos mal-intencionados. Os dados resultantes, também críticos para fins de depuração e investigação, devem ser arquivados para todas as versões do software, de modo a resultar em uma resposta de segurança eficiente se estiverem comprometidos. As opções de compilador a seguir geram informações críticas para uma resposta de segurança:
/ZH:SHA_SHA256
no Visual C++: garante que um algoritmo criptograficamente seguro seja usado para gerar todos os hashes do arquivo de código-fonte no PDB./Zi
,/ZI
(Formato de Informações de Depuração) no Visual C++: além de publicar símbolos despojados para coletar dados de falhas e outros cenários de uso público, garante que os builds produzam e arquivem PDBs privados para todos os binários disponibilizados. As ferramentas de análise de binários requerem símbolos completos para verificar se muitas mitigações de segurança foram habilitadas no momento da compilação. Os símbolos privados são críticos em uma resposta de segurança e reduzem os custos de depuração e investigação quando os engenheiros estão em uma corrida para avaliar e limitar os danos por ocasião de uma exploração./SOURCELINK
no Vinculador do Visual C++ — Incluir o arquivo Sourcelink no PDB: o link para o código-fonte original é um sistema independente de linguagem e de controle do código-fonte que fornece depuração do código-fonte para binários. A depuração do código-fonte aumenta muito a eficiência do intervalo de validações de segurança antes do lançamento e a resposta a incidentes após o lançamento.
Habilitar os erros do compilador para evitar problemas no momento da criação do código
A compilação deve habilitar verificações de compilador relevantes para a segurança, como erros que causam interrupções, por exemplo:
/sdl
no Visual C++ — Habilitar verificações de segurança adicionais transforma muitos avisos relevantes para a segurança em erros e habilita recursos avançados de geração de código seguro.- BinSkim BA2007. EnableCriticalCompilerWarnings | GitHub mantém uma lista de avisos do compilador em C/C++ recomendados pela Microsoft que devem sempre ser habilitados e transformados em erros.
Marcar os binários como compatíveis com as mitigações de segurança do runtime do sistema operacional
As configurações do compilador e do vinculador devem optar por recursos de geração de código que detectem e atenuem a execução de código mal-intencionado, incluindo:
- Prevenção contra corrupção da pilha
/SAFESEH
— A imagem tem manipuladores de exceção seguros: produz uma tabela dos manipuladores de exceção seguros de imagem para binários x86./GS
— Verificação de Segurança de Buffers: detecta alguns estouros de buffer que substituem endereços de retorno, endereços de manipuladores de exceção ou determinados tipos de parâmetros.
- Execução de código independente de posição
/DYNAMICBASE
— Usar Randomização de Layout de Espaço de Endereços: gera imagens executáveis que podem ser realocadas aleatoriamente no momento do carregamento./HIGHENTROPVA
e/LARGEADDRESSAWARE
— Compatível com ASLR de 64 bits e manipulação de endereços grandes: habilita o uso de todo o espaço de endereço de 64 bits para realocação da imagem.
- Integridade do fluxo de código
/guard:cf
— Habilitar a Proteção de Fluxo de Controle: insere verificações do runtime para destinos de chamadas indiretas./CETCOMPAT
— Compatível com a pilha de sombra de CET: marca uma imagem executável como compatível com a implementação pela Microsoft do recurso Pilha de Sombra da Tecnologia de Implementação de Fluxo de Controle (CET) da Intel./guard:ehcont
— Habilitar metadados de continuação de EH: gera uma lista de endereços virtuais relativos (RVA) seguros de todos os destinos de continuação de manipulação de exceções.
- Prevenção de execução de dados
/NXCOMPAT
— Compatível com a Prevenção de Execução de Dados: marca uma imagem executável de 32 bits como compatível com o recurso Prevenção de Execução de Dados do Windows (DEP). Os builds de 64 bits são compatíveis com o DEP por padrão.
Impedir a divulgação de informações confidenciais
As configurações do compilador devem optar por impedir a descoberta de informações confidenciais. Nos últimos anos, pesquisadores revelaram vazamentos de informações não intencionais provenientes de recursos de hardware como a execução especulativa.
No nível do software, os dados confidenciais podem ser transmitidos aos invasores se forem vazados inesperadamente. O fato de não zerar os buffers e outros usos indevidos de buffers pode resultar no vazamento de dados confidenciais privados para invasores que chamam uma API confiável. A melhor maneira de lidar com essa classe de problemas é habilitar uma análise estática adicional e usar contêineres de recursos seguros, conforme descritos anteriormente.
/Qspectre
— Mitigar ataques de canal lateral de execução especulativa: insere instruções para criar uma barreira que ajude a impedir a divulgação de dados confidenciais produzidos pela execução especulativa. Essas mitigações devem ser habilitadas nos códigos que armazenam dados confidenciais na memória e operam além de um perímetro de confiança. A Microsoft sempre recomenda medir o impacto no desempenho com relação aos parâmetros de comparação apropriados ao habilitar mitigações do Spectre devido à possibilidade de introduzir verificações de runtime em loops ou blocos críticos de desempenho. Esses caminhos de código podem desabilitar mitigações por meio do modificador despectre(nomitigation)
declspec
. Os projetos habilitados/Qspectre
também devem ser vinculados a bibliotecas que também são compiladas com essas mitigações, incluindo as bibliotecas de runtime da Microsoft.
2.6 Casos de teste de caixa preta
Resumo
Os testes de caixa preta não dependem do conhecimento do funcionamento interno do componente testado. Os testes de caixa preta são projetados para testar a funcionalidade dos recursos do produto de ponta a ponta, em qualquer camada ou nível. Os testes de caixa preta podem ser testes funcionais, testes de interface do usuário, testes de desempenho e testes de integração. Os testes de caixa preta são importantes para medir a confiabilidade geral e o funcionamento correto, além de garantir que o produto se comporte conforme o esperado.
Relação com outras seções
Esses tipos de testes baseados em requisitos são úteis para validar as suposições feitas no Modelo de Ameaça e abranger possíveis ameaças, conforme abordadas na seção em questão. Esses testes são úteis para testar a integração entre componentes separados do produto, especialmente os que estão além de perímetros de confiança, conforme descrito no modelo de ameaça. Casos de teste de caixa preta também são úteis para testar todos os tipos de casos de borda para a validação de informações fornecidas pelo usuário. É útil testar tanto casos de borda conhecidos quanto casos de erro. O teste de fuzzing também é útil para testar casos menos óbvios.
Automação e regressão
Execute esses testes regularmente e compare os resultados com os de execuções anteriores para capturar alterações interruptivas ou regressões de desempenho. Além disso, executar esses testes em vários computadores e configurações de instalação diferentes pode ajudar a abranger quaisquer problemas que possam surgir em alterações de instalação ou arquiteturas diferentes.
Despejos de memória
Esses testes ajudam a encontrar problemas de confiabilidade e têm a capacidade de testar muitos cenários diferentes que podem possivelmente enfrentar panes, travamentos, impasses etc. Ao coletar despejos de memória como parte de falhas de teste, você pode importar os despejos diretamente para o Visual Studio para investigar mais profundamente quais partes do código estão atingindo esses problemas. Se executar testes funcionais de dentro do Visual Studio, você poderá replicar e depurar falhas com facilidade ao ver exatamente em que ponto dentro da caixa preta o teste apresenta falha, além de poder testar correções rapidamente.
Para começar a usar testes de depuração, confira Depurar testes de unidade com o Gerenciador de Testes — Visual Studio (Windows)
No Azure
O Azure DevOps também pode ajudar a gerenciar e validar esses testes com o uso de Planos de Teste. Esses testes podem ser usados para garantir a aprovação com uma validação manual e para executar testes automatizados associados aos requisitos do produto. Mais informações sobre os Azure Test Plans e como usá-los para executar testes automatizados podem ser encontradas aqui:
- O que é Azure Test Plans? Ferramentas de teste manuais, exploratórias e automatizadas. — Azure Test Plans
- Executar testes automatizados dos planos de teste: Azure Test Plans
2.7 Casos de teste baseados em código
Resumo
Os casos de teste baseados em código são parte integrante da manutenção da segurança e confiabilidade do produto. Esses testes devem ser pequenos e rápidos e não devem exercer um impacto mútuo, para que possam ser executados em paralelo. Os testes baseados em código são de fácil execução para os desenvolvedores localmente, em seus computadores de desenvolvimento, sempre que fizerem alterações no código, sem que precisem se preocupar em reduzir o ritmo de seu ciclo de desenvolvimento.
Tipos e sua relação com outras seções
Os tipos comuns de casos de teste baseados em código incluem:
- testes de unidade;
- testes parametrizados para abranger funções com vários tipos de entradas de dados;
- testes de componentes para manter cada componente de teste separado; e
- testes de simulação, para validar partes do código que se comunicam com outros serviços sem expandir o escopo do teste de modo a incluir esses serviços propriamente ditos.
Esses testes se baseiam no código interno já escrito, enquanto os testes de caixa preta se baseiam nos requisitos funcionais externos do produto.
Meta
Por meio desses testes, o objetivo é alcançar um alto nível de cobertura de testes relacionados ao seu código. Você deve acompanhar ativamente essa cobertura para verificar se existem lacunas. À medida que você adiciona mais testes que exercitam mais caminhos de código, a confiança geral na segurança e o grau de confiabilidade do seu código aumentam.
Visual Studio
As ferramentas do gerenciador de testes no Visual Studio facilitam a execução frequente desses testes e recebem feedback sobre os índices de aprovação/falha e sobre os locais de falha rapidamente. Muitas das estruturas de teste também são compatíveis com os recursos do CodeLens para ver o status do teste no local do teste propriamente dito, facilitando o acréscimo e a manutenção do conjunto de testes. O Gerenciador de Testes também facilita o gerenciamento desses testes, permitindo o uso de grupos de teste, playlists de teste personalizadas, filtragem, classificação, pesquisa e muito mais.
Para saber mais, veja:
- Conceitos básicos de testes de unidade — Visual Studio (Windows): uma introdução e visão geral
- Executar testes de unidade com o Gerenciador de Testes — Visual Studio (Windows): uma visão mais profunda do que está disponível para ajudar a gerenciar um conjunto possivelmente grande de testes de unidade com o Gerenciador de Testes
O Visual Studio também vem com ferramentas para acompanhar a cobertura do código. Essas ferramentas permitem que você se certifique de que as alterações de código realizadas sejam cobertas por testes existentes ou adicione novos testes para abranger caminhos de código novos e não testados. As ferramentas também mostram o percentual de cobertura do código para garantir que seja mantido acima de um nível pretendido e assegurar a confiança na qualidade geral do código.
Para obter informações sobre essas ferramentas, confira Testes de cobertura de código — Visual Studio (Windows)
No Azure
O Azure DevOps também pode ajudar a acompanhar os resultados de cobertura de código para o produto inteiro, como parte do processo do pipeline de build. Para obter mais informações, confira Rever a cobertura de código — Azure Pipelines.
2.8 Casos de teste históricos
Resumo
Os casos de teste históricos, também conhecidos como casos de teste de regressão, impedem que problemas antigos ressurjam novamente e aumentem a cobertura de testes do produto de modo geral. Você deve se certificar de que, quando um bug for corrigido, o projeto também adicione um caso de teste correspondente. Ao longo do tempo, à medida que as correções são feitas, a consistência geral do conjunto de testes continuará a aumentar, fornecendo garantias melhores de confiabilidade e segurança.
Principais qualidades e sua relação com outras seções
Como eles fazem testes para detectar regressões de bug, esses testes devem ser rápidos e fáceis de executar, para que possam ser executados juntamente com os Casos de Teste Baseados em Código e contribuir para a cobertura geral de código do produto. Paralelamente, usar exemplos reais de clientes para inspirar novos casos de teste é uma ótima maneira de aprimorar a cobertura e a qualidade dos testes.
Visual Studio
O Visual Studio permite que você adicione testes ao pacote com facilidade e, ao mesmo tempo, faça as alterações para corrigir o bug, além de executar os testes e a cobertura de código rapidamente para garantir que todos os casos novos sejam levados em conta. Fazer referência à ID do bug do seu sistema de acompanhamento de problemas no seu código no qual você escreve o teste é uma boa maneira de conectar testes de regressão aos problemas correspondentes. Prefira usar o Azure Boards e os planos de teste junto com o Visual Studio:
- para associar testes, casos de teste e problemas; e
- para acompanhar todos os aspectos de um problema e seus testes correspondentes.
Para saber mais, veja:
- Associar testes automatizados a casos de teste — Azure Test Plans
- Vincular itens de trabalho a outros objetos — Azure DevOps
Eventualmente, a integração desses testes à área de teste de unidade que pretende abranger a seção do código ajuda a manter o conjunto de testes organizado e torná-lo mais fácil de gerenciar. Você pode usar o agrupamento de testes do Gerenciador de Testes para acompanhar com eficácia os testes associados entre si. Para obter mais informações, confira Executar testes de unidade com o Gerenciador de Testes — Visual Studio (Windows)
2.9 Teste de fuzzing
Resumo O teste de fuzzing (também conhecido como teste de fuzz) é uma técnica de teste de software automatizado que envolve o fornecimento de dados inválidos, inesperados ou aleatórios como entrada de dados para um programa. Em seguida, o programa é monitorado quanto a exceções como panes, falhas integradas ou de asserções de código injetadas no compilador e possíveis vazamentos de memória.
Diretrizes
Use o teste de fuzzing em todos os softwares que possam processar entradas de dados não confiáveis que um invasor possa controlar. Se estiver compilando um novo aplicativo e o conjunto de testes associado a ele, inclua o teste de fuzzing para os módulos importantes o mais cedo possível. A execução do teste de fuzzing em um software pela primeira vez quase sempre revela vulnerabilidades reais desconhecidas anteriormente. Quando começa com os testes de fuzzing, você não para nunca mais.
Relação com outras seções
Quando reporta uma falha, o teste de fuzzing sempre fornece, naturalmente, um caso de teste reproduzível que demonstra o bug. Esse caso de teste pode ser reproduzido, resolvido e adicionado aos Casos de Teste Históricos.
Quando usar dois antissépticos, como o Antisséptico de Endereços (ASan) e o fuzzing:
- Execute primeiro os testes normais com os antissépticos habilitados para ver se existem problemas e, assim que o código estiver assepticamente limpo, comece o teste de fuzzing.
- Para C ou C++, existem compiladores que automatizam a injeção de asserções de runtime e metadados que habilitam o ASan. Quando compilados para o ASan, os binários resultantes se vinculam a uma biblioteca de runtime que pode diagnosticar com precisão mais de 15 categorias de erros de segurança de memória sem nenhum falso positivo. Para C ou C++, sempre que você tiver um código-fonte, use o LibFuzzer, que requer que o ASan seja habilitado primeiro.
- Para bibliotecas escritas em Java, C#, Python, Rust etc., use a estrutura AFL++.
Principais qualidades
- O teste de fuzzing encontra vulnerabilidades que a análise estática do programa, testes de recursos exaustivos e a inspeção manual do código muitas vezes deixam passar.
- O teste de fuzzing é uma maneira eficaz de encontrar bugs de segurança e confiabilidade no software, tanto assim que o Microsoft Security Development Lifecycle requer o teste de fuzzing em cada interface não confiável de cada produto (confira também a Modelagem de Ameaças).
- Sempre use o teste de fuzzing para softwares que possam processar entradas de dados não confiáveis.
- O teste de fuzzing é eficaz para aplicativos autônomos com grandes analisadores de dados.
CI/CD do Azure e do GitHub
Modifique seus builds para torná-los compatíveis com a criação contínua de executáveis que usam o LibFuzzer ou o AFL++. Você pode adicionar os recursos extras de computação necessários para fazer o teste de fuzzing em serviços como o OSS-Fuzz ou o OneFuzz.
2.10 Verificação de Aplicativos Web
Resumo
Dentro do escopo do Microsoft Visual C++ no Windows, a Microsoft recomenda:
- Prefira o TypeScript, JavaScript e ASP.NET para aplicativos web.
- Não escreva extensões web em C++. A Microsoft preteriu o ActiveX.
- Quando o código é compilado para Emscripten/WASM, deixa de ser C++ e outras ferramentas se aplicam.
- A Microsoft fornece o RESTler, um aplicador de testes de fuzzing em APIs REST com estado.
Visão geral e principais qualidades
Um verificador de aplicativos web explora um aplicativo web rastreando suas páginas da web e o examina para detectar vulnerabilidades de segurança. Esse rastreamento envolve a geração automática de entradas de dados mal-intencionadas e a avaliação das respostas do aplicativo. Criticamente, a verificação de aplicativos web deve abranger/dar suporte ao seguinte:
- Catalogar todos os aplicativos web na sua rede, incluindo os novos e desconhecidos, e ser capaz de se ampliar para passar de alguns poucos para milhares de aplicativos.
- Realizar uma verificação profunda de versões de software, serviços de SOAP e REST API e APIs usadas por dispositivos móveis.
- Inserir primitivos de segurança no desenvolvimento e implantação de aplicativos em ambientes de DevOps. Esses primitivos trabalham junto com o rastreador.
- Detecção de malware.
2.11 Verificar componentes de software incluídos
Resumo
Aborde seu código em C++ da mesma forma que o código escrito em outras linguagens de programação e aplique quaisquer ferramentas de Análise de Composição de Software (SCA) e Análise de Origem (OA ) adotadas por sua empresa para seu código em C++. Os fluxos de trabalho e verificações de segurança devem ser projetados como parte dos sistemas de CI/CD (integração contínua e entrega contínua).
Defesa upstream
Para mitigar o risco de ataques a dependências upstream, os códigos-fonte/componentes de terceiros devem ser armazenados em um ativo controlado pela empresa, no qual sejam executadas as ferramentas de SCA e OA.
- As ferramentas devem verificar e alertar quando vulnerabilidades são identificadas (incluindo bancos de dados públicos), como, por exemplo: Página Inicial | CVE
- Execute a análise estática em todos os componentes de software incluídos em seu aplicativo/repositório para identificar padrões de código vulneráveis.
Defesa de dependências
Execute e mantenha uma auditoria de dependências para validar que todas essas ocorrências sejam levadas em conta e cobertas por suas ferramentas de SCA e OA.
- Os componentes devem ser auditados regularmente e atualizados para as versões verificadas mais recentes.
- Dependências do feed de pacotes.
- As ferramentas de SCA/OA abrangem e auditam todas as dependências de pacote provenientes de um único feed.
SBOM
Produza uma SBOM (lista de materiais de software) com seu produto listando todas as dependências, tais como:
- origem (por exemplo, URL (Uniform Resource Locator))
- version
- consistência (por exemplo, hash SHA-256 do código-fonte) e outros meios para validar a consistência, como builds determinísticos.
- Exigir e auditar arquivos de SBOM nas dependências do software ou produzidos como parte de um build, incluindo OSS (software de código aberto).
- A Microsoft está padronizando e recomenda o SPDX (Intercâmbio de Dados de Pacotes de Software) versão 2.2 ou posterior | Linux Foundation como formato do documento da SBOM.
- O determinismo de build pode ser usado para produzir binários idênticos em termos de bits de forma independente e fornecer verificações independentes de integridade:
- Atestado de reprodutibilidade de terceiros ou do próprio fornecedor
- Outras técnicas, como a assinatura de binários por meio de uma fonte de certificado confiável, também podem fornecer algumas garantias de integridade de binários.
Recursos adicionais
As soluções da Microsoft incluem as seguintes diretrizes e produtos:
- Microsoft Supply Chain Platform | Microsoft
- Proteger sua cadeia de fornecimento de software | Segurança do GitHub
- vcpkg — os registros privados do vcpkg permitem o redirecionamento da aquisição de OSS para recursos controlados pela empresa para aquisição de códigos-fonte para uma dependência, de modo a minimizar o risco de ataques upstream ou over-the-wire (por meio de uma conexão de rede).