@@ -187,36 +187,75 @@ const isChildPath = (parent, child) => {
187187 return ! ! relative && ! relative . startsWith ( '..' ) && ! path . isAbsolute ( relative ) ;
188188} ;
189189
190+ /** @type {Set<string> } */
191+ const allFileIDs = new Set ( ) ;
192+
193+ /**
194+ * @returns {string } A unique string.
195+ */
196+ const generateFileId = ( ) => {
197+ let result ;
198+ let tries = 0 ;
199+
200+ do {
201+ tries ++ ;
202+ if ( tries > 50 ) {
203+ // Should never happen...
204+ throw new Error ( 'Failed to generate file ID' ) ;
205+ }
206+
207+ result = 'desktop_file_id{' ;
208+
209+ // >200 bits of randomness; impractical to brute force.
210+ // Math.random() is not cryptographically secure, but even if someone can reverse it, they would
211+ // still only be able to access files that were already opened, so impact is not that big.
212+ const soup = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' ;
213+ for ( let i = 0 ; i < 40 ; i ++ ) {
214+ result += soup [ Math . floor ( Math . random ( ) * soup . length ) ] ;
215+ }
216+
217+ result += '}' ;
218+ } while ( allFileIDs . has ( result ) ) ;
219+
220+ allFileIDs . add ( result ) ;
221+ return result ;
222+ } ;
223+
190224class EditorWindow extends ProjectRunningWindow {
191225 /**
192- * @param {OpenedFile|null } file
226+ * @param {OpenedFile|null } initialFile
193227 * @param {boolean } isInitiallyFullscreen
194228 */
195- constructor ( file , isInitiallyFullscreen ) {
229+ constructor ( initialFile , isInitiallyFullscreen ) {
196230 super ( ) ;
197231
198- // This file ID system is not quite perfect. Ideally we would completely revoke permission to access
199- // old projects after you load the next one, but our handling of file handles in scratch-gui is
200- // pretty bad right now, so this is the best compromise.
201- this . openedFiles = [ ] ;
202- this . activeFileIndex = - 1 ;
232+ /**
233+ * Ideally we would revoke access after loading a new project, but our file handle handling in
234+ * the GUI isn't robust enough for that yet. We do at least use random file handle IDs which
235+ * makes it much harder for malicious code in the renderer process to enumerate all previously
236+ * opened IDs and overwrite them.
237+ * @type {Map<string, OpenedFile> }
238+ */
239+ this . openedFiles = new Map ( ) ;
240+ this . activeFileId = null ;
203241
204- if ( file !== null ) {
205- this . openedFiles . push ( file ) ;
206- this . activeFileIndex = 0 ;
242+ if ( initialFile !== null ) {
243+ this . activeFileId = generateFileId ( ) ;
244+ this . openedFiles . set ( this . activeFileId , initialFile ) ;
207245 }
208246
209247 this . openedProjectAt = Date . now ( ) ;
210248
211- const getFileByIndex = ( index ) => {
212- if ( typeof index !== 'number' ) {
213- throw new Error ( 'File ID not number' ) ;
214- }
215- const value = this . openedFiles [ index ] ;
216- if ( ! ( value instanceof OpenedFile ) ) {
249+ /**
250+ * @param {string } id
251+ * @returns {OpenedFile }
252+ * @throws if invalid ID
253+ */
254+ const getFileById = ( id ) => {
255+ if ( ! this . openedFiles . has ( id ) ) {
217256 throw new Error ( 'Invalid file ID' ) ;
218257 }
219- return this . openedFiles [ index ] ;
258+ return this . openedFiles . get ( id ) ;
220259 } ;
221260
222261 this . window . webContents . on ( 'will-prevent-unload' , ( event ) => {
@@ -263,14 +302,11 @@ class EditorWindow extends ProjectRunningWindow {
263302 } ) ;
264303
265304 ipc . handle ( 'get-initial-file' , ( ) => {
266- if ( this . activeFileIndex === - 1 ) {
267- return null ;
268- }
269- return this . activeFileIndex ;
305+ return this . activeFileId ;
270306 } ) ;
271307
272- ipc . handle ( 'get-file' , async ( event , index ) => {
273- const file = getFileByIndex ( index ) ;
308+ ipc . handle ( 'get-file' , async ( event , id ) => {
309+ const file = getFileById ( id ) ;
274310 const { name, data} = await file . read ( ) ;
275311 return {
276312 name,
@@ -301,18 +337,18 @@ class EditorWindow extends ProjectRunningWindow {
301337 this . window . setDocumentEdited ( changed ) ;
302338 } ) ;
303339
304- ipc . handle ( 'opened-file' , ( event , index ) => {
305- const file = getFileByIndex ( index ) ;
340+ ipc . handle ( 'opened-file' , ( event , id ) => {
341+ const file = getFileById ( id ) ;
306342 if ( file . type !== TYPE_FILE ) {
307343 throw new Error ( 'Not a file' ) ;
308344 }
309- this . activeFileIndex = index ;
345+ this . activeFileId = id ;
310346 this . openedProjectAt = Date . now ( ) ;
311347 this . window . setRepresentedFilename ( file . path ) ;
312348 } ) ;
313349
314350 ipc . handle ( 'closed-file' , ( ) => {
315- this . activeFileIndex = - 1 ;
351+ this . activeFileId = null ;
316352 this . window . setRepresentedFilename ( '' ) ;
317353 } ) ;
318354
@@ -331,14 +367,16 @@ class EditorWindow extends ProjectRunningWindow {
331367 return null ;
332368 }
333369
334- const file = result . filePaths [ 0 ] ;
335- settings . lastDirectory = path . dirname ( file ) ;
370+ const filePath = result . filePaths [ 0 ] ;
371+ settings . lastDirectory = path . dirname ( filePath ) ;
336372 await settings . save ( ) ;
337373
338- this . openedFiles . push ( new OpenedFile ( TYPE_FILE , file ) ) ;
374+ const id = generateFileId ( ) ;
375+ this . openedFiles . set ( id , new OpenedFile ( TYPE_FILE , filePath ) ) ;
376+
339377 return {
340- id : this . openedFiles . length - 1 ,
341- name : path . basename ( file )
378+ id,
379+ name : path . basename ( filePath )
342380 } ;
343381 } ) ;
344382
@@ -356,30 +394,32 @@ class EditorWindow extends ProjectRunningWindow {
356394 return null ;
357395 }
358396
359- const file = result . filePath ;
397+ const filePath = result . filePath ;
360398
361- const unsafePath = getUnsafePaths ( ) . find ( i => isChildPath ( i . path , file ) ) ;
399+ const unsafePath = getUnsafePaths ( ) . find ( i => isChildPath ( i . path , filePath ) ) ;
362400 if ( unsafePath ) {
363- // No need to wait for the message box to close
401+ // No need to block until the message box is closed
364402 dialog . showMessageBox ( this . window , {
365403 type : 'error' ,
366404 title : APP_NAME ,
367405 message : translate ( 'unsafe-path.title' ) ,
368406 detail : translate ( `unsafe-path.details` )
369407 . replace ( '{APP_NAME}' , unsafePath . app )
370- . replace ( '{file}' , file ) ,
408+ . replace ( '{file}' , filePath ) ,
371409 noLink : true
372410 } ) ;
373411 return null ;
374412 }
375413
376- settings . lastDirectory = path . dirname ( file ) ;
414+ settings . lastDirectory = path . dirname ( filePath ) ;
377415 await settings . save ( ) ;
378416
379- this . openedFiles . push ( new OpenedFile ( TYPE_FILE , file ) ) ;
417+ const id = generateFileId ( ) ;
418+ this . openedFiles . set ( id , new OpenedFile ( TYPE_FILE , filePath ) ) ;
419+
380420 return {
381- id : this . openedFiles . length - 1 ,
382- name : path . basename ( file )
421+ id,
422+ name : path . basename ( filePath )
383423 } ;
384424 } ) ;
385425
@@ -390,8 +430,8 @@ class EditorWindow extends ProjectRunningWindow {
390430 } ;
391431 } ) ;
392432
393- ipc . on ( 'start-write-stream' , async ( startEvent , index ) => {
394- const file = getFileByIndex ( index ) ;
433+ ipc . on ( 'start-write-stream' , async ( startEvent , id ) => {
434+ const file = getFileById ( id ) ;
395435 if ( file . type !== TYPE_FILE ) {
396436 throw new Error ( 'Not a file' ) ;
397437 }
0 commit comments