@@ -776,6 +776,17 @@ async function httpDownloadAttempt(
776776 ? res . headers . location
777777 : new URL ( res . headers . location , url ) . toString ( )
778778
779+ // Reject HTTPS-to-HTTP downgrade redirects.
780+ const redirectParsed = new URL ( redirectUrl )
781+ if ( isHttps && redirectParsed . protocol !== 'https:' ) {
782+ reject (
783+ new Error (
784+ `Redirect from HTTPS to HTTP is not allowed: ${ redirectUrl } ` ,
785+ ) ,
786+ )
787+ return
788+ }
789+
779790 resolve (
780791 httpDownloadAttempt ( redirectUrl , destPath , {
781792 ca,
@@ -946,6 +957,17 @@ async function httpRequestAttempt(
946957 ? res . headers . location
947958 : new URL ( res . headers . location , url ) . toString ( )
948959
960+ // Reject HTTPS-to-HTTP downgrade redirects.
961+ const redirectParsed = new URL ( redirectUrl )
962+ if ( isHttps && redirectParsed . protocol !== 'https:' ) {
963+ reject (
964+ new Error (
965+ `Redirect from HTTPS to HTTP is not allowed: ${ redirectUrl } ` ,
966+ ) ,
967+ )
968+ return
969+ }
970+
949971 resolve (
950972 httpRequestAttempt ( redirectUrl , {
951973 body,
@@ -1141,8 +1163,10 @@ export async function httpDownload(
11411163 // Download to a temp file first, then atomically rename to destination.
11421164 // This prevents partial/corrupted files at the destination path if download fails,
11431165 // and preserves the original file (if any) until download succeeds.
1166+ const crypto = getCrypto ( )
11441167 const fs = getFs ( )
1145- const tempPath = `${ destPath } .download`
1168+ const tempSuffix = crypto . randomBytes ( 6 ) . toString ( 'hex' )
1169+ const tempPath = `${ destPath } .${ tempSuffix } .download`
11461170
11471171 // Clean up any stale temp file from a previous failed download.
11481172 if ( fs . existsSync ( tempPath ) ) {
@@ -1165,20 +1189,28 @@ export async function httpDownload(
11651189
11661190 // Verify checksum if sha256 hash is provided.
11671191 if ( sha256 ) {
1168- const crypto = getCrypto ( )
11691192 // eslint-disable-next-line no-await-in-loop
11701193 const fileContent = await fs . promises . readFile ( tempPath )
11711194 const computedHash = crypto
11721195 . createHash ( 'sha256' )
11731196 . update ( fileContent )
11741197 . digest ( 'hex' )
11751198
1176- if ( computedHash !== sha256 . toLowerCase ( ) ) {
1199+ const expectedHash = sha256 . toLowerCase ( )
1200+
1201+ // Use constant-time comparison to prevent timing attacks.
1202+ if (
1203+ computedHash . length !== expectedHash . length ||
1204+ ! crypto . timingSafeEqual (
1205+ Buffer . from ( computedHash ) ,
1206+ Buffer . from ( expectedHash ) ,
1207+ )
1208+ ) {
11771209 // eslint-disable-next-line no-await-in-loop
11781210 await safeDelete ( tempPath )
11791211 throw new Error (
11801212 `Checksum verification failed for ${ url } \n` +
1181- `Expected: ${ sha256 . toLowerCase ( ) } \n` +
1213+ `Expected: ${ expectedHash } \n` +
11821214 `Computed: ${ computedHash } ` ,
11831215 )
11841216 }
0 commit comments