From 06bfefa2fce48a34d12c65482531469989550bc6 Mon Sep 17 00:00:00 2001 From: ghost <49853598+JSONbored@users.noreply.github.com> Date: Wed, 8 Jul 2026 21:23:42 -0700 Subject: [PATCH] fix(selfhost): redact D1 probe token errors --- src/selfhost/d1-size-probe.ts | 24 ++++++++++++----- test/unit/selfhost-d1-size-probe.test.ts | 34 +++++++++++++++++++++++- 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/src/selfhost/d1-size-probe.ts b/src/selfhost/d1-size-probe.ts index 46c244078f..bed112b81a 100644 --- a/src/selfhost/d1-size-probe.ts +++ b/src/selfhost/d1-size-probe.ts @@ -65,8 +65,8 @@ const D1_PROBE_FETCH_TIMEOUT_MS = 10_000; export function resolveD1SizeProbeConfig(env: D1SizeProbeEnv): D1SizeProbeConfig | null { const accountId = env.CLOUDFLARE_D1_MONITOR_ACCOUNT_ID; const databaseId = env.CLOUDFLARE_D1_MONITOR_DATABASE_ID; - const apiToken = env.CLOUDFLARE_D1_MONITOR_API_TOKEN; - if (!accountId || !databaseId || !apiToken) return null; + const apiToken = env.CLOUDFLARE_D1_MONITOR_API_TOKEN?.trim(); + if (!accountId || !databaseId || !apiToken || /[\x00-\x1f\x7f]/.test(apiToken)) return null; return { accountId, databaseId, apiToken, tables: DEFAULT_MONITORED_TABLES }; } @@ -175,9 +175,21 @@ interface D1ProbeSample { let lastSample: D1ProbeSample | null = null; -function logD1ProbeError(part: "database_info" | "table_row_count", error: unknown, table?: string): void { +function redactD1ProbeSecret(message: string, apiToken: string): string { + return message.split(apiToken).join("[redacted]"); +} + +function logD1ProbeError(config: D1SizeProbeConfig, part: "database_info" | "table_row_count", error: unknown, table?: string): void { incr("gittensory_d1_probe_errors_total", { part }); - console.error(JSON.stringify({ level: "error", event: "d1_size_probe_error", part, ...(table ? { table } : {}), message: errorMessage(error).slice(0, 200) })); + console.error( + JSON.stringify({ + level: "error", + event: "d1_size_probe_error", + part, + ...(table ? { table } : {}), + message: redactD1ProbeSecret(errorMessage(error), config.apiToken).slice(0, 200), + }), + ); } /** @@ -195,13 +207,13 @@ export async function runD1SizeProbe(env: D1SizeProbeEnv, fetchImpl: typeof fetc const [freshInfo, freshRowCounts] = await Promise.all([ fetchD1DatabaseInfo(config, fetchImpl).catch((error: unknown) => { - logD1ProbeError("database_info", error); + logD1ProbeError(config, "database_info", error); return null; }), Promise.all( config.tables.map((table) => fetchD1TableRowCount(config, table, fetchImpl).catch((error: unknown) => { - logD1ProbeError("table_row_count", error, table); + logD1ProbeError(config, "table_row_count", error, table); return null; }), ), diff --git a/test/unit/selfhost-d1-size-probe.test.ts b/test/unit/selfhost-d1-size-probe.test.ts index e0ed728216..d53a3fc226 100644 --- a/test/unit/selfhost-d1-size-probe.test.ts +++ b/test/unit/selfhost-d1-size-probe.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { LATEST_ONLY_SIGNAL_SNAPSHOT_TYPES } from "../../src/db/retention"; import { d1DatabaseSizeBytesSample, @@ -16,6 +16,7 @@ import { import { renderMetrics, resetMetrics, gauge, gaugeVector, counterValue } from "../../src/selfhost/metrics"; afterEach(() => { + vi.restoreAllMocks(); resetD1SizeProbeForTest(); resetMetrics(); }); @@ -77,6 +78,18 @@ describe("resolveD1SizeProbeConfig / isD1SizeProbeEnabled", () => { expect(isD1SizeProbeEnabled(FULL_ENV)).toBe(true); }); + it("trims the api token before building authorization headers", async () => { + const config = resolveD1SizeProbeConfig({ ...FULL_ENV, CLOUDFLARE_D1_MONITOR_API_TOKEN: " token-1 " }); + expect(config).not.toBeNull(); + expect(config?.apiToken).toBe("token-1"); + + const fetchImpl: typeof fetch = async (_input, init) => { + expect(new Headers(init?.headers).get("authorization")).toBe("Bearer token-1"); + return new Response(envelope({ file_size: 1, num_tables: 1 })); + }; + await expect(fetchD1DatabaseInfo(config!, fetchImpl)).resolves.toEqual({ fileSizeBytes: 1, numTables: 1 }); + }); + it("returns null (disabled) when the account id is missing", () => { expect(resolveD1SizeProbeConfig({ ...FULL_ENV, CLOUDFLARE_D1_MONITOR_ACCOUNT_ID: undefined })).toBeNull(); }); @@ -89,6 +102,10 @@ describe("resolveD1SizeProbeConfig / isD1SizeProbeEnabled", () => { expect(resolveD1SizeProbeConfig({ ...FULL_ENV, CLOUDFLARE_D1_MONITOR_API_TOKEN: "" })).toBeNull(); }); + it("returns null (disabled) when the api token contains a control character", () => { + expect(resolveD1SizeProbeConfig({ ...FULL_ENV, CLOUDFLARE_D1_MONITOR_API_TOKEN: "token-1\nTAIL" })).toBeNull(); + }); + it("isD1SizeProbeEnabled is false with no config at all", () => { expect(isD1SizeProbeEnabled({})).toBe(false); }); @@ -224,6 +241,21 @@ describe("runD1SizeProbe", () => { expect(d1TableRowCountSamples()).toEqual([]); }); + it("redacts the Cloudflare api token from logged probe errors", async () => { + const secretToken = "cf-secret-token"; + const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined); + const failingFetch: typeof fetch = async (_input, init) => { + throw new TypeError(`Headers.append: ${new Headers(init?.headers).get("authorization")} is an invalid header value.`); + }; + + await runD1SizeProbe({ ...FULL_ENV, CLOUDFLARE_D1_MONITOR_API_TOKEN: secretToken }, failingFetch); + + expect(consoleError).toHaveBeenCalled(); + const logged = consoleError.mock.calls.map((call) => String(call[0])).join("\n"); + expect(logged).not.toContain(secretToken); + expect(logged).toContain("Bearer [redacted]"); + }); + it("isolates a single failing table: other tables still update and the failed one keeps its stale value", async () => { await runD1SizeProbe( FULL_ENV,