DevSecOps - DoS - Throughput e Rate Limit (Parte 1)
Tags: security, devops, webdev
Published: 2020-06-08 23:24:39

Este post faz parte de uma coleção de posts sobre segurança de software ao longo do processo de desenvolvimento e operação (DevSecOps). O tipo de vulnerabilidade abordado neste artigo é **negação de serviço** através da exploração de alto volume de requisições (throughput).

Negação de serviço, ou [DoS](https://owasp.org/www-community/attacks/Denial_of_Service) (a.k.a. DDoS), consiste em uma série de técnicas de exploração que **instabilizam** ou até mesmo **indisponibilizam** um serviço, visando encontrar brechas de segurança ao longo da manutenção da instabilidade, retirá-lo do ar ou simplesmente fazer com que o seu custo de infraestrutura aumente consideravelmente, mesmo que o sistema não tenha sido impactado.

A OWASP[^1] considerou este tipo de exploração [o quarto mais popular](https://apisecurity.io/encyclopedia/content/owasp/api4-lack-of-resources-and-rate-limiting.htm) em termos de brechas de segurança para APIs, em 2019.

É possível tentar retirar um serviço do ar de diversas maneiras, e abordaremos como mitigar as três principais (mas neste artigo falaremos apenas da primeira):

**1) Sabendo lidar com altas taxas de requisições (_throughput_);**
2) Sabendo lidar com altas cargas em cada requisição (_payload_);
3) Limitando tamanho de paginações de recursos.

#### Como identificar um DoS

O primeiro passo é identificar uma situação de _DoS_. Geralmente fazemos isso analisando métricas de acesso ou logs de requisição.

Se você ainda não tem certeza da situação, comece analisando a quantidade de requisições que o seu sistema tem recebido nos últimos minutos / horas, preferencialmente procure pelo _throughput_ recente do sistema (ex: total de requisições por segundo ou requisições por minuto). Se existe uma tentativa de _DoS_, você perceberá um aumento considerável no número de requisições em pequenos intervalos de tempo. Por exemplo, se você estiver visualizando um gráfico, ele representaria um crescimento:

![Aumento no gráfico de requisições](https://user-images.githubusercontent.com/526075/83980914-b248ff80-a8ef-11ea-9b54-ae82b4e7c088.png)
<figcaption>Crescimento no gráfico de requisições recebidas pelo sistema</figcaption>

Aplicações como AWS CloudWatch, NewRelic, Datadog, Nginx Amplify possuem diversos destes tipos de gráficos. Geralmente eles são o primeiro indício de que algo está incomum.

Se você não tiver essas informações, procure nos logs de acesso de seus servidores. Comece analisando "de fora para dentro" (por exemplo, se a sua arquitetura tem um load balancer ou proxy reverso, comece por eles, e depois os servidores de aplicação e assim por diante).

Por exemplo, um [log de acesso normal do _nginx_](https://docs.nginx.com/nginx/admin-guide/monitoring/logging/) seria:

```
2020-06-05T10:14:02Z GET / 1.1.1.1 "Mozilla/5.0 (X11; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0"
2020-06-05T10:15:23Z GET /products 2.2.2.2 "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1"
2020-06-05T10:16:53Z POST /admin 3.3.3.3 "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:58.0) Gecko/20100101 Firefox/58.0"
2020-06-05T10:17:20Z GET / 4.4.4.4 "Mozilla/5.0 (Android 4.4.2; Tablet; rv:65.0) Gecko/65.0 Firefox/65.0"
2020-06-05T10:18:22Z GET / 5.5.5.5 "Mozilla/5.0 (Android 4.4.2; Tablet; rv:65.0) Gecko/65.0 Firefox/65.0"
2020-06-05T10:19:11Z GET /products 6.6.6.6 "Mozilla/5.0 (Linux; U; Android 5.0.2; en-US; XT1068 Build/LXB22.46-28) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.108 UCBrowser/12.10.2.1164 Mobile Safari/537.36"
```

Nesta situação, garanta que seus logs utilizam timestamp (data e hora da requisição), mas principalmente preocupe-se em identificar o IP de origem das requisições (ex: _1.1.1.1_, _2.2.2.2_, etc) e o [cabeçalho User Agent](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent), pois **quando essas duas informações se repetem por várias requisições, temos o principal sintoma de _DoS_**. As repetições podem seguir um padrão variável, mas muitas vezes elas serão constantes. Exemplo:

```
2020-06-05T10:14:02Z GET / 1.1.1.1 "Mozilla/5.0 (X11; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0"
2020-06-05T10:15:23Z GET /products 2.2.2.2 "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1"
2020-06-05T10:16:53Z POST /admin 3.3.3.3 "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:58.0) Gecko/20100101 Firefox/58.0"
2020-06-05T10:16:53Z POST /admin 3.3.3.3 "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:58.0) Gecko/20100101 Firefox/58.0"
2020-06-05T10:16:53Z POST /admin 3.3.3.3 "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:58.0) Gecko/20100101 Firefox/58.0"
2020-06-05T10:16:53Z POST /admin 3.3.3.3 "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:58.0) Gecko/20100101 Firefox/58.0"
2020-06-05T10:16:53Z POST /admin 3.3.3.3 "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:58.0) Gecko/20100101 Firefox/58.0"
2020-06-05T10:16:53Z POST /admin 3.3.3.3 "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:58.0) Gecko/20100101 Firefox/58.0"
2020-06-05T10:16:53Z POST /admin 3.3.3.3 "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:58.0) Gecko/20100101 Firefox/58.0"
2020-06-05T10:16:53Z POST /admin 3.3.3.3 "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:58.0) Gecko/20100101 Firefox/58.0"
2020-06-05T10:16:53Z POST /admin 3.3.3.3 "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:58.0) Gecko/20100101 Firefox/58.0"
2020-06-05T10:16:53Z POST /admin 3.3.3.3 "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:58.0) Gecko/20100101 Firefox/58.0"
2020-06-05T10:16:53Z POST /admin 3.3.3.3 "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:58.0) Gecko/20100101 Firefox/58.0"
2020-06-05T10:16:53Z POST /admin 3.3.3.3 "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:58.0) Gecko/20100101 Firefox/58.0"
2020-06-05T10:16:53Z POST /admin 3.3.3.3 "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:58.0) Gecko/20100101 Firefox/58.0"
2020-06-05T10:16:53Z POST /admin 3.3.3.3 "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:58.0) Gecko/20100101 Firefox/58.0"
2020-06-05T10:16:53Z POST /admin 3.3.3.3 "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:58.0) Gecko/20100101 Firefox/58.0"
2020-06-05T10:16:53Z POST /admin 3.3.3.3 "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:58.0) Gecko/20100101 Firefox/58.0"
2020-06-05T10:16:53Z POST /admin 3.3.3.3 "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:58.0) Gecko/20100101 Firefox/58.0"
2020-06-05T10:16:53Z POST /admin 3.3.3.3 "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:58.0) Gecko/20100101 Firefox/58.0"
2020-06-05T10:16:53Z POST /admin 3.3.3.3 "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:58.0) Gecko/20100101 Firefox/58.0"
2020-06-05T10:16:53Z POST /admin 3.3.3.3 "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:58.0) Gecko/20100101 Firefox/58.0"
2020-06-05T10:16:53Z POST /admin 3.3.3.3 "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:58.0) Gecko/20100101 Firefox/58.0"
2020-06-05T10:17:20Z GET / 4.4.4.4 "Mozilla/5.0 (Android 4.4.2; Tablet; rv:65.0) Gecko/65.0 Firefox/65.0"
2020-06-05T10:18:22Z GET / 5.5.5.5 "Mozilla/5.0 (Android 4.4.2; Tablet; rv:65.0) Gecko/65.0 Firefox/65.0"
2020-06-05T10:19:11Z GET /products 6.6.6.6 "Mozilla/5.0 (Linux; U; Android 5.0.2; en-US; XT1068 Build/LXB22.46-28) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.108 UCBrowser/12.10.2.1164 Mobile Safari/537.36"
```

O log acima demonstra um padrão repetitivo em excesso em um curto espaço de tempo (menos de um segundo), em requisições com o verbo _POST_ na rota _/admin_ (ex: um ataque de força bruta ao tentar submeter o formulário de login várias vezes):

```
2020-06-05T10:16:53Z POST /admin 3.3.3.3 "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:58.0) Gecko/20100101 Firefox/58.0"
```

O que nos leva a crer que existe, sim, uma tentativa de _DoS_. É importante ressaltar que o mesmo ataque pode gerar inúmeras requisições de origens e user agents diferentes. Portanto, se você não encontrar um padrão para bloquear um conjunto específico de requisições, a melhor abordagem é intervalar o acesso ao seu sistema em uma janela de tempo, que veremos mais tarde.

Não deixe para configurar ferramentas em horas emergenciais, garanta que seu software possui um monitoramento adequado e que atende suas necessidades de manutenção de uma forma segura.

#### Exemplo de ataque por throughput

Um ataque mal-intencionado pode tentar reproduzir em alto volume qualquer requisição que seu sistema disponibilize, mesmo estando sob autenticação. Se o ataque possui acesso a fazer uma requisição, ele possui acesso a fazer 1 milhão, a menos que isso seja tratado, como veremos a seguir.

Uma pessoa com conhecimento não muito avançado de redes e desenvolvimento web conseguiria identificar como são enviadas as requisições para o seu sistema, através de analisadores de pacotes (bem usados para analisar tráfego de aplicativos), como [WireShark](https://www.wireshark.org/) ou [TcpDump](https://www.tcpdump.org/), ou simplesmente com as ferramentas de desenvolvimento dos navegadores Web (ex: [Chrome DevTools](https://developers.google.com/web/tools/chrome-devtools)).

Por exemplo, imagine que seu sistema possui um dashboard analítico, com várias métricas complexas e consultas de dados cruzados que demandam bastante recurso computacional para apresentá-los em tempo real[^2], como o exemplo abaixo ([Opsview, Ltd / CC BY-SA](http://creativecommons.org/licenses/by-sa/3.0/)):

![Opsview Monitor 6.0 Dashboard sample](https://upload.wikimedia.org/wikipedia/commons/8/8d/Opsview_Monitor_6.0_Dashboard.jpg)
<figcaption>Exemplo de dashboard com várias métricas</figcaption>

Suponhamos que, para atender a este dashboard, exista a seguinte API:

```
GET /api/dashboard
Authorization: token-do-usuario-id-1 
```

Uma vez que o ataque conheça a estrutura dessa requisição, e estando o token do usuário id-1 devidamente válido (em muitos sistemas, basta cadastrar um usuário para obter um token válido), é possível criar scripts que executem muitas vezes a requisição acima, afim de instabilizar o seu sistema:

```python
import requests

url = 'https://seuservico.com/api/dashboard'
headers = { 'Authorization': 'token-do-usuario-id-1' }

for i in range(1000000):
    requests.get(url, headers=headers)
```

Idealmente (do ponto de vista de programação), o código que dispara essas requisições mal-intencionadas deveria ser assíncrono e paralelo, mas isso está fora do escopo abordado aqui, o importante é conhecer o funcionamento desses scripts para conseguir evitá-los. Ainda seria possível utilizar frameworks de testes de carga, como [artillery](https://artillery.io/), [JMeter](https://jmeter.apache.org/), [k6](https://k6.io), [Gatling](https://gatling.io/), etc.

Existem dois tipos de solução bem comuns para isso lidar com altas taxas de requisições:

1) Bloquear a origem das requisições (ex: IP, user agent, sessão ou token); ou
2) Estabelecer um limite para a quantidade de requisições que uma ou mais origens podem realizar em um determinado período de tempo. Ex: permitir que o token do usuário id-1 execute apenas uma requisição por segundo. Essa abordagem é conhecida como [throttling](https://en.wikipedia.org/wiki/Throttling_process_(computing)).

Ambas alternativas podem ser resolvidas em nível de infraestrutura ou código de aplicação.

#### Resolvendo com infraestrutura

Resolver essa exploração com recursos de infraestrutura tem as vantagens de ser uma solução bem mais rápida (geralmente envolve configurar corretamente os servidores de firewall ou aplicação) e comprometer menos a arquitetura da solução. Bloquear ou intervalar requisições via software exige mais recursos computacionais de servidores que são provisionados para atender a aplicação, ao invés de realizar estas tarefas em servidores de firewall ou proxy reversos (como _nginx_), otimizados para esse tipo de responsabilidade.

Os pontos negativos ficam por conta do gargalo de conhecimento em infraestrutura que, infelizmente, muitas equipes enfrentam e do custo financeiro de algumas dessas soluções.

Já utilizei o _AWS Web Application Firewall_ e gostei bastante, inclusive existem concorrentes de players gigantes do mercado de [SRE](https://pt.wikipedia.org/wiki/Site_Reliability_Engineering):

* [AWS Web Application Firewall](https://aws.amazon.com/pt/waf/);
* [CloudFlare Web Application Firewall](https://www.cloudflare.com/pt-br/waf/);
* [Akamai Web Application Protector](https://www.akamai.com/br/pt/products/security/web-application-protector-enterprise-waf-firewall-ddos-protection.jsp)

Converse com os interessados pela arquitetura e infraestrutura do seu software, avalie serviços e soluções (em alguns casos configurar uma instância de proxy reverso é mais vantajoso) e monte um plano de ação para, de preferência, ter prevenção contra esse tipo de exploração.

#### Resolvendo com software

Podemos escrever pedaços de código que façam estas verificações anti-DoS (bloquear ou intervalar) e incluí-los **no início** do ciclo de vida das requisições no servidor de aplicação (os _endpoints_). Porém, muito provavelmente esse tipo de código já foi criado previamente e existe na stack de desenvolvimento que você utiliza, seja de forma incorporada (_built-in_) ou via código terceiro.

Por exemplo, para quem trabalha com o Express framework, existe uma biblioteca open-source chamada _express-rate-limit_:

{% github nfriedly/express-rate-limit %}

Através desta biblioteca, podemos configurar facilmente um _middleware_ para o Express framework, conforme a própria documentação:

```javascript
const rateLimit = require("express-rate-limit");

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100 // limit each IP to 100 requests per windowMs
});

//  apply to all requests
app.use(limiter);
```

De forma mais "automática", existem bibliotecas que fazem o bloqueio para você e aumentam o intervalo seguindo sequências, pré-determinadas, como a lib _express-brute_:

{% github AdamPflug/express-brute %}

```javascript
var ExpressBrute = require('express-brute');

// stores state locally, don't use this in production - VEJA NOTA ABAIXO -
var store = new ExpressBrute.MemoryStore();
var bruteforce = new ExpressBrute(store);

app.post('/auth',
	bruteforce.prevent, // error 429 if we hit this route too often
	function (req, res, next) {
		res.send('Success!');
	}
);
``` 

O mesmo pode ser feito em demais tecnologias e frameworks, consulte a documentação da stack que você está utilizando! Veja as principais:

* [Django REST Framework Throttling](https://www.django-rest-framework.org/api-guide/throttling/)
* [Serverless Framework API Gateway Throttling](https://github.com/DianaIonita/serverless-api-gateway-throttling)
* [Koa Rate Limit](https://github.com/koajs/ratelimit)
* [Spring Boot Throttling](https://github.com/shlomokoren/spring-boot-throttling)
* [ASP.NET Core Rate Limit](https://github.com/stefanprodan/AspNetCoreRateLimit)
* [Rack Attack for Rails](https://github.com/kickstarter/rack-attack)

:warning: **Importante** :warning:

Independente da sua escolha, **lembre-se que estas soluções devem funcionar bem em arquiteturas escaláveis**, então se você configurar uma destas bibliotecas no modo "in-memory", todo o bloqueio de requisições funcionará de forma separada em cada nodo da arquitetura, ou seja, **a requisição maliciosa X será bloqueada no servidor A mas não no servidor B**. Por este motivo, a grande maioria destas bibliotecas oferecem a forma de configurá-las com um armazenamento compartilhado das informações (geralmente utilizando [Redis](https://redis.io/) ou [memcached](https://memcached.org/)). 

#### Conclusão

1. Monitore constantemente sua aplicação e seus servidores;
2. Ajuste seu software e sua arquitetura para, a qualquer momento (faça simulações!), bloquear ou intervalar requisições.

Questione _"que ferramentas e técnicas a stack tecnológica me fornece para evitar DoS?"_. Então consulte as documentações e faça os devidos testes! Em termos de segurança de informação, a máxima **prevenir é melhor que remediar** vale muito.

[^1]: A [OWASP](https://owasp.org/) é uma comunidade muito valiosa em segurança de informação. É sempre muito importante estarmos atentos aos dados e relatórios periódicos que eles publicam!
[^2]: Para facilitar a didática e o exemplo, vamos assumir que o cálculo dessas métricas é feito em tempo real. Porém, muitos desses sistemas utilizam estruturas analíticas pré-calculadas.