DEV Community

Bernardo Bosak de Rezende
Bernardo Bosak de Rezende

Posted on

DevSecOps - DoS - Paginação de APIs (Parte 3)

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 paginações de recursos de APIs.

Se você não leu as duas partes anteriores da série, recomendo sua leitura antes de continuar, pois ali são explicados alguns conceitos importantes, como o próprio DoS e log de acessos:

É comum, hoje em dia, que os sistemas precisem fornecer muitos registros de seus bancos de dados para que aplicações cliente consumam estas informações, seja exibindo em uma UI ou até mesmo realizando outros fluxos de operações com as informações obtidas. Seria impraticável, por vários motivos (latência, banda, tempo de resposta e, principalmente, experiência de usuário), fornecer todos os dados de uma tabela (muitas vezes gigantesca) de uma só vez. Como solução, os sistemas entregam seus dados em "lotes" pequenos. Estes lotes são chamados de páginas (de registros), e esse processo é chamado de paginação.

Ok, mas qual a relação disso com DoS (Denial of Service)?

Exemplo de como derrubar uma aplicação por ausência ou má configuração de paginação

Digamos que, um sistema de controle áudio visual, armazene informações sobre quais músicas estão disponíveis para serem ouvidas, e que você precisa disponibilizar estes dados para que várias aplicações possam tratá-los (ex: uma aplicação web, um aplicativo, uma planilha Excel com scripts VBA). Esta "exposição" dos dados pode ser feita de diversas formas (ex: SOAP, REST, protocolos binários, arquivos, etc), mas, como de costume, utilizarei um padrão forte de mercado que são as Web APIs modeladas em REST1:

GET /api/songs

Na prática, isso significa que as aplicações cliente podem dizer "sistema de controle áudio visual, me disponibilize todas as músicas em sua base de dados". Se a quantidade de músicas for gigantesca (dezenas de milhões de registros), você consegue imaginar o problema para transferir estes dados, correto? Seus servidores podem ficar sem memória, a operação levaria um tempo impraticável que degradaria qualquer experiência de uso, ou seria necessário uma arquitetura de streaming de dados para transferir tanta informação de forma robusta.

Por estes motivos, nestas comunicações de software entre diferentes camadas físicas, é comum que os registros sejam disponibilizados de forma paginada. Ou seja, fornecemos os primeiros X elementos, depois mais X e assim por diante. Desta forma, a modelagem da nossa API de músicas mudou um pouco:

GET /api/songs?limit=20&offset=0

No endpoint acima, offset representa a quantidade de registros que desejamos pular antes de começar a consultá-los, e limit é a quantidade máxima de registros que serão consultados. Neste exemplo, o valor 0 é passado para offset e 20 para limit, ou seja: desejamos obter, no máximo, as 20 primeiras canções do banco de dados (critério de ordenação natural, pois nada foi especificado). Se quisermos os "próximos 20", basta trocarmos offset para 20 e continuar com o mesmo valor de limit. Se quisermos a "terceira página de 20 registros", offset recebe 40 e assim por diante. Também é comum que essa técnica de paginação utilize os termos skip (invés de offset) e take (invés de limit).

Outra alternativa é utilizar diretamente o conceito de página, onde informaríamos o número da página e o tamanho da mesma (20 primeiros registros = page 1 e pageSize 20).

Mas, independente da técnica de paginação, o importante é que você esteja ciente da importância do uso de paginação para a experiência de uso do software, para sua performance e... para a sua segurança também!

Isso mesmo, as paginações podem ser utilizadas para instabilizar seu sistema (um dos objetivos do DoS), pois se o código que realiza a paginação não controla o tamanho das páginas, você está exposto a receber tamanhos gigantescos de página e ter seu serviço instabilizado!

Vejamos, uma tentativa de ataque pode tentar realizar grandes quantidades da seguinte requisição:

GET /api/songs?limit=10000000&offset=0

Caso não exista uma "limitação" para o valor informado em limit, não existe ganho prático de fazer paginação. Isso é ruim para a performance e também expõe uma brecha para instabilidade, pois é possível consultar páginas com grandes quantidades de dados.

Solução

A solução é simples: estabelecer um limite seguro para o valor máximo que pode ser informado em limit (ou pageSize, take, etc).

Isso pode ser feito com uma verificação dos valores passados por parâmetro:

DEFAULT_LIMIT = 20
MAX_LIMIT = 100

limit = request.args.get('limit', DEFAULT_LIMIT)
if int(limit) > MAX_LIMIT:
    abort(400)
# query database with pagination parameters

Preferencialmente, centralize este controle de forma que todos os seus endpoints paginados realizem esta validação com facilidade e sem duplicação de código.

Por exemplo, podemos utilizar Python Decorators:

@app.route('/api/songs')
@ensure_safe_limit()
def get_songs():
    # normally here we'd call a database paginated query
    return jsonify([{'name': 'Respect', 'by': 'Aretha Franklin'},
                   {'name': 'Um dia frio', 'by': 'Djavan'}])

O decorator @ensure_safe_limit garante que nossa página estará segura de limites muito altos. Basta aplicá-lo em todos os endpoints paginados. O mesmo pode ser feito com Higher-Order Functions e com middlewares. Procure a documentação do seu framework ou plataforma de desenvolvimento!

A solução completa pode ser conferida no repl.it abaixo. A implementação do decorator está em utils.py.

Caso o limite "único" que você estabelece em todo seu software não atenda algumas situações específicas, aumente-o separadamente, sempre com bastante cautela e testes. Outra opção é você estabelecer estes limites por perfis de usuários do seu sistema. Por exemplo, usuários administradores possuem um limite de 1000 registros por páginas, usuários "visualizadores" possuem um limite de 50, pois realizam esta operação com muito mais frequência, etc.

Conclusão

É possível ver a importância de compor a proteção do seu software contra DoS em várias técnicas diferentes, pois além de controlar a quantidade de recursos por página, também é preciso controlar o throughput (quantidade de requisições por intervalo de tempo) com que isso é solicitado, conforme abordado na parte 1 desta série.

  • Caso seu software permitir que terceiros informem a quantidade de registros "por página", estabeleça um limite seguro dentro do seu contexto de aplicação. Adote isso como padrão de paginação.
  • Faça as devidas mensurações para encontrar um tamanho limite ideal para atender seus usuários sem expor seu sistema.
  • Conforme já dito, desenvolva uma visão "caixa preta" e analise a sua API com uma perspectiva externa, sem detalhes de implementação. A paginação é um possível ponto de brecha, mas existem outros. Por onde você pode estar vulnerável?

  1. O exemplo dado em REST não anula ou inviabiliza o fato de que todo tipo de integração de dados, com a opção de paginar os recursos transferidos, tenha o mesmo tipo de preocupação abordado neste post, contra a exploração de grandes paginações para fazer a negação de serviço. Sempre questione a interface de programação que você está disponibilizando para terceiros! 

Top comments (0)