Sep 23, 2026·7 min read·17 visits
Hatchet fails to restrict durable event log queries to the caller's tenant ID, allowing authenticated workers to view task metadata from other tenants if the target task UUID is known.
CVE-2026-88978 is a critical cross-tenant data exposure vulnerability in Hatchet, a platform for orchestrating background tasks and durable workflows. The flaw exists in the durable-task event retrieval system where client-supplied task, node, and branch UUIDs are resolved via the ListSatisfiedEntries database query without verifying the tenant ownership of the requesting worker context.
Hatchet is an open-source orchestration engine designed to manage background tasks, AI agents, and durable workflows. In multi-tenant environments, the control plane is responsible for enforcing strict isolation boundaries, ensuring that authenticated workers can only retrieve, execute, or query data belonging to their designated tenant context. Real-time scheduling and execution flow state is maintained through high-performance gRPC channels connecting workers to the central control plane engine.
A security audit of the event retrieval implementation identified a multi-tenant isolation bypass, designated as CVE-2026-88978. The vulnerability is located within the WorkerStatus gRPC pathway, which processes status check queries from active worker daemons. Due to a missing authorization filter, the system resolves client-supplied task, node, and branch UUIDs without validating that the requesting worker possesses legitimate access rights to the tenant containing those objects.
This security flaw corresponds to CWE-639 (Authorization Bypass Through User-Controlled Key) and CWE-863 (Incorrect Authorization). The underlying application validates the worker's session credentials but fails to enforce tenant-scoped access parameters during database queries. As a result, any authenticated tenant worker on a shared deployment can access event-log entries belonging to independent organizations, provided the target resource identifiers are known.
The technical root cause of CVE-2026-88978 resides inside the file pkg/repository/durable_events.go and its execution of the ListSatisfiedEntries SQL query. When an authenticated worker calls the WorkerStatus gRPC service, the control plane resolves the state of pending durable tasks. To retrieve status information, the repository layer queries the database via the GetSatisfiedDurableEvents function, passing in client-provided arrays of task external IDs, node IDs, and branch IDs.
Prior to the patch introduced in version 0.106.1, the underlying PostgreSQL query joined the v1_lookup_table and v1_task tables using only the client-supplied UUIDs as search keys. The query omitted any condition verifying that the tenant_id column of the target records matched the tenant_id associated with the requesting worker's authenticated session. Because the database engine executes join operations globally across shared relational tables, matching records across all tenants are returned to the caller.
While Hatchet generates durable task identifiers using high-entropy UUIDv4 values, which prevents practical brute-force guessing attacks, this design pattern introduces structural insecure direct object references (IDOR). If a target UUIDv4 is exposed through external logs, diagnostic consoles, or network transit interceptions, the isolation boundary between tenants is nullified. Relying on UUID unguessability as the sole protective control violates fundamental multi-tenant software architecture principles.
Remediation of this vulnerability required updating the database query definitions, re-generating the Go database drivers using sqlc, and modifying the calling conventions in the repository layer. The primary fix was implemented in the commit 15bc7923d3a8ebfeb0d88c19160dd24828921e85.
In the database schema definition file pkg/repository/sqlcv1/durable_event_log.sql, a strict WHERE clause was appended to restrict the join lookup to records matching the caller's tenant identifier:
-- Vulnerable Query Structure
WITH inputs AS (
SELECT * FROM unnest(@taskExternalIds::UUID[]) as external_id
)
SELECT lt.id, lt.tenant_id, t.id as task_id
FROM inputs i
JOIN v1_lookup_table lt ON lt.external_id = i.external_id
JOIN v1_task t ON (t.id, t.inserted_at) = (lt.task_id, lt.inserted_at)
-- Patched Query Structure (v0.106.1)
WITH inputs AS (
SELECT * FROM unnest(@taskExternalIds::UUID[]) as external_id
)
SELECT lt.id, lt.tenant_id, t.id as task_id
FROM inputs i
JOIN v1_lookup_table lt ON lt.external_id = i.external_id
JOIN v1_task t ON (t.id, t.inserted_at) = (lt.task_id, lt.inserted_at)
WHERE lt.tenant_id = @tenantId::UUIDIn the Go repository layer, the durableEventsRepository was updated to accept the tenant identifier and bind it to the parameters struct passed to sqlc. The following diff highlights the code modification in pkg/repository/durable_events.go:
// Vulnerable Repository Call
func (r *durableEventsRepository) GetSatisfiedDurableEvents(ctx context.Context, taskExternalIds []uuid.UUID, nodeIds []int64, branchIds []int64) {
r.queries.ListSatisfiedEntries(ctx, db, ListSatisfiedEntriesParams{
Taskexternalids: taskExternalIds,
Nodeids: nodeIds,
Branchids: branchIds,
})
}
// Patched Repository Call (v0.106.1)
func (r *durableEventsRepository) GetSatisfiedDurableEvents(ctx context.Context, tenantId uuid.UUID, taskExternalIds []uuid.UUID, nodeIds []int64, branchIds []int64) {
r.queries.ListSatisfiedEntries(ctx, db, ListSatisfiedEntriesParams{
Taskexternalids: taskExternalIds,
Nodeids: nodeIds,
Branchids: branchIds,
Tenantid: tenantId,
})
}This structural patch ensures complete resolution of the vulnerability. When the PostgreSQL engine processes the updated query, any rows that do not match the requested tenantId are filtered out prior to returning, ensuring that database read operations remain confined to the caller's security boundary.
To exploit this vulnerability, an attacker must satisfy several preconditions. First, the attacker must have valid, authenticated credentials for a worker operating under Tenant A. Second, the attacker must acquire the specific UUIDv4 task identifier of a target task running within Tenant B's environment.
Once the target identifier is obtained, the attacker establishes a gRPC connection to the control plane, authenticating as a worker for Tenant A. The attacker then constructs a crafted WorkerStatus request payload containing the target UUIDv4 task identifier. The control plane forwards these parameters directly to the database layer without tenant validation.
Because the vulnerable query lacks an lt.tenant_id constraint, the database returns matched execution records from the lookup table, regardless of which tenant owns them. The control plane subsequently packages this data and transmits it back to the attacker's client, resulting in unauthorized data disclosure.
The logic flow of this multi-tenant bypass is visualized in the following diagram:
The security impact of CVE-2026-88978 is limited to a loss of confidentiality in multi-tenant deployments. Exploitation allows an attacker to inspect step event logs, parameters, and structural execution maps of workflows running in separate tenant spaces. In workflows coordinating AI agents or database synchronizations, these event logs may contain sensitive credentials, runtime variables, or proprietary data assets.
The vulnerability does not affect system availability or data integrity. The vulnerable query path is strictly read-only, preventing attackers from injecting tasks, modifying workflow states, or deleting records belonging to other tenants. No remote code execution vectors are directly exposed through this specific database query flaw.
CVSS v3.1 evaluated this issue at 4.3 (Medium Severity), reflecting the low complexity of the exploit offset by the necessity of obtaining a high-entropy UUIDv4 and possessing authenticated worker access. For single-tenant deployments of Hatchet, the risk profile is non-existent as all database records reside within a single administrative and security boundary.
The recommended remediation path is to upgrade the Hatchet control plane and workers to version 0.106.1 or newer. This update applies the database schema modifications and ensures that all event querying is bound to verified tenant contexts. Administrators should review their active deployment tags and verify successful schema migration execution.
For systems where immediate patching is not feasible, log-level mitigations should be applied. Configure workers and API gateways to suppress high-verbosity debugging outputs that might expose task, node, or branch UUIDv4 identifiers. Additionally, implementing network-level access control limits the exposure of the gRPC interface to verified worker subnets.
Detecting historical exploitation attempts requires auditing the database queries against the PostgreSQL engine. Security teams can execute queries to identify instances where the requesting worker's tenant registration differed from the tenant owner of the queried task uuid. Discrepancies in query telemetry indicate potential cross-tenant isolation violations.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N| Attribute | Detail |
|---|---|
| CWE ID | CWE-639 / CWE-863 |
| Attack Vector | Network (gRPC) |
| CVSS Score | 4.3 (Medium) |
| Exploit Status | None (No public exploit available) |
| KEV Status | Not Listed |
| Impact | Cross-tenant data exposure |
The system uses user-controlled keys to access database records directly without verifying that the requesting user is authorized to view those records.
CVE-2026-48749 (tracked as GHSA-vghh-5rfx-xhq8 for LXD and GHSA-2q3f-q5pq-g8wv for Incus) is a critical container escape vulnerability in the Canonical LXD and Linux Containers Incus system container managers. By importing a specially crafted container image archive containing duplicate tar member headers, an authenticated attacker can replace the instance rootfs directory with a symbolic link pointing to the underlying host root filesystem. When performing file operations on a stopped container, the privileged forkfile helper binary resolves the symbolic link without confinement and chroots directly into the host filesystem, granting arbitrary root-level file read and write access.
GHSA-fmc8-p6q7-75cc (associated with CVE-2026-48755) is a critical argument injection vulnerability in Canonical LXD (github.com/canonical/lxd). The vulnerability occurs during instance and storage volume backup operations when processing the user-supplied compression_algorithm configuration parameter. While LXD validates the primary executable token against an allowlist of supported compression binaries, it fails to sanitize or restrict subsequent tokens parsed via shellquote.Split. These additional arguments are passed directly to exec.Command. An authenticated attacker with privileges to trigger backups can inject command-line flags for utilities like zstd to read staged files from a container filesystem and write them to arbitrary paths on the underlying host filesystem, such as /etc/cron.d/, achieving root-level arbitrary code execution on the host.
A critical path traversal vulnerability caused by improper input validation (CWE-20) exists in the direct image download handler of Canonical LXD and Linux Containers Incus. When an authenticated user requests an image import using the URL source type, the daemon reads the image fingerprint from an untrusted HTTP response header (`Incus-Image-Hash` or `LXD-Image-Hash`) without validating that it conforms to a SHA-256 format. This unvalidated string is concatenated directly into a filesystem path via `filepath.Join` prior to file creation and cryptographic verification, allowing directory traversal sequences to write arbitrary files on the host filesystem as root.
CVE-2026-9639 (GHSA-j93m-3j9p-m5m8) is a null pointer dereference vulnerability in Canonical LXD affecting versions 5.0.0 through 5.21.4 and 6.0 through 6.8. The flaw resides in the (*lxdBackend).CreateCustomVolumeFromBackup method within lxd/storage/backend_lxd.go. An authenticated attacker with the can_create_storage_volumes permission can upload a crafted custom volume backup tarball that omits the expires_at field on a snapshot entry, triggering an unhandled SIGSEGV panic and terminating the LXD daemon.
An operating system command injection vulnerability (CWE-78) exists in GeoNode versions prior to 5.1.0. The vulnerability occurs in the vector upload processing pipeline when handling user-supplied geospatial files. By supplying a crafted layer name containing shell metacharacters, an authenticated attacker with basic data upload privileges can execute arbitrary commands on the underlying server or container hosting the GeoNode application and Celery worker processes.
GeoNode versions prior to 5.0.3 contain an unrestricted file upload vulnerability (CWE-434) within the document and dataset ingestion architecture. Authenticated low-privileged users can bypass upload validation via direct REST API interactions, file extension masquerading, metadata mutation via PATCH requests, or malicious archive submissions. This allows attackers to store and serve dangerous file types, alter on-disk file references, or trigger path traversal and resource exhaustion during archive extraction.