GuardVibe
News
· 6 min read

Trigger.dev Default Secrets Leak Run Env Vars; a2ui XSS Fixed

Trigger.dev fixes six self-hosted bugs, led by a built-in secret that leaks run env vars. Also: an a2ui javascript: URL XSS and two @fastify/busboy crashes.

Trigger.dev published six advisories for its self-hosted webapp on October 2, led by a critical one: a default secret compiled into the source lets anyone who can reach an untouched install pull decrypted environment variables out of task runs. The same day, @a2ui/web_core fixed a critical javascript: URL bug in agent-rendered buttons, and @fastify/busboy fixed two unauthenticated denial-of-service bugs in multipart parsing.

What shipped

Trigger.dev: built-in coordinator secret exposes run secrets

The /coordinator Socket.IO namespace mounts on every webapp boot and authenticates with COORDINATOR_SECRET, which defaults to the literal string coordinator-secret (and PROVIDER_SECRET to provider-secret). The advisory notes that the override variables were missing from the self-hosting docs, .env.example and the Helm values. Anyone who connects with the default can request a run and receive its decrypted environment variables. The attacker needs a run's internal id, so this is not a one-request takeover, but those env vars are usually your production API keys.

Fix: move self-hosted instances to 4.5.4 or later (4.5.6 covers everything below), and set your own random COORDINATOR_SECRET and PROVIDER_SECRET values.

Trigger.dev: secrets copied from .env.example allow forged logins

The Docker Compose .env.example shipped fixed values for SESSION_SECRET, MAGIC_LINK_SECRET, ENCRYPTION_KEY and MANAGED_WORKER_SECRET. If you ran cp .env.example .env, an attacker who knows MAGIC_LINK_SECRET can forge a magic link for any email address. Self-hosted instances create accounts automatically unless WHITELISTED_EMAILS is set, so the advisory describes the chain as ending in full infrastructure compromise.

Fix: upgrade to 4.5.6, then rotate all four secrets with values you generate yourself. Upgrading alone does not change secrets that are already in your .env.

Trigger.dev: cross-tenant SQL injection, replay IDOR and webhook SSRF

The remaining four bugs are all in multi-tenant code:

  • The query compiler behind POST /api/v1/query escapes every user input except the window-function name. That gap lets an authenticated customer inject a subquery that runs outside the per-tenant WHERE guard and reads other organizations' analytics rows.
  • The run-replay POST action never called requireUser, even though the GET loader in the same file did. Any authenticated user could replay another organization's run by its friendlyId.
  • A separate replay bug took the target environment id straight from the form body, so a user could run their own task in someone else's environment.
  • Webhook alert URLs were stored as a bare z.string() and fetched from the server with no private-IP filtering, giving project members SSRF to internal services and cloud metadata endpoints.

Fix: 4.5.6 covers all four.

@a2ui/web_core: javascript: URLs in agent-rendered buttons

  • Package: @a2ui/web_core 0.9.0 up to (not including) 0.10.2, fixed in 0.10.2
  • Severity: critical, CVSS 9.3 — GHSA-72qq-p3r5-f7wq (CVE-2026-10032)

A2UI lets an agent describe UI that your app then renders. The default catalog's openUrl action passes the agent-supplied URL straight to window.open(). A malicious or prompt-injected agent can attach a javascript: URL to a button, and clicking it runs script in your app's origin.

npm install @a2ui/web_core@latest   # 0.10.2 or later

@fastify/busboy: two unauthenticated multipart crashes

A multipart part header named __proto__ or constructor makes the parser throw. If you call write() directly without a try/catch, that throw can kill the process. On 3.1.0 and later, a boundary of exactly 252 bytes also sends the boundary search into a CPU-bound loop. Both are reachable before your middleware runs, by anyone who can send multipart/form-data. The advisory names @fastify/multipart as one common way it gets into an app.

npm ls @fastify/busboy
npm update @fastify/busboy   # or pin "overrides": { "@fastify/busboy": "^3.2.1" }

Default secrets, explained

A default secret is a credential that ships with the software: a fallback in code (env.SECRET ?? "changeme"), a value in .env.example, or a sample in a Helm chart. It is public the moment the repository is, and every install that never overrides it shares the same key. Trigger.dev had both kinds in one release cycle, and the fixes look the same in any codebase.

This pattern turns up constantly in AI-generated code. When an agent writes a config loader, it tries to make the app boot on the first run, and a fallback value is the quickest way to stop a missing variable from crashing startup. Agents also write .env.example files with realistic-looking values, and the setup docs they produce tell users to cp .env.example .env. Each step is reasonable, and together they put a shared secret into production.

The vulnerable shape, in a typical Next.js or Node config module:

// config.ts — boots everywhere, secure nowhere
export const env = {
  SESSION_SECRET: process.env.SESSION_SECRET ?? "dev-session-secret",
  WEBHOOK_SECRET: process.env.WEBHOOK_SECRET || "whsec_test",
};

The fixed version fails loudly instead of quietly:

// config.ts — refuse to start without real secrets
import { z } from "zod";

const Secret = z.string().min(32).refine(
  (s) => !/changeme|example|secret|test|dev-/i.test(s),
  "looks like a placeholder",
);

export const env = z
  .object({ SESSION_SECRET: Secret, WEBHOOK_SECRET: Secret })
  .parse(process.env);

In .env.example, leave the value empty (SESSION_SECRET=) and add a comment with the command that generates one (openssl rand -hex 32). An empty value fails validation. A plausible-looking one sails through.

The review check: search every place a secret is read (process.env.*SECRET*, *_KEY, *_TOKEN) and ask one question: if this variable is unset, does the app refuse to start? Any ??, || or .default(...) on a secret means the answer is no. Then open .env.example: anything that isn't blank or an obvious placeholder is a value someone will deploy.

Check your own repo

npm ls trigger.dev @a2ui/web_core @fastify/busboy        # are any of these in your tree?
grep -rnE "(SECRET|_KEY|TOKEN)[^=]*(\?\?|\|\|)\s*['\"]" src  # secrets with hardcoded fallbacks
npx guardvibe audit .                                      # full audit incl. dependency CVEs (OSV)
npm audit

If you self-host Trigger.dev, compare the secret values in your .env against the project's .env.example. Any match means rotate, not just upgrade.

Sources

Get the next one in your feed reader

Follow GuardVibe in your feed reader. No account, no email.