11import { app , net } from 'electron' ;
22import fs from 'node:fs' ;
33import path from 'node:path' ;
4+ import dns from 'node:dns' ;
5+ import { isIP } from 'node:net' ;
46import { randomUUID } from 'node:crypto' ;
57
8+ const isPrivateIPv4 = ( ip ) => {
9+ const [ a , b ] = ip . split ( '.' ) . map ( Number ) ;
10+ if ( a === 0 || a === 127 || a === 10 ) { return true ; } // unspecified, loopback, private
11+ if ( a === 172 && b >= 16 && b <= 31 ) { return true ; } // private
12+ if ( a === 192 && b === 168 ) { return true ; } // private
13+ if ( a === 169 && b === 254 ) { return true ; } // link-local
14+ return false ;
15+ } ;
16+
17+ const isPrivateIPv6 = ( ip ) => {
18+ const addr = ip . toLowerCase ( ) ;
19+ if ( addr === '::1' || addr === '::' ) { return true ; } // loopback, unspecified
20+ const mapped = addr . match ( / : : f f f f : ( \d + \. \d + \. \d + \. \d + ) $ / ) ; // IPv4-mapped
21+ if ( mapped ) { return isPrivateIPv4 ( mapped [ 1 ] ) ; }
22+ if ( / ^ f [ c d ] / . test ( addr ) ) { return true ; } // unique-local fc00::/7
23+ if ( / ^ f e [ 8 9 a b ] / . test ( addr ) ) { return true ; } // link-local fe80::/10
24+ return false ;
25+ } ;
26+
27+ const isBlockedAddress = ( address ) => {
28+ const kind = isIP ( address ) ;
29+ if ( kind === 4 ) { return isPrivateIPv4 ( address ) ; }
30+ if ( kind === 6 ) { return isPrivateIPv6 ( address ) ; }
31+ return true ; // not a resolvable IP literal: refuse
32+ } ;
33+
634// Downloads a remote protocol to a temp file. Runs in the main process, where
735// fetch is not subject to the renderer's CORS policy. Returns the temp path,
836// which is inside app temp so the extract step's path guard permits it.
37+ //
38+ // SSRF guard: the URL is user-supplied, so we resolve the host and refuse
39+ // private/loopback/link-local targets, and reject redirects (which could
40+ // otherwise bounce a public host to an internal address).
941const downloadProtocol = async ( url ) => {
1042 let parsed ;
1143 try {
@@ -17,7 +49,12 @@ const downloadProtocol = async (url) => {
1749 throw new Error ( `Disallowed URL scheme: ${ parsed . protocol } ` ) ;
1850 }
1951
20- const response = await net . fetch ( parsed . href ) ;
52+ const addresses = await dns . promises . lookup ( parsed . hostname , { all : true } ) ;
53+ if ( addresses . length === 0 || addresses . some ( ( { address } ) => isBlockedAddress ( address ) ) ) {
54+ throw new Error ( 'Refusing to download from a private, loopback, or link-local address.' ) ;
55+ }
56+
57+ const response = await net . fetch ( parsed . href , { redirect : 'error' } ) ;
2158 if ( ! response . ok ) {
2259 throw new Error ( `Unexpected response status ${ response . status } ` ) ;
2360 }
0 commit comments