zypher.co / reports / getconnects-cross-tenant-analytics-pii signed in · client link
Penetration test report

GetConnects

F-03: Cross-tenant analytics and PII exposure
April 2026 Delivered 2026-04-23
Risk index
96
/ 100
Severity distribution
Critical
1
High
0
Medium
0
Low
0
Info
0
Executive summary

The /api/analytics/admin-events endpoint returns event data for any organisation when the caller supplies an organisationId in the request body. The handler does not check that the authenticated session has permission to view that tenant. A sibling analytics endpoint on the same platform enforces this check correctly, so enforcement is inconsistent per handler rather than absent by design.

The exposed data set is broader than a contact list. It includes campaign-level event streams: SMS sent events with their message bodies, delivery outcomes, replies, and the contacts they were addressed to. A low-privilege user in any organisation on the platform can observe live outbound communications for every other tenant.

Finding 3 of 5 from the GetConnects multi-tenant API assessment. Delivered , published .

Engagement
Client
GetConnects
Endpoint
POST /api/analytics/admin-events
Class
BOLA (OWASP API1)
Auth required
Any valid low-privilege session
CVSS vector
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N
Status
Reported, remediation and retest cycle in progress
Redaction notice. Tenant names, contact details, phone numbers, and SMS message content have been replaced with placeholders in the response body. Endpoint path, request shape, response shape, and severity are unmodified.

Findings

1 total
01 Critical
F-03: Cross-tenant analytics and PII exposure
BOLA, PII, SMS content · POST /api/analytics/admin-events
9.6 reported +
Description

Same shape as F-01, one class higher on the impact scale because the analytics event stream carries live message content rather than static contact records. The request needs any valid session cookie to reach the handler; the session's own tenant is never compared against the requested tenant.

Impact

Contact PII (names and mobile numbers) plus SMS message content, including replies, plus campaign and activity metadata. Because the response crosses organisation boundaries, one low-privilege user in one tenant can observe live outbound communications for every other tenant on the platform.

The confidentiality impact is amplified for regulated verticals. GetConnects tenants include political campaigns, healthcare providers, and other industries where the fact and content of a message is legally sensitive independent of the contact's identity. For clients whose messaging is subject to consent and record-keeping obligations, exposure of the message body to a third party is not merely a privacy incident, it is a compliance one.

Remediation
  1. Centralise tenant scoping in middleware. Read the session tenant, reject any request whose organisationId in the body does not match.
  2. Audit every analytics handler against the pattern used by the sibling endpoint that does enforce the check. Copy the pattern into any handler that reads events, activity, or campaign data.
  3. Add response-shape testing in CI that runs the analytics endpoints under three synthetic sessions (unauthenticated, low-priv-A, admin-B), failing the build if any returns data outside the caller's scope.
  4. Alert on cross-tenant reads at the log ingestion layer. Any request where session.tenant_id does not match the requested tenant should page on-call, since there is no legitimate reason a normal user would trigger one.
Evidence

Proof-of-concept request:

POST /api/analytics/admin-events HTTP/2
Host: [REDACTED]
Content-Type: application/json
Cookie: token=<low_priv_user>

{
  "json": {
    "organisationId": "<target_org_id>",
    "fromDate": "2026-03-24T06:03:45.531Z",
    "toDate": "2026-04-23T06:03:45.531Z",
    "page": 1,
    "limit": 20
  }
}

Response, truncated and redacted:

{
  "json": {
    "events": [
      {
        "organisation": { "name": "[Tenant name redacted]" },
        "contact": {
          "firstName": "[Redacted]",
          "lastName": "[Redacted]",
          "mobile": { "normalised": "+XX XXX XXX XXX" }
        },
        "eventContent": {
          "data": "[SMS message body redacted]"
        },
        "type": "SMS Sent"
      }
    ]
  }
}

Root cause

  • The handler was written without a tenant-membership check, and the middleware layer does not apply one uniformly.
  • A sibling analytics endpoint on the same codebase enforces the check correctly, which shows the check is understood by the team but was not applied here.
  • The tenant identifier is supplied by the client rather than derived from the authenticated session.

Timeline

DateEvent
Engagement window; finding delivered on .
Following weeksRemediation and retest cycle.
Public release of redacted writeup.
Remediation in progress Delivered 2026-04-23 Published 2026-07-12
More reports →