fix: redact sensitive query values from timeout diagnostics (#2319 round 23)

The timeout message interpolated the raw request URL, and
createFetchTracker records that message verbatim into the Network log
and the persisted session. An OAuth endpoint's query string can carry a
`code`, an `access_token` or a `client_secret` — exactly the fields
fetchTracking already redacts everywhere else it records a URL — so a
stalled signed endpoint would have written its secret to both.

Redacted with the existing `redactUrlQuery`, in the
OAuthRequestTimeoutError constructor rather than at each display site,
so both the message and the public `url` field are covered by
construction and no future consumer can reintroduce it. The route's own
proxied-timeout message and the URL in its 504 marker get the same
treatment, since both cross back to the browser and are recorded there.

The path and every non-sensitive parameter survive, so the endpoint
stays identifiable — which was the point of naming it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YFnM6RqB4nXqNysgUYmjmq
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-09-11 07:57:38 -04:00
commit 1e501fa5cb
2 changed files with 55 additions and 4 deletions

View file

@ -63,6 +63,8 @@
* halves the 60s the SDK handshake incidentally provides, and it is the only
* bound at all on the legs that sit outside an SDK request.
*/
import { redactUrlQuery } from "../mcp/fetchTracking.js";
export const DEFAULT_OAUTH_REQUEST_TIMEOUT_MS = 30_000;
/**
@ -73,13 +75,26 @@ export const DEFAULT_OAUTH_REQUEST_TIMEOUT_MS = 30_000;
* hosts, and which of them stalled is the whole diagnostic.
*/
export class OAuthRequestTimeoutError extends Error {
/**
* The endpoint that stalled, with sensitive query values redacted.
*
* Redacted in the constructor rather than at each display site, so every
* consumer is covered by construction: this error's message is recorded
* verbatim by `createFetchTracker` into the Network log and the persisted
* session, and an OAuth endpoint can carry a `code`, an `access_token` or a
* `client_secret` in its query string — which `fetchTracking` already
* deliberately redacts everywhere else it records a URL (Copilot). The path
* and every non-sensitive parameter survive, so the endpoint stays
* identifiable, which was the point of naming it.
*/
readonly url: string;
readonly timeoutMs: number;
constructor(url: string, timeoutMs: number) {
super(`OAuth request to ${url} timed out after ${timeoutMs}ms`);
const safeUrl = redactUrlQuery(url);
super(`OAuth request to ${safeUrl} timed out after ${timeoutMs}ms`);
this.name = "OAuthRequestTimeoutError";
this.url = url;
this.url = safeUrl;
this.timeoutMs = timeoutMs;
}
}

View file

@ -29,6 +29,7 @@ import { watch as chokidarWatch, type FSWatcher } from "chokidar";
import { createTransportNode } from "../../node/transport.js";
import { createProxyFetch } from "../../node/proxyFetch.js";
import { OAUTH_TIMEOUT_WIRE_CODE } from "../../../auth/requestTimeout.js";
import { redactUrlQuery } from "../../fetchTracking.js";
import type {
RemoteConnectRequest,
RemoteSendRequest,
@ -1266,6 +1267,11 @@ export function createRemoteApp(
Number.isFinite(requestedTimeoutMs)
? Math.min(2_147_483_647, Math.max(0, Math.round(requestedTimeoutMs)))
: undefined;
// Redacted for the same reason `OAuthRequestTimeoutError` redacts: this
// message and this URL are handed back to the browser, recorded in the
// Network log and persisted, and an OAuth endpoint's query string can carry
// a `code`, an `access_token` or a `client_secret` (Copilot).
const safeUrl = redactUrlQuery(url);
const timer =
deadlineMs === undefined
? undefined
@ -1273,7 +1279,7 @@ export function createRemoteApp(
deadlineFired = true;
controller.abort(
new Error(
`proxied request to ${url} timed out after ${deadlineMs}ms`,
`proxied request to ${safeUrl} timed out after ${deadlineMs}ms`,
),
);
}, deadlineMs);
@ -1338,7 +1344,7 @@ export function createRemoteApp(
{
error: msg,
code: OAUTH_TIMEOUT_WIRE_CODE,
url,
url: safeUrl,
timeoutMs: deadlineMs,
},
504,