DEV Community

Bernardo Bosak de Rezende
Bernardo Bosak de Rezende

Posted on • Edited on

DevSecOps - Ataque de alteração de parâmetros da requisição

Segurança para devs

Este post faz parte de uma coleção de posts sobre segurança de software ao longo do processo de desenvolvimento (DevSecOps). O tipo de vulnerabilidade abordado neste artigo é Alteração de parâmetros da requisição.

A tradução deste nome é livre, mas o termo mais comum, em inglês, é Parameter Tampering. Você pode conferir a documentação no catálogo da OWASP 1.

Basicamente, Parameter Tampering consiste em qualquer tipo de exploração de segurança baseada em alterar as informações que uma aplicação cliente envia para o servidor (ex: uma requisição REST ou um formulário HTML), afim de obter resultados diferentes, como acessar informações de outros usuários ou alterar informações que, normalmente por restrições de UI, não seria possível.

Alguns exemplos do que pode ser feito com esse tipo de exploração:

  • Em um sistema com troca de mensagens instantâneas (chat), poder enviar uma mensagem se passando por outra pessoa (usuário X envia mensagem para usuário A como se fosse usuário B).
  • Em um sistema de vendas, poder alterar o valor do desconto de uma compra.
  • Em um sistema de rastreamento, poder acompanhar a entrega de um pedido de outra pessoa.

Por se tratar de uma gama bem ampla de ataques, vamos focar aqui nos problemas que podem ser resolvidos com design de APIs e um pouquinho de código. Para isso, vamos tomar a seguinte especificação de funcionalidade:

Como usuário de um ecommerce
E estando autenticado na área do cliente
Quando acessar "Meus Pedidos"
Então devo visualizar pedidos realizados apenas pelo mesmo usuário que está autenticado

Gosto desse exemplo, pois ilustra bem uma situação corriqueira em equipes de desenvolvimento: criar consultas de dados. Implementamos esse tipo de consulta a todo momento, por centenas de vezes e em muitos sistemas, mas nem sempre questionamos o nível de permissão de acesso à essas consultas.

Para atender a funcionalidade descrita acima, suponhamos a seguinte modelagem do servidor, seguindo a arquitetura REST:

GET /api/usuarios/{id}/pedidos
Authorization: token-do-usuario-id-{id}

Provavelmente a implementação em alto nível (Python)2, para atender à esta rota, seria algo como:

@app.route('/usuarios/<int:id_usuario>/pedidos')
def pedidos_unsafe(id_usuario):
    token = decodificar_token(request)
    if not token['valido']:
        abort(401) # Unauthorized
    return consultar_pedidos(id_usuario)

Aparentemente, não há nada de errado com o código acima. A função decodificar_token interpreta o cabeçalho Authorization (enviado com a requisição), e confere se o token3 está válido (na grande maioria dos casos, essa validação consiste em verificar se o preenchimento do token está correto e se o mesmo não expirou ou foi invalidado - o que pode ser disparado por um evento de troca de senha, por exemplo -). Caso esteja válido, o sistema prossegue o fluxo, consultando (no banco de dados) pelos pedidos referentes ao id enviado na requisição.

Porém, além de não atender à especificação4, esta implementação expõe uma falha de segurança. Note que apesar de validarmos o token, não verificamos se o usuário relacionado aquele token é o mesmo usuário recebido no parâmetro da requisição. Aqui está a brecha para o Parameter Tampering, pois podemos enviar o token para o usuário 1 e ainda assim pedir os dados do usuário 2:

GET /api/usuarios/2/pedidos
Authorization: token-do-usuario-id-1

Uma forma simples de evitar esse exploit seria garantir que o usuário do token é o mesmo informado no parâmetro:

if token['id_usuario'] != id_usuario:
    abort(403) # Forbidden

Implementação completa:

@app.route('/usuarios/<int:id_usuario>/pedidos')
def pedidos_safe(id_usuario):
    token = decodificar_token(request)
    if not token['valido']:
        abort(401) # Unauthorized
    if token['id_usuario'] != id_usuario:
        abort(403) # Forbidden
    return consultar_pedidos(id_usuario)

Pronto! Agora as requisições que possuem o argumento id do usuário diferente do id do usuário vinculado ao token serão respondidas com um código de erro do servidor 403 (forbidden).

Podemos fazer duas simples evoluções:

  1. Centralizar e unificar esta lógica de verificação de token x parâmetro de query, pois provavelmente muitas rotas terão essa brecha; e
  2. Quando algum usuário tentar acessar informações que não são suas, registrar esta intenção para futuras auditorias.

Centralizando e unificando as regras de segurança

Para o item 1, podemos utilizar o padrão de projeto Decorator, presente nativamente em linguagens / frameworks como Python, Java e C# ou Higher-Order Functions para linguagens funcionais que ainda não possuem suporte a decorators (ex: JavaScript até junho de 2020), de forma que o código da solução se reduziria a:

@app.route('/usuarios/<int:id_usuario>/pedidos')
@garantir_mesmo_usuario(nome_argumento='id_usuario')
def pedidos_safe(id_usuario):
  return consultar_pedidos(id_usuario)

Neste caso, toda "mágica" fica por conta de garantir_mesmo_usuario5, uma função decoradora que nos permite executar código antes e depois da implementação que desejamos executar quando a rota for acionada por uma aplicação cliente (neste exemplo, consultar pedidos). Desta forma, conseguimos reduzir bastante o código e ainda reaproveitar esta lógica em outros pontos de alteração. A implementação do decorator pode ser conferida no link no final deste artigo.

Similarmente, poderíamos criar esse comportamento de verificar a segurança em uma função externa que "empacota" (wrapper) a implementação da consulta (exemplo para node.js express):

app.get('/usuarios/:idUsuario/pedidos',
  garantirMesmoUsuario(async (req, res) => {
      const { params: { idUsuario } } = req
      const pedidos = await consultarPedidos(idUsuario)
      res.send(pedidos)
    }
  )
)

Perceba que garantirMesmoUsuario é um função que recebe como argumento... uma outra função. Essa "outra função" é a implementação que desejamos executar quando a rota /usuarios/:idUsuario/pedidos for acionada com o método GET. Primeiro fazemos as verificações devidas de segurança, e aí invocamos a "outra função" em return func(req, res):

const garantirMesmoUsuario = func => (req, res) => {
  const token = decodificarToken(req)
  if (!token.valido) {
    return res.sendStatus(401)
  }
  const { params: { idUsuario } } = req
  // comparação estrita vai falhar se token.idUsuario for Number
  // e idUsuario for String
  if (token.idUsuario !== idUsuario) {
    return res.sendStatus(403)
  }
  return func(req, res)
}

Esta implementação funciona de forma muito similar aos mecanismos de middleware, presente em alguns frameworks para desenvolvimento Web, como Express e Django.

Registrando tentativas de acesso inadequado

Agora que temos toda estrutura de verificação centralizada, é fácil registrar as tentativas de acessos inadequados. Isso pode ser importante caso o software esteja sob tentativas de ataque ou se algum usuário teve suas credenciais comprometidas, permitindo ao sistema reagir corretamente a isso.

    if token['id_usuario'] != id_usuario:
+       registrar_tentativa(token, id_usuario)
        abort(403) # Forbidden

registrar_tentativa é uma função que pode gravar a tentativa no output do sistema (geralmente um stdout de terminal), em um algum arquivo de log transacional, em algum banco de dados gerenciado ou em um serviço especializado de processamento de logs de auditoria (AWS CloudTrail, ELK, Papertrail, etc).

Os detalhes de implementação desta função são, como o próprio termo diz, detalhes. O que importa é que seu sistema armazene estas informações e saiba reagir corretamente a essas tentativas de acesso. Por exemplo: enviar uma notificação para o usuário sobre o acesso inadequado e invalidar todos os tokens relacionados a ele.

"UX também é segurança!"

(Alguma pessoa lúcida, em algum momento lúcido)

Lembre-se: segurança de informação não é apenas sobre tecnologias, frameworks e padrões que inserimos no código e na arquitetura. É também sobre projetar fluxos e interações com o usuário final que garantam a máxima integridade de todos os dados que forem tratados no uso do software.

EDIT 1: essa vulnerabilidade de alterar o ID do recurso a ser consultado também é conhecida como BOLA (Broken Object Level Authorization) ou IDOR (Insecure Direct Object Reference) (créditos @jradesenv). Porém, Parameter Tampering permite outros tipos de explorações, como veremos a seguir.

Outras considerações

Nem sempre os parâmetros que podem ser alterados estarão na URL da requisição, é preciso cuidar os campos que podem ser enviados no corpo ou até mesmo os cabeçalhos. Exemplo:

POST /api/mensagens
Authentication: token-usuario-id-1

{
  "de": 2,
  "para": 3,
  "mensagem": "oi! pode me passar seu endereço?"
}

Se nada for feito no servidor (backend), é possível mandar mensagem do usuário 2 para 3 tendo apenas as credenciais do usuário 1, que pode ter sido criado simplesmente para executar algum ataque. Essa alteração de parâmetros acontece no campo de (id do usuário remetente).

Pode ser mais fácil explorar esta vulnerabilidade se os identificadores únicos dos usuários forem incrementais (ex: 1, 2, 3, ... 10401), facilitando a vida de quem for executar o ataque (ou até mesmo escrever um script para automatizar o exploit). Identificadores incrementais também podem trazer problemas de concorrência de escrita (embora nos últimos anos as tecnologias de banco de dados tenham evoluído bastante nesse aspecto). Prefira abordagens de identificação única com baixa previsibilidade, como UUID ou Hi/Lo!

Outra vulnerabilidade muito comum referente a Parameter Tempering é o acesso irrestrito a informações retirando parâmetros específicos (como um id de busca) e acessando todos registros de uma tabela (consulta mais ampla sem filtros), também conhecido como Broken Function Access Authorization, tomemos as seguintes especificações:

Funcionalidade: Detalhar pedido específico
Como usuário do ecommerce
E estando autenticado na área do cliente
Quando entrar em "Meus Pedidos" e escolher o pedido 1
Então deve abrir a tela com os detalhes do pedido 1

Funcionalidade: Listar todos pedidos
Como usuário administrador do ecommerce
E estando autenticado na área administrativa
Quando clicar no item "Todos os pedidos" do menu
Então devo visualizar todos os pedidos do ecommerce

Para a primeira funcionalidade, podemos assumir a seguinte API:

GET /api/pedidos/{id}
Authentication: token-do-usuario-{id}

Para a segunda funcionalidade:

GET /api/pedidos
Authentication: token-do-usuario-admin

Porém, se nada for feito, o usuário 1 (que não é administrador), poderá acessar /api/pedidos apenas retirando o id do pedido da URL, podendo então visualizar todos os pedidos. Ao criar Web APIs que vão expôr dados de forma ampla e com poucos filtros, questione quem deve poder acessar essas informações e faça os devidos ajustes, pois com um conhecimento básico de Web e protocolos de rede, é possível tentar acessar praticamente todos os dados que seu sistema "expõe" para as aplicações utilizarem.

Muitos frameworks Web já possuem mecanismos para auxiliar a autorização por diferentes papéis de usuários (Role Based Security), a exemplo dos decoradores de autorização do Django ou a anotação @Secured do Spring framework. Consulte a documentação das tecnologias que você usa antes de escrever manualmente esses filtros!

Conclusão

Esse tipo de vulnerabilidade nos parâmetros de requisições nem sempre é fácil de identificar e pode se manifestar de diferentes formas, mas elenco algumas práticas que podem ajudar na grande maioria dos casos:

  • Questionar, o quanto antes e de forma contínua, que tipo de papéis de usuário poderão acessar as informações que serão disponibilizadas. Refletir as respostas no desenho da solução.
  • Analisar todos os parâmetros que são trocados entre camada pública (cliente) e camada privada (servidor). Identificar quais desses parâmetros:
    1. São utilizados para localizar registros que podem ser lidos ou alterados (ex: ids de busca). Questionar o quanto o usuário que está autenticado (seja por token, cookie, sessão, etc) pode alterar dos parâmetros destes parâmetros elencados.
    2. Precisam ser re-validados (além da validação já realizada na camada de UI). Por exemplo, em um sistema com restrição de idade, é preciso garantir, no servidor, que o campo "data de nascimento" atende às regras exigidas, mesmo que essas já tenham sido impostas na camada de UI, pois em uma tentativa maliciosa esta informação pode ser alterada fora da aplicação cliente confiável.
  • Sempre que possível, preferir mecanismos de geração de identificadores únicos de baixa previsibilidade, como os já citados UUID ou Hi/Lo.
  • Desenvolva uma visão "caixa preta" e analise a sua API com uma perspectiva externa, sem detalhes de implementação. O que uma pessoa maliciosa poderia tentar fazer com as URLs e parâmetros públicos?
  • Principal: tente incutir as práticas e revisões de segurança de informação na cultura de desenvolvimento do time, de forma que isso seja diluído em preocupações e tarefas constantes e frequentes.

Segurança da informação também é qualidade de software!


  1. 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! 

  2. Por motivos didáticos e de brevidade, a implementação omite algumas definições de funções, rotas, servidor, etc. Se você quiser todos os detalhes, confira a implementação completa

  3. Todos os exemplos podem ser interpretados com qualquer mecanismo de autenticação (token, cookie, session id, etc). Por ser uma das abordagens mais populares atualmente, utilizaremos ao longo do artigo a autenticação por token. 

  4. É comum que as especificações (independente do formato) não contenham algumas preocupações básicas de segurança. Neste caso, o critério de aceite pode aparecer como "Então devo visualizar pedidos". Uma pessoa que trabalha com produtos digitais deve se preocupar com muitas coisas, então é completamente compreensível que alguns detalhes de segurança passem desapercebidos, por isso a importância da equipe de desenvolvimento se preocupar com infosec de uma forma iterativa ao longo das entregas (e não apenas no final dos projetos através de auditorias), revisando as especificações e fortalecendo uma cultura onde qualidade de software está diretamente relacionada à segurança de informação. 

  5. Também por motivos didáticos e de brevidade, a função decoradora garantir_mesmo_usuario possui duas responsabilidades: 1) verificar se o token é válido; e 2) verificar se o usuário do token é o mesmo da requisição. Em termos de boas práticas e design de código, o mais correto seria "dividir" esta função em duas menores, onde cada qual possuiria sua responsabilidade única. 

Top comments (0)