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:
parent
45d87911eb
commit
1e501fa5cb
2 changed files with 55 additions and 4 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue