Skip to content

Commit 1c8dfbf

Browse files
committed
fix(core): redact secrets from structured log messages
1 parent 8c4f389 commit 1c8dfbf

2 files changed

Lines changed: 27 additions & 10 deletions

File tree

packages/core/src/logger.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@ const DEFAULT_FILTERED_KEYS = [
3838
"completedwaitpoints",
3939
];
4040

41-
// Belt-and-braces value-shape check: catches secrets that land under a field name we didn't
42-
// think to deny-list (a live trigger.dev API key, a bearer token, or an OpenAI-style secret key).
43-
const SECRET_VALUE_PATTERN = /^(tr_[a-zA-Z0-9_-]{4,}|sk-[a-zA-Z0-9_-]{4,}|Bearer\s+\S+)/;
41+
// Belt-and-braces value-shape check: catches secrets anywhere in values that land under a field
42+
// name we didn't think to deny-list (a trigger.dev API key, bearer token, or OpenAI-style key).
43+
const SECRET_VALUE_PATTERN = /(tr_[a-zA-Z0-9_-]{4,}|sk-[a-zA-Z0-9_-]{4,}|Bearer\s+\S+)/;
4444

4545
// Per-field and per-structure caps so a single unbounded object (a run payload, a batch of
4646
// items, a DB row) can't blow up log line size or CPU. Truncation keeps the field present and
@@ -159,7 +159,7 @@ export class Logger {
159159
const currentSpan = trace.getSpan(context.active());
160160

161161
const structuredError = extractStructuredErrorFromArgs(this.#filteredKeys, ...args);
162-
const structuredMessage = extractStructuredMessageFromArgs(...args);
162+
const structuredMessage = extractStructuredMessageFromArgs(this.#filteredKeys, ...args);
163163

164164
const structuredLog = {
165165
...structureArgs(safeJsonClone(args) as Record<string, unknown>[], this.#filteredKeys),
@@ -225,19 +225,23 @@ function extractStructuredErrorFromArgs(
225225
message: filterKeys(nestedError.message, filteredKeys),
226226
stack: filterKeys(nestedError.stack, filteredKeys),
227227
name: nestedError.name,
228-
metadata: "metadata" in nestedError ? filterKeys(nestedError.metadata, filteredKeys) : undefined,
228+
metadata:
229+
"metadata" in nestedError ? filterKeys(nestedError.metadata, filteredKeys) : undefined,
229230
};
230231
}
231232

232233
return;
233234
}
234235

235-
function extractStructuredMessageFromArgs(...args: Array<Record<string, unknown> | undefined>) {
236+
function extractStructuredMessageFromArgs(
237+
filteredKeys: Set<string>,
238+
...args: Array<Record<string, unknown> | undefined>
239+
) {
236240
// Check to see if there is a `message` key in the args, and if so, return it
237241
const structuredMessage = args.find((arg) => arg?.message);
238242

239243
if (structuredMessage) {
240-
return structuredMessage.message;
244+
return filterKeys(structuredMessage.message, filteredKeys);
241245
}
242246

243247
return;

packages/core/test/logger.test.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,13 +94,13 @@ describe("Logger redaction", () => {
9494
expect(JSON.stringify(line)).not.toContain("tr_prod_secret_value");
9595
});
9696

97-
it("redacts values that look like a secret even under a non-denied key", () => {
97+
it("redacts values that contain a secret even under a non-denied key", () => {
9898
const logger = new Logger("test", "info");
9999

100100
const line = captureLogLine(() =>
101101
logger.info("value pattern check", {
102-
someRandomField: "tr_live_abcdef123456",
103-
anotherField: "Bearer some.jwt.value",
102+
someRandomField: "request failed with tr_live_abcdef123456",
103+
anotherField: "authorization used Bearer some.jwt.value",
104104
normalField: "just some text",
105105
})
106106
);
@@ -110,6 +110,19 @@ describe("Logger redaction", () => {
110110
expect(line.normalField).toBe("just some text");
111111
});
112112

113+
it("redacts a structured message before assigning it to $message", () => {
114+
const logger = new Logger("test", "info");
115+
116+
const line = captureLogLine(() =>
117+
logger.info("structured message check", {
118+
message: "request failed with Bearer secret.jwt.value",
119+
})
120+
);
121+
122+
expect(line.$message).toMatch(/^\[filtered/);
123+
expect(JSON.stringify(line)).not.toContain("secret.jwt.value");
124+
});
125+
113126
it("truncates strings longer than the per-field cap", () => {
114127
const logger = new Logger("test", "info");
115128
const longValue = "a".repeat(20_000);

0 commit comments

Comments
 (0)