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 (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 OWASP1 considerou este tipo de exploração o quarto mais popular 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:
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 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, 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 ou TcpDump, ou simplesmente com as ferramentas de desenvolvimento dos navegadores Web (ex: 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 real2, como o exemplo abaixo (Opsview, Ltd / CC BY-SA):
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:
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, JMeter, k6, Gatling, 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.
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:
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:
express-rate-limit / express-rate-limit
Basic rate-limiting middleware for the Express web server
express-rate-limit
Basic rate-limiting middleware for Express. Use to limit repeated requests to public APIs and/or endpoints such as password reset Plays nice with express-slow-down and ratelimit-header-parser.
Usage
The full documentation is available on-line.
import { rateLimit } from 'express-rate-limit'
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
limit: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes).
standardHeaders: 'draft-7', // draft-6: `RateLimit-*` headers; draft-7: combined `RateLimit` header
legacyHeaders: false, // Disable the `X-RateLimit-*` headers.
// store: ... , // Redis, Memcached, etc. See below.
})
// Apply the rate limiting middleware to all requests.
app.use(limiter)
Data Stores
The rate limiter comes with a built-in memory store, and supports a variety of external data stores.
Configuration
All function options may be async…
Através desta biblioteca, podemos configurar facilmente um middleware para o Express framework, conforme a própria documentação:
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:
AdamPflug / express-brute
Brute-force protection middleware for express routes by rate limiting incoming requests
express-brute
A brute-force protection middleware for express routes that rate-limits incoming requests, increasing the delay with each request in a fibonacci-like sequence.
Installation
via npm:
$ npm install express-brute
A Simple Example
var ExpressBrute = require('express-brute');
// stores state locally, don't use this in production
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!');
}
);
Classes
ExpressBrute(store, options)
-
store
An instance ofExpressBrute.MemoryStore
or some other ExpressBrute store (see a list of known stores below). -
options
-
freeRetries
The number of retries the user has before they need to start waiting (default: 2) -
minWait
The initial wait time (in…
-
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
- Serverless Framework API Gateway Throttling
- Koa Rate Limit
- Spring Boot Throttling
- ASP.NET Core Rate Limit
- Rack Attack for Rails
⚠️ Importante ⚠️
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 ou memcached).
Conclusão
- Monitore constantemente sua aplicação e seus servidores;
- 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.
-
A OWASP é 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! ↩
-
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. ↩
Top comments (0)