Query security manager extension permissions concurrently (#400)

This commit is contained in:
Thomas Weber authored and GitHub committed 2026-10-31 15:32:29 -05:00
1 parent cecd467dd2
commit 0cb82afe62
3 files changed
+53 -11

No files matched your search

+21 -10
View File
@@ -772,15 +772,15 @@ class VirtualMachine extends EventEmitter {
*/
async _loadExtensions (extensionIDs, extensionURLs = new Map()) {
const defaultExtensionURLs = require('./extension-support/tw-default-extension-urls');
const extensionPromises = [];
const builtinExtensionIDs = [];
const customExtensionIDs = [];
const customExtensionURLs = [];
for (const extensionID of extensionIDs) {
if (this.extensionManager.isExtensionLoaded(extensionID)) {
// Already loaded
} else if (this.extensionManager.isBuiltinExtension(extensionID)) {
// Builtin extension
this.extensionManager.loadExtensionIdSync(extensionID);
builtinExtensionIDs.push(extensionID);
} else {
// Custom extension
let url = extensionURLs.get(extensionID);
if (!url && Object.prototype.hasOwnProperty.call(defaultExtensionURLs, extensionID)) {
url = defaultExtensionURLs[extensionID];
@@ -788,14 +788,25 @@ class VirtualMachine extends EventEmitter {
if (!url) {
throw new Error(`Unknown extension: ${extensionID}`);
}
if (await this.securityManager.canLoadExtensionFromProject(url)) {
extensionPromises.push(this.extensionManager.loadExtensionURL(url));
} else {
throw new Error(`Permission to load extension denied: ${extensionID}`);
}
customExtensionIDs.push(extensionID);
customExtensionURLs.push(url);
}
}
return Promise.all(extensionPromises);
// Ask about every custom extension concurrently, before loading any of them, so that the
// security manager can combine them into a single prompt if it wants to.
const permissions = await Promise.all(
customExtensionURLs.map(url => this.securityManager.canLoadExtensionFromProject(url))
);
const deniedExtensionIDs = customExtensionIDs.filter((_, i) => !permissions[i]);
if (deniedExtensionIDs.length > 0) {
throw new Error(`Permission to load extension denied: ${deniedExtensionIDs.join(', ')}`);
}
for (const extensionID of builtinExtensionIDs) {
this.extensionManager.loadExtensionIdSync(extensionID);
}
return Promise.all(customExtensionURLs.map(url => this.extensionManager.loadExtensionURL(url)));
}
/**
+2 -1
View File
@@ -92,9 +92,10 @@ test('Loading project uses default extension URLs', async () => {
}
});
assert.deepEqual(events, [
// All permission checks happen before any extensions are loaded
'canLoadExtensionFromProject https://extensions.turbowarp.org/lab/text.js',
'loadExtensionURL https://extensions.turbowarp.org/lab/text.js',
'canLoadExtensionFromProject https://example.com/box2d.js',
'loadExtensionURL https://extensions.turbowarp.org/lab/text.js',
'loadExtensionURL https://example.com/box2d.js'
]);
});
+30
View File
@@ -54,6 +54,36 @@ test('Allow both extensions', async () => {
assert.deepEqual(new Set(loadedExtensions), new Set([FETCH_EXTENSION, BITWISE_EXTENSION]));
});
test('Asks about every extension concurrently before loading any', async () => {
const vm = new VirtualMachine();
const loadedExtensions = [];
vm.extensionManager.loadExtensionURL = url => {
loadedExtensions.push(url);
return Promise.resolve();
};
const pending = [];
let resolveAllAsked;
const allAsked = new Promise(resolve => {
resolveAllAsked = resolve;
});
vm.securityManager.canLoadExtensionFromProject = url => new Promise(resolve => {
pending.push({url, resolve});
if (pending.length === 2) {
resolveAllAsked();
}
});
const loadPromise = vm.loadProject(testProject);
// Neither request is answered until both have been made
await allAsked;
assert.deepEqual(new Set(pending.map(i => i.url)), new Set([FETCH_EXTENSION, BITWISE_EXTENSION]));
assert.deepEqual(loadedExtensions, []);
for (const {resolve} of pending) {
resolve(true);
}
await loadPromise;
assert.deepEqual(new Set(loadedExtensions), new Set([FETCH_EXTENSION, BITWISE_EXTENSION]));
});
test('canFetch', async () => {
const vm = new VirtualMachine();
setupUnsandboxedExtensionAPI(vm);