Skip to content

Commit ada6322

Browse files
committed
feat: support takedown of extensions via updates.phcode.io/extension_takedown.json
1 parent abdbc9e commit ada6322

File tree

8 files changed

+141
-16
lines changed

8 files changed

+141
-16
lines changed

docs/API-Reference/utils/NodeUtils.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,26 @@ This validates that the system-wide license file exists, contains valid JSON, an
150150

151151
**Kind**: global function
152152
**Returns**: <code>Promise.&lt;boolean&gt;</code> - - Resolves with `true` if the device is licensed, `false` otherwise.
153+
<a name="getOSUserName"></a>
154+
155+
## getOSUserName() ⇒ <code>Promise.&lt;string&gt;</code>
156+
Retrieves the operating system username of the current user.
157+
This method is only available in native apps.
158+
159+
**Kind**: global function
160+
**Returns**: <code>Promise.&lt;string&gt;</code> - A promise that resolves to the OS username of the current user.
161+
**Throws**:
162+
163+
- <code>Error</code> Throws an error if called in a browser environment.
164+
165+
<a name="getSystemSettingsDir"></a>
166+
167+
## getSystemSettingsDir() ⇒ <code>Promise.&lt;string&gt;</code>
168+
Retrieves the directory path for system settings. This method is applicable to native apps only.
169+
170+
**Kind**: global function
171+
**Returns**: <code>Promise.&lt;string&gt;</code> - A promise that resolves to the path of the system settings directory.
172+
**Throws**:
173+
174+
- <code>Error</code> If the method is called in browser app.
175+

src/config.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"extension_store_url": "https://store.core.ai/src/",
3030
"app_notification_url": "assets/notifications/dev/",
3131
"app_update_url": "https://updates.phcode.io/tauri/update-latest-experimental-build.json",
32+
"extensionTakedownURL": "https://updates.phcode.io/extension_takedown.json",
3233
"linting.enabled_by_default": true,
3334
"build_timestamp": "",
3435
"googleAnalyticsID": "G-P4HJFPDB76",

src/extensibility/ExtensionManager.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,6 +1006,7 @@ define(function (require, exports, module) {
10061006
exports.updateExtensions = updateExtensions;
10071007
exports.getAvailableUpdates = getAvailableUpdates;
10081008
exports.cleanAvailableUpdates = cleanAvailableUpdates;
1009+
exports.isExtensionTakenDown = ExtensionLoader.isExtensionTakenDown;
10091010

10101011
exports.ENABLED = ENABLED;
10111012
exports.DISABLED = DISABLED;

src/extensibility/ExtensionManagerView.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ define(function (require, exports, module) {
4545
PreferencesManager = require("preferences/PreferencesManager"),
4646
DefaultExtensions = JSON.parse(require("text!extensions/default/DefaultExtensions.json")),
4747
warnExtensionIDs = new Set(DefaultExtensions.warnExtensionStoreExtensions.extensionIDs),
48-
dontLoadExtensionIDs = new Set(DefaultExtensions.dontLoadExtensions.extensionIDs),
4948
Metrics = require("utils/Metrics");
5049

5150

@@ -365,7 +364,7 @@ define(function (require, exports, module) {
365364
(entry.installInfo.metadata.name === ThemeManager.getCurrentTheme().name);
366365

367366
context.defaultFeature = warnExtensionIDs.has(info.metadata.name);
368-
context.isDeprecatedExtension = dontLoadExtensionIDs.has(info.metadata.name);
367+
context.isDeprecatedExtension = ExtensionManager.isExtensionTakenDown(info.metadata.name);
369368

370369
context.allowInstall = context.isCompatible && !context.isInstalled;
371370

src/extensibility/ExtensionManagerViewModel.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,7 @@ define(function (require, exports, module) {
3030
registry_utils = require("extensibility/registry_utils"),
3131
EventDispatcher = require("utils/EventDispatcher"),
3232
Strings = require("strings"),
33-
PreferencesManager = require("preferences/PreferencesManager"),
34-
DefaultExtensions = JSON.parse(require("text!extensions/default/DefaultExtensions.json")),
35-
dontLoadExtensionIDs = new Set(DefaultExtensions.dontLoadExtensions.extensionIDs);
33+
PreferencesManager = require("preferences/PreferencesManager");
3634

3735
/**
3836
* @private
@@ -310,7 +308,7 @@ define(function (require, exports, module) {
310308

311309
})
312310
.filter(function (entry) {
313-
return !dontLoadExtensionIDs.has(entry.registryInfo.metadata.name);
311+
return !ExtensionManager.isExtensionTakenDown(entry.registryInfo.metadata.name);
314312
})
315313
.map(function (entry) {
316314
return entry.registryInfo.metadata.name;
@@ -562,7 +560,7 @@ define(function (require, exports, module) {
562560
self.extensions[key].installInfo.locationType === ExtensionManager.LOCATION_DEFAULT;
563561
})
564562
.filter(function (key) {
565-
return !dontLoadExtensionIDs.has(key);
563+
return !ExtensionManager.isExtensionTakenDown(key);
566564
});
567565
this._sortFullSet();
568566
this._setInitialFilter();

src/extensions/default/DefaultExtensions.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@
3838
]
3939
},
4040
"dontLoadExtensions": {
41-
"note": "Use this only for first party extensions. we should always allow third party extension loading.",
42-
"extensionIDs": []
41+
"note": "To take down any compromised extensions, see `extensionTakedownURL` in repo"
4342
}
4443
}

src/nls/root/strings.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -809,7 +809,7 @@ define({
809809
"EXTENSION_INCOMPATIBLE_NEWER": "This extension requires a newer version of {APP_NAME}.",
810810
"EXTENSION_INCOMPATIBLE_OLDER": "This extension currently only works with older versions of {APP_NAME}.",
811811
"EXTENSION_DEFAULT_FEATURE_PRESENT": "You may not need this extension. {APP_NAME} already has this feature.",
812-
"EXTENSION_DEPRECATED_NOT_LOADED": "Extension is deprecated and not loaded.",
812+
"EXTENSION_DEPRECATED_NOT_LOADED": "Extension not loaded. It is either deprecated or insecure.",
813813
"EXTENSION_LATEST_INCOMPATIBLE_NEWER": "Version {0} of this extension requires a newer version of {APP_NAME}. But you can install the earlier version {1}.",
814814
"EXTENSION_LATEST_INCOMPATIBLE_OLDER": "Version {0} of this extension only works with older versions of {APP_NAME}. But you can install the earlier version {1}.",
815815
"EXTENSION_NO_DESCRIPTION": "No description",

src/utils/ExtensionLoader.js

Lines changed: 110 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)