From 5c980436b6665a817ebce7f8b0801f1882c03fce Mon Sep 17 00:00:00 2001 From: Ender Date: Sun, 2 Aug 2026 00:24:52 +0200 Subject: [PATCH] fix(cli): stop logging project env var values in "started attempt" debug log MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The managed run controller logged the raw `startRunAttempt` response body, which carries `envVars` — the project environment variables injected into the run — along with the trigger payload and run metadata. That went to the runner container's stdout on every attempt, and to the webapp debug-log endpoint when `TRIGGER_SEND_RUN_DEBUG_LOGS` is enabled. The stdout path runs through `redact()`, but its deny-list matches whole lowercased key names, so it filters `payload`/`metadata` and leaves every entry inside the `envVars` map untouched: `DATABASE_URL` or `SUPABASE_SERVICE_ROLE_KEY` match nothing in the list, and only values shaped like `tr_*`, `sk-*` or `Bearer *` are caught by the value pattern. The debug-log HTTP sink applies no redaction at all. Log an explicit projection instead: run/snapshot identifiers, task, queue and machine preset, plus the environment variable *names*. Names are the part with debugging value ("did this var reach the runner?"); the values never are. Because `envVars` keys are user-defined, no name-based deny-list can classify them — so values are dropped wholesale rather than filtered. The remaining fields are an allow-list, so a new field on the API response cannot silently reintroduce a leak. Same shape as #4336, which fixed the equivalent leak in `taskRunProcess.ts`. Refs #3566 Co-Authored-By: Claude Opus 5 --- .../redact-started-attempt-debug-log.md | 5 ++ .../src/entryPoints/managed/execution.ts | 3 +- .../managed/executionLogging.test.ts | 88 +++++++++++++++++++ .../entryPoints/managed/executionLogging.ts | 30 +++++++ 4 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 .changeset/redact-started-attempt-debug-log.md create mode 100644 packages/cli-v3/src/entryPoints/managed/executionLogging.test.ts create mode 100644 packages/cli-v3/src/entryPoints/managed/executionLogging.ts diff --git a/.changeset/redact-started-attempt-debug-log.md b/.changeset/redact-started-attempt-debug-log.md new file mode 100644 index 00000000000..f6cc98e34f8 --- /dev/null +++ b/.changeset/redact-started-attempt-debug-log.md @@ -0,0 +1,5 @@ +--- +"trigger.dev": patch +--- + +Stop logging project environment variable values in the managed run controller's "started attempt" debug log. The entry now records run/snapshot identifiers and the environment variable **names** only. diff --git a/packages/cli-v3/src/entryPoints/managed/execution.ts b/packages/cli-v3/src/entryPoints/managed/execution.ts index c5bb4875bce..f10578a7e79 100644 --- a/packages/cli-v3/src/entryPoints/managed/execution.ts +++ b/packages/cli-v3/src/entryPoints/managed/execution.ts @@ -25,6 +25,7 @@ import { type SnapshotState, SnapshotManager } from "./snapshot.js"; import type { SupervisorSocket } from "./controller.js"; import { RunNotifier } from "./notifier.js"; import type { TaskRunProcessProvider } from "./taskRunProcessProvider.js"; +import { startedAttemptLogProperties } from "./executionLogging.js"; class ExecutionAbortError extends Error { constructor(message: string) { @@ -447,7 +448,7 @@ export class RunExecution { podScheduledAt: this.podScheduledAt?.getTime(), }); - this.sendDebugLog("started attempt", { start: start.data }); + this.sendDebugLog("started attempt", startedAttemptLogProperties(start.data)); return { ...start.data, metrics }; } diff --git a/packages/cli-v3/src/entryPoints/managed/executionLogging.test.ts b/packages/cli-v3/src/entryPoints/managed/executionLogging.test.ts new file mode 100644 index 00000000000..ef544a46e1b --- /dev/null +++ b/packages/cli-v3/src/entryPoints/managed/executionLogging.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, it } from "vitest"; +import type { WorkloadRunAttemptStartResponseBody } from "@trigger.dev/core/v3/workers"; +import { startedAttemptLogProperties } from "./executionLogging.js"; + +const projectSecret = "project-env-var-secret-value"; +const payloadSecret = "trigger-payload-secret-value"; + +function createStartResponse(): WorkloadRunAttemptStartResponseBody { + return { + snapshot: { + id: "snapshot-1", + friendlyId: "run_snapshot_1234", + executionStatus: "EXECUTING", + description: "Attempt started", + createdAt: new Date(), + }, + run: { + id: "run-1", + friendlyId: "run_1234", + status: "EXECUTING", + attemptNumber: 2, + }, + execution: { + run: { + id: "run-1", + payload: JSON.stringify({ apiKey: payloadSecret }), + payloadType: "application/json", + tags: [], + isTest: false, + isReplay: false, + createdAt: new Date(), + startedAt: new Date(), + }, + attempt: { number: 2, startedAt: new Date() }, + task: { id: "test-task", filePath: "test.ts" }, + queue: { id: "queue-1", name: "test-queue" }, + environment: { id: "env-1", slug: "test", type: "PRODUCTION" }, + organization: { id: "org-1", slug: "test-org", name: "Test Org" }, + project: { id: "proj-1", ref: "proj_test", slug: "test", name: "Test" }, + machine: { name: "small-1x", cpu: 0.5, memory: 0.5, centsPerMs: 0 }, + }, + envVars: { + DATABASE_URL: projectSecret, + SOME_PROVIDER_API_KEY: projectSecret, + }, + } as unknown as WorkloadRunAttemptStartResponseBody; +} + +describe("startedAttemptLogProperties", () => { + it("does not include environment variable values or the run payload", () => { + const serialized = JSON.stringify(startedAttemptLogProperties(createStartResponse())); + + expect(serialized).not.toContain(projectSecret); + expect(serialized).not.toContain(payloadSecret); + }); + + it("keeps environment variable names so operators can confirm what reached the run", () => { + const properties = startedAttemptLogProperties(createStartResponse()); + + expect(properties.envVarKeys).toEqual(["DATABASE_URL", "SOME_PROVIDER_API_KEY"]); + }); + + it("logs the identifiers needed to debug an attempt", () => { + const properties = startedAttemptLogProperties(createStartResponse()); + + expect(properties).toEqual({ + runId: "run-1", + runFriendlyId: "run_1234", + runStatus: "EXECUTING", + attemptNumber: 2, + snapshotId: "run_snapshot_1234", + executionStatus: "EXECUTING", + taskIdentifier: "test-task", + queue: "test-queue", + machinePreset: "small-1x", + isTest: false, + envVarKeys: ["DATABASE_URL", "SOME_PROVIDER_API_KEY"], + }); + }); + + it("handles a response with no environment variables", () => { + const start = createStartResponse(); + // Older platform versions can omit envVars entirely. + delete (start as Partial).envVars; + + expect(startedAttemptLogProperties(start).envVarKeys).toEqual([]); + }); +}); diff --git a/packages/cli-v3/src/entryPoints/managed/executionLogging.ts b/packages/cli-v3/src/entryPoints/managed/executionLogging.ts new file mode 100644 index 00000000000..96b295e5319 --- /dev/null +++ b/packages/cli-v3/src/entryPoints/managed/executionLogging.ts @@ -0,0 +1,30 @@ +import type { WorkloadRunAttemptStartResponseBody } from "@trigger.dev/core/v3/workers"; + +/** + * Builds the debug-log properties for the "started attempt" entry. + * + * The raw `startRunAttempt` response body carries `envVars` — the project environment + * variables injected into the run — plus the trigger payload and run metadata. Logging the + * response as-is wrote all of that to the runner's stdout, and to the webapp debug-log + * endpoint when `TRIGGER_SEND_RUN_DEBUG_LOGS` is enabled. + * + * `envVars` keys are user-defined, so no name-based deny-list can reliably classify them. + * Only the names are logged, never the values — enough to confirm which variables reached the + * run. Everything else is an explicit allow-list of identifiers, so a new field on the API + * response can't reintroduce a leak. + */ +export function startedAttemptLogProperties(start: WorkloadRunAttemptStartResponseBody) { + return { + runId: start.run.id, + runFriendlyId: start.run.friendlyId, + runStatus: start.run.status, + attemptNumber: start.run.attemptNumber, + snapshotId: start.snapshot.friendlyId, + executionStatus: start.snapshot.executionStatus, + taskIdentifier: start.execution.task.id, + queue: start.execution.queue.name, + machinePreset: start.execution.machine.name, + isTest: start.execution.run.isTest, + envVarKeys: Object.keys(start.envVars ?? {}), + }; +}