Repository navigation
fix(bundle): validate presigned download URLs and disable redirects #586
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -42,6 +42,8 @@ import { createWriteStream } from 'node:fs'; | |
| import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path'; | ||
| import type { CliFailureContext, CliTestStep } from '../commands/test.js'; | ||
| import { ApiError, InterruptError, TransportError, localValidationError } from './errors.js'; | ||
| import { disallowedIpReason } from './target-url.js'; | ||
| import * as net from 'node:net'; | ||
| import { globalShutdown, type ShutdownHandle } from './interrupt.js'; | ||
| import { defaultSleep, sleepUnlessInterrupted } from './poll-support.js'; | ||
| import { requireEnum } from './validate.js'; | ||
|
|
@@ -860,6 +862,35 @@ function sidecarExtension(kind: 'log' | 'network' | 'console' | 'screenshot' | ' | |
| * retried up to STREAM_URL_MAX_RETRIES times with a fixed delay. | ||
| * Presigned URLs are valid for 15 minutes, so retries are safe. | ||
| */ | ||
|
|
||
| /** | ||
| * SSRF guard for presigned artifact/code downloads: require https and reject | ||
| * loopback, link-local, RFC1918, and the IPv6 forms `disallowedIpReason` | ||
| * already classifies. Runs before any fetch so a disallowed URL never leaves | ||
| * the machine. | ||
| */ | ||
| function assertSafeDownloadUrl(url: string): void { | ||
| let parsed: URL; | ||
| try { | ||
| parsed = new URL(url); | ||
| } catch { | ||
| throw localValidationError('url', 'must be a valid URL'); | ||
| } | ||
| if (parsed.protocol !== 'https:') { | ||
| throw localValidationError('url', 'must use the https scheme'); | ||
| } | ||
| const host = parsed.hostname.replace(/^\[|\]$/g, '').toLowerCase(); | ||
| if (host === 'localhost') { | ||
| throw localValidationError('url', 'localhost is not allowed'); | ||
| } | ||
| if (net.isIP(host) !== 0) { | ||
| const reason = disallowedIpReason(host); | ||
| if (reason !== undefined) { | ||
| throw localValidationError('url', reason.reason); | ||
| } | ||
| } | ||
|
Comment on lines
+886
to
+891
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift Validate resolved addresses before artifact downloads. If a presigned HTTPS hostname resolves to a private or link-local address, The PR objective requires validation of resolved hostname addresses. Based on learnings: “pin each connection's resolved DNS address via a custom 🤖 Prompt for AI AgentsSource: Learnings |
||
| } | ||
|
|
||
| export async function streamUrlToFile( | ||
| url: string, | ||
| filePath: string, | ||
|
|
@@ -884,6 +915,7 @@ async function streamUrlToFileTracked( | |
| sleepFn: (ms: number) => Promise<void>, | ||
| shutdownSignal: AbortSignal, | ||
| ): Promise<void> { | ||
| assertSafeDownloadUrl(url); | ||
| const artifactUrl = redactArtifactUrlForDetails(url); | ||
| for (let attempt = 1; attempt <= STREAM_URL_MAX_RETRIES; attempt++) { | ||
| if (shutdownSignal.aborted) throw shutdownSignal.reason; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
Validate resolved addresses before code downloads.
If a presigned HTTPS hostname resolves to a private or link-local address,
net.isIP(host)returns0.readPresignedBodythen passes the URL tofetchImpl, which can connect to that address despiteredirect: 'error'. Resolve and classify the hostname’s addresses, and bind the connection to an approved result. Apply the check to each new connection. Node fetch supports a custom dispatcher for connection control. (nodejs.org)The PR objective requires validation of resolved hostname addresses. Based on learnings: “pin each connection's resolved DNS address via a custom
lookuphook.”🤖 Prompt for AI Agents
Source: Learnings