From a784c0b64139746b37b6f6b24a34abbee1cfcc0d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 10 Aug 2026 12:08:04 +0000 Subject: [PATCH 1/3] fix(ghcr): validate OCI manifest response before accessing layers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fetchManifest() cast response.json() as OciManifest without validating the response shape. If the registry returns malformed JSON or a response without a layers array, findLayerByFilename() crashes with TypeError: Cannot read properties of undefined. Add try/catch around response.json() with debug logging, and validate that layers is an array before returning. Co-authored-by: Miguel Betegón --- packages/cli/src/lib/ghcr.ts | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/lib/ghcr.ts b/packages/cli/src/lib/ghcr.ts index 43d6deb7e6..dabb7e35e8 100644 --- a/packages/cli/src/lib/ghcr.ts +++ b/packages/cli/src/lib/ghcr.ts @@ -20,6 +20,9 @@ import { getUserAgent } from "./constants.js"; import { customFetch } from "./custom-ca.js"; import { UpgradeError } from "./errors.js"; +import { logger } from "./logger.js"; + +const log = logger.withTag("ghcr"); /** Default timeout for GHCR HTTP requests (10 seconds) */ const GHCR_REQUEST_TIMEOUT = 10_000; @@ -250,7 +253,26 @@ export async function fetchManifest( ); } - return (await response.json()) as OciManifest; + let json: unknown; + try { + json = await response.json(); + } catch (err) { + log.debug("Failed to parse manifest JSON", err); + throw new UpgradeError( + "network_error", + `Manifest for tag "${tag}" returned invalid JSON` + ); + } + + const manifest = json as OciManifest; + if (!Array.isArray(manifest?.layers)) { + throw new UpgradeError( + "network_error", + `Manifest for tag "${tag}" has no layers array` + ); + } + + return manifest; } /** From c2f3eb600bc625083f1e5086a9c0f971980cf81b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 10 Aug 2026 12:08:11 +0000 Subject: [PATCH 2/3] fix(issues): validate getSharedIssue response shape before returning groupID MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getSharedIssue() cast response.json() as { groupID: string } without validation. If the API returns a different shape (missing groupID, null, or non-string), the caller passes undefined to subsequent API calls, producing confusing 404 errors. Add try/catch around response.json() with debug logging, and validate groupID is a non-empty string before returning. Co-authored-by: Miguel Betegón --- packages/cli/src/lib/api/issues.ts | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/lib/api/issues.ts b/packages/cli/src/lib/api/issues.ts index ec2ac970a8..a136007af5 100644 --- a/packages/cli/src/lib/api/issues.ts +++ b/packages/cli/src/lib/api/issues.ts @@ -17,6 +17,7 @@ import { } from "../custom-ca.js"; import { applyCustomHeaders } from "../custom-headers.js"; import { ApiError, ValidationError } from "../errors.js"; +import { logger } from "../logger.js"; import { resolveOrgRegion } from "../region.js"; import { invalidateCachedResponsesMatching } from "../response-cache.js"; import { getApiBaseUrl } from "../sentry-client.js"; @@ -31,6 +32,8 @@ import { unwrapPaginatedResult, } from "./infrastructure.js"; +const log = logger.withTag("api.issues"); + const TRAILING_SLASH_RE = /\/$/; /** @@ -745,5 +748,28 @@ export async function getSharedIssue( ); } - return (await response.json()) as { groupID: string }; + let json: unknown; + try { + json = await response.json(); + } catch (err) { + log.debug("Failed to parse shared issue JSON", err); + throw new ApiError( + "Share link returned invalid JSON", + response.status, + undefined, + `shared/issues/${shareId}` + ); + } + + const result = json as Record; + if (typeof result?.groupID !== "string" || !result.groupID) { + throw new ApiError( + "Share link response missing groupID", + response.status, + "The share link returned an unexpected response shape.", + `shared/issues/${shareId}` + ); + } + + return { groupID: result.groupID }; } From 8db85c3e7fa43325c51922f55dae5279cf5ffbf9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 10 Aug 2026 12:08:18 +0000 Subject: [PATCH 3/3] fix(hex-id-recovery): guard adapter data casts with Array.isArray MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The event, trace, and span fuzzy-lookup adapters cast the data field from API responses as typed arrays and call .map() without checking Array.isArray(). If the API returns non-array data (null, undefined, or an error object), .map() crashes with TypeError. Add Array.isArray guards with debug logging, returning empty arrays on malformed responses. Co-authored-by: Miguel Betegón --- packages/cli/src/lib/hex-id-recovery.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/cli/src/lib/hex-id-recovery.ts b/packages/cli/src/lib/hex-id-recovery.ts index 91304533fd..95dc38eeb7 100644 --- a/packages/cli/src/lib/hex-id-recovery.ts +++ b/packages/cli/src/lib/hex-id-recovery.ts @@ -457,6 +457,10 @@ const eventAdapter: FuzzyLookupAdapter = async (ctx) => { statsPeriod: ctx.period ?? SCAN_PERIODS.event, sort: "date", }); + if (!Array.isArray(data)) { + log.debug("listTransactions returned non-array data", typeof data); + return []; + } return (data as TransactionListItem[]).map((t) => t.id); }; @@ -469,6 +473,10 @@ const traceAdapter: FuzzyLookupAdapter = async (ctx) => { statsPeriod: ctx.period ?? SCAN_PERIODS.trace, sort: "date", }); + if (!Array.isArray(data)) { + log.debug("listSpans (trace) returned non-array data", typeof data); + return []; + } return (data as SpanListItem[]).map((s) => s.trace); }; @@ -494,6 +502,10 @@ const spanAdapter: FuzzyLookupAdapter = async (ctx) => { statsPeriod: ctx.period ?? SCAN_PERIODS.span, sort: "date", }); + if (!Array.isArray(data)) { + log.debug("listSpans (span) returned non-array data", typeof data); + return []; + } return (data as SpanListItem[]).map((s) => s.id); };