← Notes

CVE-2026-73068: ToolJet Cross-Tenant TooljetDB Authorization Bypass

Overview

CVECVE-2026-73068
AdvisoryGHSA-cx42-x23w-vhvf
Componentserver/src/modules/tooljet-db/controller.ts
AffectedToolJet < 3.16.208
FixedToolJet 3.16.208
ClassCWE-863 (Incorrect Authorization)
CVSS v3.15.5 (Moderate) — CVSS:3.1/AV:A/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L
Found byLukas Johannes Moeller (@JohannesLks)

TL;DR

Every TooljetDB controller endpoint took an organizationId directly from the URL path and forwarded it to the table-operations service. The JWT guard validated the caller's session against the workspace in the tj-workspace-id header, but nothing compared that workspace to the URL segment.

An authenticated user in Workspace B could keep their own valid session, replace the URL value with Workspace A's ID, and operate on A's TooljetDB tables: enumerate and inspect them, create or rename tables, change columns, and delete tables.

Root Cause

The TooljetDB controller accepted organizationId as a path parameter and passed it into tableOperationsService.perform(). Authorization was split across two layers that each assumed the other had established tenant ownership:

  • The JWT guard checked that the session was valid for the workspace named by tj-workspace-id.
  • The CASL ability factory granted TooljetDB verbs according to the caller's role, not according to ownership of the workspace in the URL.

Neither layer bound request.params.organizationId to the authenticated workspace. A builder authorized to create or drop tables in their own tenant therefore carried those same verbs into a victim tenant selected through the path.

Reachability

No administrative access to the victim workspace was needed. The attacker used their own valid cookie and workspace header and changed only the path parameter:

GET    /api/tooljet-db/organizations/<victim-org-id>/tables
GET    /api/tooljet-db/organizations/<victim-org-id>/table/<table-name>
POST   /api/tooljet-db/organizations/<victim-org-id>/table
DELETE /api/tooljet-db/organizations/<victim-org-id>/table/<table-name>
Cookie: tj_auth_token=<attacker's own JWT>
tj-workspace-id: <attacker's own workspace id>

The mismatch survived authentication because the header and cookie still described a legitimate attacker session. The untrusted tenant selector lived in a different part of the request.

Live Proof (Excerpt)

=== Cross-Tenant Table Enumeration ===
GET /api/tooljet-db/organizations/<victim-workspace-id>/tables
-> {"result":[{"id":"a48926d1-...","table_name":"secret_employees"}]}

=== Cross-Tenant Table Creation ===
POST /api/tooljet-db/organizations/<victim-workspace-id>/table
Body: {"table_name":"attacker_table", ...}
-> {"result":{"id":"63aa138e-...","table_name":"attacker_table"}}

=== Cross-Tenant Table Deletion ===
DELETE /api/tooljet-db/organizations/<victim-workspace-id>/table/secret_employees
-> {"result":true}

After the final request, secret_employees was gone from the victim workspace and attacker_table existed in it.

Impact

  • Cross-tenant enumeration of table names, identifiers, and column schemas.
  • Cross-tenant creation and renaming of tables and modification of their columns.
  • Destructive deletion of another workspace's TooljetDB tables.
  • Loss of TooljetDB's tenant-isolation boundary on a shared ToolJet instance.

The published advisory scores the issue as CVSS 3.1 5.5 (Moderate). The demonstrated authorization failure nevertheless permits both reading metadata and destructive changes across tenants.

Fix

ToolJet 3.16.208 added a guard that rejects requests when the URL organizationId differs from the caller's authenticated workspace, before the controller action executes:

@Injectable()
export class TooljetDbOrganizationValidateGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    const urlOrgId = request?.params?.organizationId;
    if (!urlOrgId) return true;

    const sessionOrgId =
      request?.user?.organizationId ?? request?.headers?.['tj-workspace-id'];

    if (!sessionOrgId || sessionOrgId !== urlOrgId) {
      throw new ForbiddenException('Workspace mismatch for TooljetDB request');
    }
    return true;
  }
}

The important property is the invariant, not the framework primitive: every tenant identifier supplied by the client must match the tenant established by the authenticated session before any TooljetDB operation runs.

Disclosure

I reported the issue to ToolJet through coordinated disclosure. ToolJet fixed it in version 3.16.208 and published the advisory on August 7, 2026, crediting JohannesLks. The issue was subsequently assigned CVE-2026-73068.