Skip to content

Commit 6511380

Browse files
committed
fix security manager blocking dataurl extensions
1 parent a2c6b8f commit 6511380

3 files changed

Lines changed: 51 additions & 14 deletions

File tree

src/extension-support/extension-manager.js

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,15 @@ class ExtensionManager {
225225
* @param {string} extensionURL - the URL for the extension to load OR the ID of an internal extension
226226
* @returns {Promise} resolved once the extension is loaded and initialized or rejected on failure
227227
*/
228+
_isLocalExtensionURL (extensionURL) {
229+
try {
230+
const parsedURL = new URL(extensionURL);
231+
return parsedURL.protocol === 'data:' || parsedURL.protocol === 'file:';
232+
} catch (e) {
233+
return false;
234+
}
235+
}
236+
228237
async loadExtensionURL (extensionURL) {
229238
if (this.isBuiltinExtension(extensionURL)) {
230239
this.loadExtensionIdSync(extensionURL);
@@ -244,12 +253,14 @@ class ExtensionManager {
244253

245254
this.loadingAsyncExtensions++;
246255

247-
const sandboxMode = await this.securityManager.getSandboxMode(extensionURL);
248-
const rewritten = await this.securityManager.rewriteExtensionURL(extensionURL);
256+
// data: and file: URLs are local (inline text or local file) — no sandbox, no security checks
257+
const isLocal = this._isLocalExtensionURL(extensionURL);
258+
const sandboxMode = isLocal ? 'unsandboxed' : await this.securityManager.getSandboxMode(extensionURL);
259+
const rewritten = isLocal ? extensionURL : await this.securityManager.rewriteExtensionURL(extensionURL);
249260

250261
if (sandboxMode === 'unsandboxed') {
251262
const {load} = require('./tw-unsandboxed-extension-runner');
252-
const extensionObjects = await load(rewritten, this.vm)
263+
const extensionObjects = await load(rewritten, this.vm, {bypassSecurity: isLocal})
253264
.catch(error => this._failedLoadingExtensionScript(error));
254265
const fakeWorkerId = this.nextExtensionWorker++;
255266
this.workerURLs[fakeWorkerId] = extensionURL;

src/extension-support/tw-unsandboxed-extension-runner.js

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ const parseURL = url => {
2121
/**
2222
* Sets up the global.Scratch API for an unsandboxed extension.
2323
* @param {VirtualMachine} vm
24+
* @param {{bypassSecurity?: boolean}} [options]
2425
* @returns {Promise<object[]>} Resolves with a list of extension objects when Scratch.extensions.register is called.
2526
*/
26-
const setupUnsandboxedExtensionAPI = vm => new Promise(resolve => {
27+
const setupUnsandboxedExtensionAPI = (vm, options = {}) => new Promise(resolve => {
2728
const extensionObjects = [];
2829
const register = extensionObject => {
2930
extensionObjects.push(extensionObject);
@@ -39,7 +40,10 @@ const setupUnsandboxedExtensionAPI = vm => new Promise(resolve => {
3940
Scratch.vm = vm;
4041
Scratch.renderer = vm.runtime.renderer;
4142

43+
const bypass = options.bypassSecurity === true;
44+
4245
Scratch.canFetch = async url => {
46+
if (bypass) return true;
4347
const parsed = parseURL(url);
4448
if (!parsed) {
4549
return false;
@@ -52,6 +56,7 @@ const setupUnsandboxedExtensionAPI = vm => new Promise(resolve => {
5256
};
5357

5458
Scratch.canOpenWindow = async url => {
59+
if (bypass) return true;
5560
const parsed = parseURL(url);
5661
if (!parsed) {
5762
return false;
@@ -65,6 +70,7 @@ const setupUnsandboxedExtensionAPI = vm => new Promise(resolve => {
6570
};
6671

6772
Scratch.canRedirect = async url => {
73+
if (bypass) return true;
6874
const parsed = parseURL(url);
6975
if (!parsed) {
7076
return false;
@@ -77,17 +83,33 @@ const setupUnsandboxedExtensionAPI = vm => new Promise(resolve => {
7783
return vm.securityManager.canRedirect(parsed.href);
7884
};
7985

80-
Scratch.canRecordAudio = async () => vm.securityManager.canRecordAudio();
86+
Scratch.canRecordAudio = async () => {
87+
if (bypass) return true;
88+
return vm.securityManager.canRecordAudio();
89+
};
8190

82-
Scratch.canRecordVideo = async () => vm.securityManager.canRecordVideo();
91+
Scratch.canRecordVideo = async () => {
92+
if (bypass) return true;
93+
return vm.securityManager.canRecordVideo();
94+
};
8395

84-
Scratch.canReadClipboard = async () => vm.securityManager.canReadClipboard();
96+
Scratch.canReadClipboard = async () => {
97+
if (bypass) return true;
98+
return vm.securityManager.canReadClipboard();
99+
};
85100

86-
Scratch.canNotify = async () => vm.securityManager.canNotify();
101+
Scratch.canNotify = async () => {
102+
if (bypass) return true;
103+
return vm.securityManager.canNotify();
104+
};
87105

88-
Scratch.canGeolocate = async () => vm.securityManager.canGeolocate();
106+
Scratch.canGeolocate = async () => {
107+
if (bypass) return true;
108+
return vm.securityManager.canGeolocate();
109+
};
89110

90111
Scratch.canEmbed = async url => {
112+
if (bypass) return true;
91113
const parsed = parseURL(url);
92114
if (!parsed) {
93115
return false;
@@ -96,6 +118,7 @@ const setupUnsandboxedExtensionAPI = vm => new Promise(resolve => {
96118
};
97119

98120
Scratch.canDownload = async (url, name) => {
121+
if (bypass) return true;
99122
const parsed = parseURL(url);
100123
if (!parsed) {
101124
return false;
@@ -200,9 +223,10 @@ const teardownUnsandboxedExtensionAPI = () => {
200223
* Load an unsandboxed extension from an arbitrary URL. This is dangerous.
201224
* @param {string} extensionURL
202225
* @param {Virtualmachine} vm
226+
* @param {{bypassSecurity?: boolean}} [options]
203227
* @returns {Promise<object[]>} Resolves with a list of extension objects if the extension was loaded successfully.
204228
*/
205-
const loadUnsandboxedExtension = (extensionURL, vm) => new Promise((resolve, reject) => {
229+
const loadUnsandboxedExtension = (extensionURL, vm, options = {}) => new Promise((resolve, reject) => {
206230
let isResolved = false;
207231
let registrationTimeout = null;
208232
let overallTimeout = null;
@@ -215,7 +239,7 @@ const loadUnsandboxedExtension = (extensionURL, vm) => new Promise((resolve, rej
215239
fn(arg);
216240
};
217241

218-
setupUnsandboxedExtensionAPI(vm)
242+
setupUnsandboxedExtensionAPI(vm, options)
219243
.then(extensionObjects => settle(resolve, extensionObjects))
220244
.catch(error => {
221245
error.url = extensionURL;
@@ -293,9 +317,9 @@ const prefetchExtensionScript = extensionURL => {
293317
// only let one extension register at a time. The script download is started up front (in
294318
// parallel across extensions) so only the registration step is serialized.
295319
const limiter = new AsyncLimiter(loadUnsandboxedExtension, 1);
296-
const load = (extensionURL, vm) => {
320+
const load = (extensionURL, vm, options = {}) => {
297321
prefetchExtensionScript(extensionURL);
298-
return limiter.do(extensionURL, vm);
322+
return limiter.do(extensionURL, vm, options);
299323
};
300324

301325
module.exports = {

src/virtual-machine.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -904,7 +904,9 @@ class VirtualMachine extends EventEmitter {
904904
if (!url) {
905905
throw new Error(`Unknown extension: ${extensionID}`);
906906
}
907-
if (await this.securityManager.canLoadExtensionFromProject(url)) {
907+
// data: and file: URLs are local — always allow without security manager check
908+
const isLocal = url.startsWith('data:') || url.startsWith('file:');
909+
if (isLocal || await this.securityManager.canLoadExtensionFromProject(url)) {
908910
extensionPromises.push(this.extensionManager.loadExtensionURL(url));
909911
} else {
910912
throw new Error(`Permission to load extension denied: ${extensionID}`);

0 commit comments

Comments
 (0)