Cross-tenant data access via SQL injection in the filter API
High CVSS 8.5
Class — SQL injection → cross-tenant data access
Reference — CVE-2026-44706 · independently discovered and reported
Summary. Chatwoot’s conversation and contact filter APIs
interpolated user-supplied filter values straight into SQL, unparameterized. An
authenticated user in one account could run blind, time-based SQL injection and read data
across account boundaries.
The vector. Filtering a date or number custom
attribute with the is_greater_than / is_less_than operator put
the values field directly into the query. A second vector did the same with
attribute_key, interpolated into a JSON-path expression. Custom attributes
can be created through a public endpoint, so an attacker didn’t even need a
pre-existing one. Affected endpoints:
POST /api/v1/accounts/{id}/conversations/filter
POST /api/v1/accounts/{id}/contacts/filter
Impact. A cross-tenant breach: user emails, bcrypt password hashes, API
tokens, conversation contents, contact PII, and integration credentials — readable
across accounts. Every version from 2.2.0 was affected.
Fix (shipped in 4.11.2). Parameterized queries, plus strict
attribute_key validation:
/\A[\p{L}\p{N}_.\-]+\z/
Verify →
GitHub advisory GHSA-9pgm-75gg-6948 (CVE-2026-44706)
Cross-account resource transfer via a mass-assignable account_id
High
Class — Broken access control / IDOR → cross-account transfer
Reference — Chatwoot advisory GHSA-xcjg-q398-x2v7 Publication pending
Summary. The Portals API permitted account_id in its strong
parameters, so an authenticated admin could change which account owned a resource
— transferring another account’s portals to themselves, or moving a
victim’s portal away so the owner lost access. A direct multi-tenant isolation
bypass.
The code. The owning-account field was mass-assignable:
# app/controllers/api/v1/accounts/portals_controller.rb
def portal_params
params.require(:portal).permit(
:id, :account_id, :color, :custom_domain, :name, :slug, ...)
end # ← account_id (the tenant owner) is writable from the request body
PortalPolicy#update? only verified the caller was an admin in their
current account — nothing stopped them rewriting account_id
itself.
Exploit. As an admin in account 519, transfer a portal to account 37:
PATCH /api/v1/accounts/519/portals/my-portal
{ "portal": { "account_id": 37 } }
→ the portal now belongs to account 37;
account 519 gets a 500 and can no longer reach it.
I also flagged the same mass-assignment pattern in the Automation Rules and Macros
controllers — the bug class, not just the one instance.
Impact. Cross-account data breach and data loss — help-center
portals hold articles, categories, and customer-facing content. Exploitable by any admin
in any account.
Why a scanner walks past it. The tenant owner should come from the
session, never the request body. A scanner sees an admin-authenticated PATCH
succeed and moves on; catching this means reading the permitted-params list against the
authorization policy and noticing the owner field is writable. The fix removed
account_id from the permitted params.
Verify →
fix PR #13116 (merged)