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, all 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
Moderate CVSS 6.7
Class — Mass assignment (CWE-915) → cross-account resource transfer
Reference — Chatwoot advisory GHSA-x288-jh8j-348c · reported and credited
Summary. The Portals API permitted account_id in its strong
parameters, so an authenticated admin could rewrite which account owned a portal,
and drop one they control in any other account. A direct 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 flagged the same writable account_id in the Automation Rules and Macros
controllers. The fix removed it there too.
Impact. A tenant-isolation break. An admin in one account can plant a
portal in any other account, with a name, header, and links they choose. It lands in that
tenant’s help-center admin as attacker-controlled content their own staff has no
reason to doubt. Any admin in any account could do it, against any other account on the
instance.
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 →
GitHub advisory GHSA-x288-jh8j-348c
·
fix PR #13116