diff --git a/packages/core/src/auth.ts b/packages/core/src/auth.ts index 8e44d590..222ffb80 100644 --- a/packages/core/src/auth.ts +++ b/packages/core/src/auth.ts @@ -7,7 +7,7 @@ import { exchangeGitHubActionsTokenlessToken, } from "./github-actions-tokenless"; import type { Config } from "./config"; -import { debug } from "./debug"; +import { debug, maskToken } from "./debug"; /** * Resolve the Argos authentication token. @@ -15,7 +15,10 @@ import { debug } from "./debug"; */ export async function resolveArgosToken(config: Config): Promise { if (config.token) { - debug("Authenticated with ARGOS_TOKEN."); + // Masked, and logged here only: this is the one place that knows which + // token the command ends up using, whether it came from the parameters or + // from the environment. + debug(`Authenticated with ARGOS_TOKEN (${maskToken(config.token)}).`); return config.token; } diff --git a/packages/core/src/ci-environment/index.ts b/packages/core/src/ci-environment/index.ts index 72e461d2..fe267b8a 100644 --- a/packages/core/src/ci-environment/index.ts +++ b/packages/core/src/ci-environment/index.ts @@ -73,7 +73,7 @@ export function listAncestorCommits(input: { export async function getCiEnvironment(): Promise { const context = createContext(); - debug("Detecting CI environment", context); + debug("Detecting CI environment"); const service = getCiService(context); // Service matched @@ -89,5 +89,6 @@ export async function getCiEnvironment(): Promise { return ciEnvironment; } + debug("No CI service matched"); return null; } diff --git a/packages/core/src/debug.ts b/packages/core/src/debug.ts index f998a0b6..5f2e9191 100644 --- a/packages/core/src/debug.ts +++ b/packages/core/src/debug.ts @@ -4,6 +4,30 @@ const KEY = "@argos-ci/core"; export const debug = createDebug(KEY); +/** + * Leading characters of a token kept in the debug output: enough to tell two + * tokens apart, far too few to use. + */ +const TOKEN_PREVIEW_LENGTH = 6; + +/** + * Show which token is in play without publishing it. + * + * Debug output is the documented way to report an upload problem: it gets + * pasted into public issues and written to CI logs, which are world-readable on + * public repositories, so a token printed in full is a published token + * (GHSA-28pg-v3hp-9g7f). The first few characters answer "is that the token I + * think it is?" and are useless to anyone else. + */ +export function maskToken( + token: string | null | undefined, +): string | null | undefined { + if (!token) { + return token; + } + return `${token.slice(0, TOKEN_PREVIEW_LENGTH)}…`; +} + export const isDebugEnabled = createDebug.enabled(KEY); export const debugTime = (arg: string) => { diff --git a/packages/core/src/upload.test.ts b/packages/core/src/upload.test.ts index d68ba32c..724854f4 100644 --- a/packages/core/src/upload.test.ts +++ b/packages/core/src/upload.test.ts @@ -1,4 +1,5 @@ -import { describe, it, expect } from "vitest"; +import { describe, it, expect, vi } from "vitest"; +import createDebug from "debug"; import { join } from "node:path"; import { upload } from "./upload"; import { server, setupMockServer } from "../mocks/server"; @@ -292,4 +293,30 @@ describe("#upload", () => { expect(receivedMergeQueuePrNumbers).toEqual([12, 34]); })(); }); + + it("never prints the token in debug output (GHSA-28pg-v3hp-9g7f)", async () => { + const token = "92d832e0d22ab113c8979d73a87a11130eaa24a9"; + const write = vi.spyOn(process.stderr, "write").mockReturnValue(true); + createDebug.enable("@argos-ci/core"); + + let output: string; + try { + await upload({ + branch: "main", + apiBaseUrl: "https://api.argos-ci.dev", + root: join(__dirname, "../../../__fixtures__/screenshots"), + commit: "f16f980bd17cccfa93a1ae7766727e67950773d0", + token, + }); + output = write.mock.calls.map(([chunk]) => String(chunk)).join(""); + } finally { + createDebug.disable(); + write.mockRestore(); + } + + expect(output).not.toContain(token); + // Masked once, where the token is resolved, so the user can still tell + // which one was used. + expect(output).toContain("Authenticated with ARGOS_TOKEN (92d832…)"); + }); }); diff --git a/packages/core/src/upload.ts b/packages/core/src/upload.ts index 892ed0cb..5bf9b215 100644 --- a/packages/core/src/upload.ts +++ b/packages/core/src/upload.ts @@ -188,7 +188,8 @@ export async function upload(params: UploadParameters): Promise<{ build: ArgosAPISchema.components["schemas"]["Build"]; screenshots: Screenshot[]; }> { - debug("Starting upload with params", params); + const { token: _paramsToken, ...debugParams } = params; + debug("Starting upload with params", debugParams); // Read config const [config, argosSdk] = await Promise.all([ @@ -213,7 +214,8 @@ export async function upload(params: UploadParameters): Promise<{ (config.previewBaseUrl ? { baseUrl: config.previewBaseUrl } : undefined); const globs = params.files ?? ["**/*.{png,jpg,jpeg}"]; - debug("Using config and files", config, globs); + const { token: _configToken, ...debugConfig } = config; + debug("Using config and files", debugConfig, globs); // Collect snapshots const files = await discoverSnapshots(globs, {