Skip to content

Restricted additional API keys can read the environment root secret via the env-var dump

High
carderne published GHSA-4gjx-f5hj-678x Sep 14, 2026

Software

triggerdotdev/trigger.dev

Affected versions

<= 4.6.0

Patched versions

>= 4.6.0

Description

Summary

Access presets such as Variables only are documented to grant only read/write on environment variables. The sibling bootstrap route was explicitly written so that “a machine credential never receives that root key.”

GET /api/v1/projects/:projectRef/envvars authenticates with read:envvars, then returns resolveVariablesForEnvironment(), which injects the environment’s root apiKey as TRIGGER_SECRET_KEY for every non-DEVELOPMENT environment.

A leaked Variables-only additional key therefore becomes the environment root secret (tr_prod_… / tr_stg_… / tr_preview_…): trigger, deploy, mint public JWTs, read all runs.

This is not the packet path-traversal advisories and not GHSA-wpjq (UAT caps).

Product boundary

From docs/apikeys.mdx:

Access presets define what a key can do.
Variables only — Read and write environment variables in this environment.
No restrictions — Full access to the environment.

From environmentVariableApiAccess.server.ts:

API-key exchanges must reuse this value instead of exposing the environment's root credential.

From api.v1.projects.$projectRef.$env.ts:

A machine credential never receives that root key.

OSS fallback cannot issue restricted presets (prepareApiKeyPolicy only allows FULL_ACCESS). The bug is in the shared route used by Cloud, where those presets exist.

Vulnerable code

apps/webapp/app/routes/api.v1.projects.$projectRef.envvars.ts (loader):

const authResult = await authenticateApiKeyWithScope(request, {
  action: "read",
  resource: { type: "envvars" },
});
const variables = await resolveVariablesForEnvironment(
  envVarEnvironment,
  envVarEnvironment.parentEnvironment ?? undefined
);
return json({
  variables: variables.reduce((acc, variable) => {
    acc[variable.key] = variable.value;
    return acc;
  }, {}),
});

resolveBuiltInProdVariables (environmentVariablesRepository.server.ts:1186-1190):

{ key: "TRIGGER_SECRET_KEY", value: parentEnvironment?.apiKey ?? runtimeEnvironment.apiKey }

resolveBuiltInDevVariables does not inject the secret (DEV is not affected).

Intended consumer of that injection is the worker (dequeue injects env into the task container), not this public HTTP dump.

Guarded sibling

GET /api/v1/projects/:ref/:env (CLI bootstrap):

apiKey: apiKeyForProjectEnvironmentBootstrap(authenticationResult, environment.apiKey)

returns presentedApiKeyFromAuthentication(...) for an API-key caller — the additional key itself, never the root.

GET /api/v1/projects/:ref/envvars/:slug uses getEnvironmentWithRedactedSecrets and does not merge builtins.

Impact

  • Confidentiality of the environment root secret and of every secret stored as an env var (the dump returns plaintext values, including isSecret entries resolved from the secret store).
  • Integrity: the dumped root key can trigger tasks, complete waitpoints, rotate webhooks, mint admin public JWTs.
  • Prerequisites: a valid additional key whose scopes include read:envvars bound to a STAGING/PRODUCTION/PREVIEW environment (Cloud Variables-only preset, or any custom policy with that action).

Reproduction

Independent of a live cluster: node poc/TD-01-envvars-root-key/source-probe.mjs (must print BYPASS_PRESENT).

Live lab (self-host v4.5.12 at 127.0.0.1:8640, 2026-08-23). OSS cannot issue Variables-only presets; the lab inserted a Cloud-shaped additional key (scopes=["read:envvars"]) into api_keys and enabled additionalApiKeyLookupEnabled. The dump / bootstrap / trigger handlers are the real shared routes.

Step Action Observed Evidence
A0 GET /api/v1/projects/$REF/envvars no token 401 dynamic/raw/td01_a0_anon.json
A1 Same URL, Authorization: Bearer $RESTRICTED 200, variables.TRIGGER_SECRET_KEY is the env root tr_prod_UcTW… (not the additional key) dynamic/raw/td01_a1_restricted_dump.json
N1 POST /api/v1/tasks/$TASK/trigger with $RESTRICTED 403 unauthorized dynamic/raw/td01_n1_restricted_trigger.json
N2 GET /api/v1/projects/$REF/prod with $RESTRICTED 200, apiKey equals the additional key (tr_prod_sk_…) dynamic/raw/td01_n2_bootstrap.json
A2 POST .../trigger with the dumped root 422 No worker group found (auth passed; lab has no supervisor) vs N1's 403 dynamic/raw/td01_a2_dumped_root_trigger.json
N3 Org B root GET Org A envvars 404 Project not found dynamic/raw/td01_n3_orgb_dump.json

Script: dynamic/deploy/seed_and_verify.py. Evidence class: full_deployment_confirmed. Write-up: dynamic/triggerdev-dynamic-note-2026-08-23.md. Cloud itself was not contacted.

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
Low
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N

CVE ID

No known CVE

Weaknesses

Improper Privilege Management

The product does not properly assign, modify, track, or check privileges for an actor, creating an unintended sphere of control for that actor. Learn more on MITRE.

Incorrect Authorization

The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check. Learn more on MITRE.

Credits