O ModSecurity com o conjunto de regras OWASP CRS liga-se em dez minutos e desliga-se três dias depois, quando os artigos deixam de gravar na administração, o carregamento de ficheiros se parte e um cliente não consegue fazer uma encomenda porque a morada tinha um apóstrofo. A conclusão «o WAF atrapalha o trabalho» surge sozinha, e é falsa: quase todos esses bloqueios se resolvem com três ou quatro exceções precisas, e tudo está em encontrá-las corretamente.

Não ligue já o bloqueio

A primeira semana é só de observação. Em /etc/modsecurity/modsecurity.conf:

SecRuleEngine DetectionOnly

Neste modo o WAF regista tudo o que teria bloqueado e não bloqueia nada. Uma semana de tráfego real — incluindo o seu próprio trabalho na administração, o carregamento de imagens e uma encomenda — dá a lista dos falsos positivos verdadeiros em vez de hipotéticos. Passar a On só faz sentido quando essa lista estiver tratada.

Como funciona o CRS: não uma regra, mas uma soma

É a chave de tudo o que se segue. O CRS quase nunca bloqueia um pedido com uma única regra. Cada regra que dispara acrescenta pontos de anomalia, e o bloqueio ocorre quando a soma ultrapassa um limiar. Por isso no registo verá não uma linha mas várias, e a última será a regra com o identificador 949110, a que faz o total.

Consequência prática: a exceção tem de ser para a regra que marcou os pontos, não para a 949110. Ao desligar a regra que soma, desliga o conjunto inteiro, e do WAF fica apenas uma linha num ficheiro de configuração.

A segunda consequência é o nível de paranoia. Está em um por omissão, e essa é a escolha certa. Os níveis 2 e 3 acrescentam regras que, por construção, produzem falsos positivos em sítios vulgares; ativá-los só faz sentido quando o nível um estiver totalmente afinado.

Encontrar a regra culpada

Tudo o que é preciso está no registo de auditoria (/var/log/modsec_audit.log) e no registo de erros do servidor web. Procure pela hora do bloqueio:

sudo grep -o 'id "[0-9]*"' /var/log/modsec_audit.log | sort | uniq -c | sort -rn | head

É uma lista de frequência das regras disparadas. Depois, para um identificador concreto, veja com que exatamente disparou:

sudo grep -A5 'id "942100"' /var/log/modsec_audit.log | head -40

São precisas três coisas: o identificador da regra, o nome do parâmetro (ARGS:content, ARGS:comment) e o caminho do pedido. É com isso que se constrói a exceção.

Os suspeitos do costume

A lista repete-se de sítio para sítio:

  • 942100: injeção de SQL. Dispara em campos de texto com conteúdo longo: corpo de artigo, descrição de produto, comentário. Aspas, parênteses e palavras como select parecem suspeitos ao detetor mesmo em prosa normal;
  • 941100 e a família 941xxx: XSS. Chegam com o editor visual: as etiquetas HTML num campo são precisamente o sentido do seu trabalho;
  • 920420: Content-Type não permitido. Parte APIs e carregamentos de ficheiros: o conjunto de tipos permitidos por omissão é estreito, e em versões antigas application/json não estava lá;
  • 913100: verificador pelo User-Agent. Juntamente com os verificadores apanha ferramentas legítimas: a monitorização de disponibilidade, o curl nos seus próprios scripts;
  • 200002, 200004: erros na análise do corpo do pedido. Costumam significar não um ataque mas um limite de tamanho excedido, ou seja, o carregamento de um ficheiro grande.

Três formas de criar uma exceção

Por ordem crescente de rudeza. Todas vão para um ficheiro próprio (por exemplo /etc/modsecurity/crs/REQUEST-900-EXCLUSION-RULES.conf) e não para os ficheiros do CRS: o conjunto de regras é atualizado e as suas alterações desapareceriam com ele.

Retirar um parâmetro de uma regra. A variante mais precisa, e a que se deve procurar:

SecRuleUpdateTargetById 942100 "!ARGS:content"

Desligar uma regra apenas num caminho. Serve quando é uma página concreta a fazer barulho: um editor, uma importação, um formulário de avaliação:

SecRule REQUEST_URI "@beginsWith /admin/post" \
    "id:1001,phase:1,pass,nolog,ctl:ruleRemoveById=942100"

Desligar a regra por completo. Último recurso e quase sempre sinal de que a causa não foi encontrada:

SecRuleRemoveById 942100

A diferença entre a primeira e a terceira é considerável. No primeiro caso um campo — o texto do artigo — deixa de ser verificado contra injeção de SQL por uma regra; no segundo e no terceiro deixa de ser verificado o sítio inteiro. A diferença de esforço é de cerca de cinco minutos.

Depois das alterações, verifique a configuração e recarregue com suavidade:

sudo apachectl configtest && sudo systemctl reload apache2
sudo nginx -t && sudo systemctl reload nginx

Uma ordem que poupa uma semana

  1. Uma semana em DetectionOnly com trabalho normal no sítio, administração e carregamentos incluídos.
  2. Lista de frequência das regras a partir do registo de auditoria. Percorre-se de cima para baixo: as três ou quatro primeiras somam noventa por cento do ruído.
  3. Para cada uma: apurar que parâmetro e em que página. A exceção faz-se por parâmetro, não por regra.
  4. Só agora SecRuleEngine On.
  5. Uma vez por mês, espreitar os bloqueios: o sítio mudou, logo apareceram novos falsos positivos.

É neste último ponto que tudo costuma partir-se: o registo de auditoria são gigabytes de texto que ninguém lerá à mão. O sentido de um painel é ter à vista a lista de regras disparadas, os pedidos bloqueados e o conjunto ativo, em vez de os extrair com grep. Como isso fica mostra-o a página de demonstração abaixo.