← Tim Haines

Vulnerabilities I’ve reported

Cross-tenant and other security bugs I found and disclosed in a production multi-tenant SaaS. All fixed upstream. The links let you verify each one.

These are from Chatwoot, an open-source customer-engagement platform that runs multi-tenant: many customer accounts on one system. I found them the way I run an engagement: reading the code and testing what one account can reach across the tenant boundary, rather than scanning the running app. Two of the three are cross-tenant. One account reads another’s data. The other plants its own content in someone else’s account. Each was reported to Chatwoot and fixed.

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

Also reported & fixed

SSRF in an upload endpoint Publication pending. Any authenticated user could make the server fetch an attacker-controlled URL and exfiltrate the host’s AWS credentials. Reported to Chatwoot and fixed; advisory GHSA-qpw4-7pxw-mp9r.