@@ -56,8 +56,97 @@ define(function (require, exports, module) {
5656 PathUtils = require ( "thirdparty/path-utils/path-utils" ) ,
5757 DefaultExtensions = JSON . parse ( require ( "text!extensions/default/DefaultExtensions.json" ) ) ;
5858
59+ // takedown/dont load extensions that are compromised at app start - start
60+ const EXTENSION_TAKEDOWN_LOCALSTORAGE_KEY = "PH_EXTENSION_TAKEDOWN_LIST" ;
61+
62+ function _getTakedownListLS ( ) {
63+ try {
64+ let list = localStorage . getItem ( EXTENSION_TAKEDOWN_LOCALSTORAGE_KEY ) ;
65+ if ( list ) {
66+ list = JSON . parse ( list ) ;
67+ if ( Array . isArray ( list ) ) {
68+ return list ;
69+ }
70+ }
71+ } catch ( e ) {
72+ console . error ( e ) ;
73+ }
74+ return [ ] ;
75+ }
76+
77+ const loadedExtensionIDs = new Set ( ) ;
78+ let takedownExtensionList = new Set ( _getTakedownListLS ( ) ) ;
79+
80+ const EXTENSION_TAKEDOWN_URL = brackets . config . extensionTakedownURL ;
81+
82+ function _anyTakenDownExtensionLoaded ( ) {
83+ if ( takedownExtensionList . size === 0 || loadedExtensionIDs . size === 0 ) {
84+ return [ ] ;
85+ }
86+ let smaller ;
87+ let larger ;
88+
89+ if ( takedownExtensionList . size < loadedExtensionIDs . size ) {
90+ smaller = takedownExtensionList ;
91+ larger = loadedExtensionIDs ;
92+ } else {
93+ smaller = loadedExtensionIDs ;
94+ larger = takedownExtensionList ;
95+ }
96+
97+ const matches = [ ] ;
98+
99+ for ( const id of smaller ) {
100+ if ( larger . has ( id ) ) {
101+ matches . push ( id ) ;
102+ }
103+ }
104+
105+ return matches ;
106+ }
107+
108+ function fetchWithTimeout ( url , ms ) {
109+ const c = new AbortController ( ) ;
110+ const t = setTimeout ( ( ) => c . abort ( ) , ms ) ;
111+ return fetch ( url , { signal : c . signal } ) . finally ( ( ) => clearTimeout ( t ) ) ;
112+ }
113+
114+ // we dont want a restart after user does too much in the app causing data loss. So we wont reload after 20 seconds.
115+ fetchWithTimeout ( EXTENSION_TAKEDOWN_URL , 20000 )
116+ . then ( response => {
117+ if ( ! response . ok ) {
118+ throw new Error ( `HTTP ${ response . status } - ${ response . statusText } ` ) ;
119+ }
120+ return response . json ( ) ;
121+ } )
122+ . then ( data => {
123+ console . log ( 'Extension takedown data:' , data ) ;
124+ if ( ! Array . isArray ( data ) || ! data . every ( x => typeof x === "string" ) ) {
125+ console . error ( "Takedown list must be an array of strings." ) ;
126+ return ;
127+ }
128+ const dataToWrite = JSON . stringify ( data ) ;
129+ localStorage . setItem ( EXTENSION_TAKEDOWN_LOCALSTORAGE_KEY , dataToWrite ) ;
130+ takedownExtensionList = new Set ( data ) ;
131+ const compromisedExtensionsLoaded = _anyTakenDownExtensionLoaded ( ) ;
132+ if ( ! compromisedExtensionsLoaded . length ) {
133+ return ;
134+ }
135+ // if we are here, we have already loaded some compromised extensions. we need to reload app as soon as
136+ // possible. no await after this. all sync js calls to prevent extension from tampering with this list.
137+ const writtenData = localStorage . getItem ( EXTENSION_TAKEDOWN_LOCALSTORAGE_KEY ) ;
138+ if ( writtenData !== dataToWrite ) {
139+ // the write did not succeded. local storage write can fail if storage full, if so we may cause infinite
140+ // reloads here if we dont do the check.
141+ console . error ( "Failed to write taken down extension to localstorage" ) ;
142+ return ;
143+ }
144+ location . reload ( ) ;
145+ } )
146+ . catch ( console . error ) ;
147+ // takedown/dont load extensions that are compromised at app start - end
148+
59149 const desktopOnlyExtensions = DefaultExtensions . desktopOnly ;
60- const dontLoadExtensionIDs = new Set ( DefaultExtensions . dontLoadExtensions . extensionIDs ) ;
61150 const DefaultExtensionsList = Phoenix . isNativeApp ?
62151 [ ...DefaultExtensions . defaultExtensionsList , ...desktopOnlyExtensions ] :
63152 DefaultExtensions . defaultExtensionsList ;
@@ -417,15 +506,20 @@ define(function (require, exports, module) {
417506
418507 return promise
419508 . then ( function ( metadata ) {
509+ if ( isExtensionTakenDown ( metadata . name ) ) {
510+ logger . leaveTrail ( "skip load taken down extension: " + metadata . name ) ;
511+ console . warn ( "skip load taken down extension: " + metadata . name ) ;
512+ return new $ . Deferred ( ) . reject ( "disabled" ) . promise ( ) ;
513+ }
514+
515+ if ( metadata . name ) {
516+ loadedExtensionIDs . add ( metadata . name ) ;
517+ }
518+
420519 // No special handling for themes... Let the promise propagate into the ExtensionManager
421520 if ( metadata && metadata . theme ) {
422521 return ;
423522 }
424- if ( dontLoadExtensionIDs . has ( metadata . name ) ) {
425- logger . leaveTrail ( "skipping extension in dontLoadExtensions list: " + metadata . name ) ;
426- console . warn ( "skipping extension in dontLoadExtensions list: " + metadata . name ) ;
427- return new $ . Deferred ( ) . reject ( "disabled" ) . promise ( ) ;
428- }
429523
430524 if ( ! metadata . disabled ) {
431525 return loadExtensionModule ( name , config , entryPoint , metadata ) ;
@@ -929,6 +1023,15 @@ define(function (require, exports, module) {
9291023 return promise ;
9301024 }
9311025
1026+ function isExtensionTakenDown ( extensionID ) {
1027+ if ( ! extensionID ) {
1028+ // extensions without id can happen with local development. these are never distributed in store.
1029+ // so safe to return false here.
1030+ return false ;
1031+ }
1032+ return takedownExtensionList . has ( extensionID ) ;
1033+ }
1034+
9321035
9331036 EventDispatcher . makeEventDispatcher ( exports ) ;
9341037
@@ -952,6 +1055,7 @@ define(function (require, exports, module) {
9521055 exports . testExtension = testExtension ;
9531056 exports . loadAllExtensionsInNativeDirectory = loadAllExtensionsInNativeDirectory ;
9541057 exports . loadExtensionFromNativeDirectory = loadExtensionFromNativeDirectory ;
1058+ exports . isExtensionTakenDown = isExtensionTakenDown ;
9551059 exports . testAllExtensionsInNativeDirectory = testAllExtensionsInNativeDirectory ;
9561060 exports . testAllDefaultExtensions = testAllDefaultExtensions ;
9571061 exports . EVENT_EXTENSION_LOADED = EVENT_EXTENSION_LOADED ;
0 commit comments