[Esta tradução foi enriquecida com links e novas referências pela equipe do InfoQ Brasil]
A maneira mais eficiente de se prevenir falhas de segurança, em termos de custos, é tratar os requisitos de segurança logo nas primeiras fases do desenvolvimento de software. Em geral, tais requisitos podem ser classificados como Requisitos não- funcionais (NFRs, Non-Functional Requirements). Como muitos profissionais já descobriram, tratar requisitos de segurança e outros requisitos não-funcionais em projetos ágeis é um grande desafio, especialmente por duas razões:
- Não é simples o mapeamento de requisitos não-funcionais para histórias de usuário voltadas a funcionalidades;
- Os controles de segurança sofrem pela falta de visibilidade. Existe uma tendência nos processos ágeis de se privilegiar o desenvolvimento de funcionalidades que melhorem a experiência do cliente ou o tratamento de defeitos encontrados.
Examinaremos ambas as questões neste artigo.
Tratando NFRs em histórias de usuário
Vários especialistas em Agile sugerem métodos para a definição de requisitos não funcionais em processos de desenvolvimento baseados em histórias de usuário. Mike Cohn escreveu sobre o assunto e obteve várias sugestões interessantes nos comentários. Scott Ambler escreveu vários artigos sobre o tema nos quais argumenta que que nem todo NFR pode ser auto-contido em uma história de usuário. Dean Leffingwell apresenta uma abordagem mais profunda sobre o assunto, dedicando um capítulo completo aos requisitos não funcionais em seu livro Agile Software Requirements. A maioria dos especialistas concorda que os NFRs podem ser divididos em dois grandes grupos:
- Histórias de usuário não funcionais: São blocos de funcionalidades testáveis escritos como histórias de usuário. Os atores nessas histórias podem ser o pessoal interno de TI. Por exemplo: "Como um analista de segurança, quero que o sistema barre sucessivas tentativas mal sucedidas de autenticação, para evitar que a aplicação esteja vulnerável a ataques de força bruta".
- Restrições: São questões transversais que podem ter efeito sobre várias outras histórias de usuário. Tratam-se de uma espécie de "tributo" cobrado sobre os esforços de desenvolvimento. Exigir de todos os desenvolvedores a validação de informações provenientes de campos de formulários HTTP em aplicações web é um exemplo de restrição.
Lidar com a primeira categoria é simples e direto; basta criar um conjunto de histórias de requisitos não funcionais e adicioná-las à fila de itens a serem desenvolvidos, como por exemplo um backlog. Lidar com a segunda categoria é consideravelmente mais difícil.
Desafios com restrições de requisitos não funcionais
Alguns agilistas propuseram algumas soluções para o trabalho com restrições, incluindo:
- Adicionar as devidas restrições como critérios de aceite em algumas histórias de usuário específicas;
- Manter uma lista de todas as restrições em um repositório central, como um documento afixado de forma visível em uma parede ou numa wiki.
Nenhuma das abordagens oferece um mecanismo para selecionar quais restrições podem ser aplicadas a quais histórias. Nossa experiência no mapeamento de requisitos de segurança na SD Elements indica que essas técnicas tendem a ser difícies de escalar. Considere, por exemplo, o subconjunto de requisitos de segurança para uma aplicação web típica:
- Vincular variáveis em declarações SQL para prevenir injeção de SQL (SQL injection);
- Verificar a integridade de informações somente-leitura fornecidas pelo cliente com o intuito de prevenir a manipulação de parâmetros;
- Fazer o "escape"de informações não-confiáveis no HTML, nos atributos HTML, nas folhas de estilo CSS e em código JavaScript, para prevenir o Cross Site Scripting (XSS);
- Evitar XSS sobre DOM no JavaScript do lado do cliente;
- Utilizar aritmética segura para evitar estouro de inteiros;
- Desabilitar redirecionamentos externos para evitar redirecionamentos abertos;
- Autorizar páginas protegidas para prevenir a escalada de privilégios;
- Utilizar tokens contra a "Falsificação de solicitação entre sites" (CSRF - Cross Site Request Forgery);
- Validar a entrada de dados;
- Utilizar expressões regulares não vulneráveis a ataques de negação de serviço;
- Implementar autenticação transacional para transações de alto valor;
- Não gravar senhas no código ou em arquivos de configuração.
Estas provavelmente representam menos da metade das restrições que os desenvolvedores precisam considerar ao implementar uma história de usuário. E o conjunto de restrições aumenta quando o ambiente tiver que ser aderente a padrões como o Padrão de Segurança da Informação da Indústria do Pagamento por Cartões de Crédito (PCI DSS). Caso sejam adicionadas outras restrições de requisitos não funcionais (NFRs), como acessibilidade, a lista de restrições pode crescer rapidamente. Quando a lista começa a crescer sem controle, nossa experiência é que os desenvolvedores passam a ignorá-la completamente, apoiando-se apenas em suas memórias para aplicar as restrições em requisitos não funcionais.
Como o número de NFRs continua crescendo indefinidamente em domínios especializados como segurança de aplicação, o peso cognitivo sobre a memória dos desenvolvedores é imenso. Portanto, existe uma ênfase muito grande em tecnologias de análise estática para detectar restrições de NFRs no código. Estudos indicam que a análise estática pode detectar por volta de 40% dos defeitos possíveis de se prevenir. Isso ainda leva a uma grande quantidade de restrições que escapam da detecção da análise estática, incluindo controles de segurança específicos de domínio. Sem contar o desafio de buscar falsos positivos gerados pela análise estática sem customização.
Tratando desafios de restrições de NFRs
Para resolver o problema do excesso de restrições de NFRs, precisamos tratar quatro pontos principais:
- Priorização: Assim como defeitos e histórias de usuários, as restrições de requisitos não funcionais devem ter prioridades diferentes. Por exemplo, tratar ou validar informações pouco confiáveis nos cabeçalhos da resposta HTTP, para prevenir o HTTP Response Splitting (divisão da resposta HTTP) não é tão importante como tratar dados pouco confiáveis provenientes do HTML para evitar Cross Site Scripting. Deve-se partir do princípio que os desenvolvedores raramente terão tempo para tratar cada restrição, e oferecer um mecanismo para sugerir prioridades relativas entre as restrições em questão.
Dizemos "sugerir", pois as prioridades podem mudar dependendo do contexto e os desenvolvedores precisam decidir quais são as prioridades relativas em seus códigos específicos. Pode-se escolher usar prioridades simples como "Alta, Média e Baixa" ou uma escala numérica, como de 1 a 10.
- Filtragem: Utilizando critérios simples, pode-se frequentemente reduzir ou remover grandes quantidades de restrições de NFRs para uma história de usuário específica. Por exemplo, suponha que se esteja trabalhando em uma história de usuário que não tenha influência sobre a camada de visualização. Pode-se, com segurança, ignorar um muitas restrições particulares da camada de apresentação. Utilizando-se tags ou mesmo um filtro simples do Excel, pode-se cortar o número total de restrições para uma história em particular. Abaixo estão alguns exemplos de filtros de segurança relevantes para aplicações web:
- A história prevê a entrada de dados pelo usuário?
- A história envolve o uso de informações confidenciais, tais como senhas, números de cartões de crédito ou informações não públicas?
- A resultado da história é uma saída como uma página web, gerada ou alterada pelo usuário?
- Há interação com banco de dados?
- A história envolve a utilização de chamadas a APIs RESTful ou SOAP, seja para consumo, seja para exposição de informações?
- Há consumo de informações protegidas (ex.: informação que nem todos os usuários deveriam poder ver/modificar)?
- Contexto: É mais fácil para os desenvolvedores lembrar-se de aplicar as restrições quando estão codificando ou definindo histórias de usuário. Seria ideal que pudessem ter acesso à lista de restrições dentro do próprio IDE, ou em algum sistema de tarefas, para acesso às restrições durante a codificação. Muitos IDEs têm a funcionalidade de navegadores web embutidos; assim, a utilização de uma página web leve ou um site SharePoint podem ajudar a atingir este objetivo.
- Framework: Frameworks podem reduzir bastante o "tributo" cobrado pelas restrições. Por exemplo, um framework como Django, que já inclui proteção contra CSRF, torna possível aos desenvolvedores ignorar uma restrição, exceto se ativamente desligarem a proteção fornecida. Em nossa experiência, as maiores aplicações têm algum framework customizado que requer investimento muito alto no início do projeto. Mas caso as restrições sejam entendidas como uma espécie de "tributo" cobrado a cada história de usuário desenvolvida, a redução do número de restrições é semelhante à redução permanente da alíquota de um tributo.
Validação
O tratamento proativo das NFRs é importante, mas não dispensa validações posteriores. Caso se possua a prática de revisão manual do código, os revisores podem prestar atenção a requisitos não funcionais listados explicitamente como critérios de aceite nas histórias. Muitas restrições, contudo, podem ser traiçoieras, ou trabalhosas para serem encontradas por meio de revisões manuais de código apenas. Ferramentas de análise estática de código verificam a aderência a padrões. Alguns produtos têm segurança como foco exclusivo.
Uma coisa que pode ajudar a manter a consistência na adesão a padrões de código é a configuração de um processo de integração contínua para executar ferramentas de análise estática de código. Mas não devemos nos apoiar apenas na revisão de código - os requisitos não funcionais contra vulnerabilidades específicas de domínio, como manipulação de parâmetros HTTP, exigem o entendimento do domínio de negócio subjacente. Uma combinação de testes de regressão manuais e automáticos, que validem cenários específicos de ataques, pode ajudar a construir um sistema resiliente a vulnerabilidades específicas ao domínio.
Visibilidade
Outro fator que inibe a integração de segurança nos projetos ágeis é a falta de visibilidade. Cerimônias com os clientes, tais como Revisões de Sprint do Scrum, favorecem o trabalho com histórias de usuário ou correção de defeitos que melhorem a experiência do usuário. Alguns atributos não funcionais, como usabilidade e desempenho, possuem efeito tangível sobre a experiência do usuário e podem ser facilmente demonstradas. Outras, como segurança, raramente resultam em efeitos positivos sobre a experiência do cliente em uma Revisão de Sprint.
Na verdade, algumas funcionalidades de segurança, como o travamento de contas em caso de uso indevido, têm efeito negativo na experiência do usuário. A maioria das funcionalidades de segurança não tem impacto perceptível sobre os usuários comuns. Portanto, NFRs de segurança são invisíveis e precisam competir, com funcionalidades mais visíveis, por ciclos preciosos de desenvolvimento nas iterações. O custo de oportunidade de se trabalhar em requisitos invisíveis é particularmente alto nos estágios iniciais do desenvolvimento da aplicação, exatamente quando os desenvolvedores deveriam embutir a segurança no sistema.
Tornando a segurança visível
Não existem balas de prata que tornem a segurança uma prioridade para uma equipe ágil no começo do desenvolvimento, mas há duas maneiras de tornar NFRs de segurança visíveis para os clientes e/ou product owners:
- Criar gráficos para histórias de segurança: Caso se tenha conhecimento suficiente sobre segurança de aplicações, pode-se produzir um conjunto de controles de segurança para vulnerabilidades conhecidas. Pode-se então utilizar os controles que mapeiam para histórias de usuário de NFRs (em vez de restrições) e criar um gráfico simples ilustrando as histórias de segurança implementadas e não implementadas no sistema. Esse gráfico seria então afixado no quadro junto com os demais grandes gráficos visíveis (veja a figura abaixo).
Gráfico de histórias de segurança
Esse gráfico, claro, não precisa estar restrito às histórias de segurança. Pode-se escolher outros requisitos não funcionais importantes para exibir no mesmo gráfico, além de utilizar pontos de histórias no lugar de número de histórias para refletir o esforço. Pode-se mostrar gráficos apenas para históricas de alta prioridade e para controles de aspectos de segurança de alto risco. Toda história implementada ajudará a aumentar a contagem de "Implementado" e consequentemente contribuirá para tornar o progresso mais visível para o cliente e/ou dono do produto.
- Demonstrar fragilidades: Nada leva as pessoas a entender bem as implicações de uma vulnerabilidade de segurança quanto demonstrar uma fragilidade na prática. Essa é a diferença fundamental entre um teste de invasão de segurança e uma avaliação de vulnerabilidade. Sempre que o esforço se justificar, demonstre a exploração de uma vulnerabilidade em uma versão anterior do software e depois mostre como a nova versão se tornou imune a essa fragilidade. Assim se pode explicar com mais facilidade as implicações da vulnerabilidade em termos de impacto para o negócio ou os clientes. Isso pode ajudar enormemente a justificar o tempo gasto em controles de segurança.
Conclusões
Tratar proativamente dos requisitos não funcionais, e os de segurança em particular, em um contexto ágil não é nada simples. Vários especialistas oferecem conselhos sobre como se deve lidar com tais requisitos, particularmente como se relacionam com a criação de histórias de usuário. Mesmo que não seja possível tratar todos os pontos apresentados nesse artigo, qualquer melhoria individual pode gerar grandes benefícios no longo prazo. Adicionar validações oferece a segurança de que sua equipe de desenvolvimento atende aos requisitos não funcionais. E tornar visíveis as questões de segurança permite justificar o tempo gasto nas melhorias do sistema que não impactam diretamente a experiência do usuário.