Skip to main content

Unsandboxed extensions

Unsandboxed extensions run as plain <script> tags in the main window rather than in a sandbox. They have access to a lot of new powers and responsibilities that we will discuss below.

URL restrictions

To protect users from malicious extensions, extensions loaded from URLs will only run unsandboxed if their URL begins with one of these exactly:

  • https://extensions.turbowarp.org/
  • http://localhost:8000/

As you don't have control over extensions.turbowarp.org, you will have to use the latter option. For this, configure your local HTTP server to run on port 8000 instead of what you've been using so far.

When manually loading an extension from a file or JavaScript source code, there is an option to load the extension without the sandbox. This option to force an extension to run unsandboxed does not exist when using URLs due to security concerns.

Syntax

The syntax for unsandboxed extensions is very familiar but has some differences. Technically, if you just copy and paste your old sandboxed extensions as unsandboxed extensions, it will appear to just work. However, this is dangerous and is likely to cause bugs later.

If your sandboxed extension has code like like this:

// Old sandboxed extensions (worker or <iframe> sandbox):
class MyExtension {
getInfo () {
return { /* ... */ };
}
}
Scratch.extensions.register(new MyExtension());

Or if your extension uses an old "plugin" mechanism, such as this one: (if you don't recognize this code then don't worry about it)

class MyExtension {
getInfo () {
return { /* ... */ };
}
}
(function() {
var extensionInstance = new MyExtension(window.vm.extensionManager.runtime)
var serviceName = window.vm.extensionManager._registerInternalExtension(extensionInstance)
window.vm.extensionManager._loadedExtensions.set(extensionInstance.getInfo().id, serviceName)
})();

The unsandboxed version would have code like this:

(function(Scratch) {
'use strict';
class MyExtension {
getInfo () {
return { /* ... */ };
}
}
Scratch.extensions.register(new MyExtension());
})(Scratch);

Using this template prevents unsandboxed extensions from interfering with each other when they try to define variables, classes, or functions with the same name. By requiring everything to be defined in an immediately-invoked-function-expression (IIFE) and enabling strict mode, we prevent variables from accidentally leaking to the global scope.

All functions and variables defined by the extension must be defined within the IIFE. Additionally, each extension must make sure to use its own personal copy of the Scratch API, which this template does automatically.

An interesting thing to note about this template is that it is backward compatible with sandboxed extensions. As long as the extension doesn't use any of the features given to unsandboxed extensions, it will continue to work the same as a sandboxed extension.

A more complete example

Here you can see a complete unsandboxed extension:

unsandboxed/hello-world-unsandboxed.js - Try this extension
(function(Scratch) {
'use strict';

if (!Scratch.extensions.unsandboxed) {
throw new Error('This Hello World example must run unsandboxed');
}

class HelloWorld {
getInfo() {
return {
id: 'helloworldunsandboxed',
name: 'Unsandboxed Hello World',
blocks: [
{
opcode: 'hello',
blockType: Scratch.BlockType.REPORTER,
text: 'Hello!'
}
]
};
}
hello() {
return 'World!';
}
}
Scratch.extensions.register(new HelloWorld());
})(Scratch);

If you're using a local HTTP server, save this so you can access it through the server, then load the exact URL http://localhost:8000/hello-world-unsandboxed.js in TurboWarp. If nothing appears, see the developer console. If you see an error that the extension must be run unsandboxed, most likely you are using an old version of TurboWarp or you didn't load it from a URL that starts with http://localhost:8000/ exactly. 127.0.0.1 and 0.0.0.0 won't work! It must be localhost, port 8000 exactly.

If you're just using files, make sure to check the "Run extension without sandbox" box each time you load the extension.

Create a new empty project with a repeat (30) loop that adds the "hello" block to a list. Notice that it now runs instantly while the sandboxed version would've taken at least a second.

Observe that the majority of the code is still identical: You still create a class, then call Scratch.extensions.register(), then Scratch calls getInfo() which returns the same type of object. Just the surrounding template is different.

Increased power brings increased responsibility

Before we talk about the new APIs, we want to note some additional requirements for unsandboxed extensions:

  • Blocks must not throw errors. While sandboxed extensions could, unsandboxed extensions that do this may break scripts.
  • Input and boolean blocks must return a valid value. While sandboxed extensions are free to neglect this, unsandboxed extensions that don't return proper values (string, number, or boolean) can break scripts in unknown ways.
  • Blocks must not get stuck in infinite loops. While sandboxed extensions will usually not be able to freeze the entire window if they get stuck in a loop, unsandboxed extensions will. This can result in data loss.

Accessing Scratch internals

The big thing that unsandboxed extensions can do is directly access Scratch internals.

  const vm = Scratch.vm;

That's full access to the actual Scratch VM object. There is a lot you can do with this.

Remember -- every variable declaration must happen inside the IIFE.

// GOOD CODE
(function(Scratch) {
const vm = Scratch.vm;
// ...
}(Scratch));

// BAD CODE
const vm = Scratch.vm;
(function(Scratch) {
// ...
}(Scratch));

Dig around for a while to find what you're looking for. Your developer tools will be immensely useful as you can access Scratch from there after an extension is loaded, or use the other debugging global variables that are available (but please don't use those in extensions). You may find the scratch-vm source code or @turbowarp/types to be useful resources.

Here is an example of an extension that uses Scratch.vm to toggle turbo mode, similar to the "runtime options" extension on extensions.turbowarp.org:

unsandboxed/turbo-mode.js - Try this extension
(function(Scratch) {
'use strict';

if (!Scratch.extensions.unsandboxed) {
throw new Error('This Turbo Mode example must run unsandboxed');
}
const vm = Scratch.vm;

class TurboMode {
getInfo() {
return {
id: 'turbomodeunsandboxed',
name: 'Turbo Mode',
blocks: [
{
opcode: 'set',
blockType: Scratch.BlockType.COMMAND,
text: 'set turbo mode to [ENABLED]',
arguments: {
ENABLED: {
type: Scratch.ArgumentType.STRING,
menu: 'ENABLED_MENU'
}
}
}
],
menus: {
ENABLED_MENU: {
acceptReporters: true,
items: ['on', 'off']
}
}
};
}
set(args) {
vm.setTurboMode(args.ENABLED === 'on');
}
}
Scratch.extensions.register(new TurboMode());
})(Scratch);

The block utility object

When a sandboxed custom extension is run, all it receives are the arguments that the scripts provided. It doesn't even know which sprite is executing it. We now introduce the second argument passed to block functions: BlockUtility.

The BlockUtility object, conventionally called util, allows blocks in unsandboxed extensions to get direct access to the sprite that is running them using util.target. Similar to the VM, this is the actual object used internally. You have full access to it.

Here is an example extension that demonstrates using util.target to get the name of the current sprite or access variables.

unsandboxed/block-utility-examples.js - Try this extension
(function(Scratch) {
'use strict';

if (!Scratch.extensions.unsandboxed) {
throw new Error('This Block Utility example must run unsandboxed');
}

class BlockUtilityExamples {
getInfo() {
return {
id: 'blockutilityexamples',
name: 'BlockUtility Examples',
blocks: [
{
opcode: 'getSpriteName',
text: 'sprite name',
blockType: Scratch.BlockType.REPORTER,
},
{
opcode: 'doesVariableExist',
text: 'is there a [TYPE] named [NAME]?',
blockType: Scratch.BlockType.BOOLEAN,
arguments: {
NAME: {
type: Scratch.ArgumentType.STRING,
defaultValue: 'my variable'
},
TYPE: {
type: Scratch.ArgumentType.STRING,
menu: 'TYPE_MENU',
defaultValue: 'list'
}
}
}
],
menus: {
TYPE_MENU: {
acceptReporters: true,
items: [
// Value here corresponds to the internal types of the variables
// in scratch-vm. And yes, broadcasts are actually variables.
// https://github.com/TurboWarp/scratch-vm/blob/20c60193c1c567a65cca87b16d22c51963565a43/src/engine/variable.js#L43-L67
{
text: 'variable',
value: ''
},
'list',
{
text: 'broadcast',
value: 'broadcast_msg'
}
]
}
}
};
}
getSpriteName(args, util) {
return util.target.getName();
}
doesVariableExist(args, util) {
const variable = util.target.lookupVariableByNameAndType(args.NAME.toString(), args.TYPE);
// Remember: Boolean blocks need to explicitly return a boolean on their own
return !!variable;
}
}
Scratch.extensions.register(new BlockUtilityExamples());
})(Scratch);

Note that every sprite, script, and block shares the same block utility object. Instead of making a object each time your block runs, it just updates the properties of the shared object for performance. Thus, the only safe time to access util is immediately when the block runs. Trying to access util in a setTimeout, setInterval, Promise callback, or other non-syncronous callback will not work correctly. If you need to access properties from util later, save them in a variable ahead of time.

  // This is NOT reliable and may alert the wrong thing:
myBlock(args, util) {
setTimeout(() => {
alert(util.target.getName());
}, 1000);
}

// This will always work:
myBlock(args, util) {
const target = util.target;
setTimeout(() => {
alert(target.getName());
}, 1000);
}

Common templates

Here are some common copy-and-pasteable code snippets that can be used:

If the extension MUST be run unsandboxed, add this around the start:

  if (!Scratch.extensions.unsandboxed) {
throw new Error('Extension Name must run unsandboxed');
}

If you're using the vm, runtime or Cast APIs a lot, common practise is to define them around the start to save time:

  const vm = Scratch.vm;
const runtime = vm.runtime;
const Cast = Scratch.Cast; // Discussed later.

Permissioned APIs

Whereas sandboxed extensions are free to use APIs such as fetch() as they please, unsandboxed extensions should instead ask for permission before making a request to any remote service. This gives the user control over their privacy. While there is no technical measures enforcing this at runtime, it is required for all extensions on extensions.turbowarp.org.

Requests to some popular services such as GitHub Pages or GitLab Pages may be automatically approved, while requests to other random websites may show a prompt to the user. You shouldn't make any assumptions about this, and your code needs to ensure that it can gracefully handle the user rejecting the prompt (the extension should behave the same as it does when there is no internet connection).

These permissioned APIs will also automatically prevent projects from running arbitrary JavaScript by attempting to, for example, redirect to a javascript: URL.

Fetching APIs, WebSockets, images, audio files, etc.

Use Scratch.fetch(url) instead of fetch(url). Check await Scratch.canFetch(url) before using other APIs that connect to remote websites.

// Do not do this:
const response = await fetch(url);
// Do this instead:
const response = await Scratch.fetch(url);

// Do not do this:
const ws = new WebSocket(url);
// Do this instead:
if (await Scratch.canFetch(url)) {
const ws = new WebSocket(url);
}

// Do not do this:
const image = new Image();
image.src = src;
// Do this instead:
if (await Scratch.canFetch(src)) {
const image = new Image();
image.src = src;
}

// Do not do this:
const audio = new Audio(url);
// Do this instead:
if (await Scratch.canFetch(url)) {
const audio = new Audio(url);
}

Opening new tabs or windows

Use Scratch.openWindow(url) instead of window.open(url). Scratch.openWindow always sets the target to "_blank" to open a new tab or window. If you can't use Scratch.openWindow(url) for some reason, check await Scratch.canOpenWindow(url) before calling window.open(url).

// Do not do this:
const win = window.open(url);
// Do this instead:
const win = await Scratch.openWindow(url);

// Do not do this:
const win = window.open(url, '_blank', 'width=400,height=400')
// Do this instead:
const win = await Scratch.openWindow(url, 'width=400,height=400');

Redirecting the current page

Use Scratch.redirect(url) instead of location.href = url. If you can't use Scratch.redirect(url), check await Scratch.canRedirect(url) before running location.href = url.

// Do not do this:
location.href = url;
// Do this instead:
await Scratch.redirect(url);

Exercises

We encourage you to try to figure these out without the hints. It will make you much more familiar with how VM internals work.

  1. Create a block that presses the green flag. (Hint: vm.greenFlag)
  2. Create a block that returns the x position of the sprite, similar to the "x position" block. (Hint: target.x)
  3. Create a block that moves the sprite to the center of the screen, similar to "go to x: 0 y: 0". (Hint: target.setXY(x, y))

Next steps

Depending on the server you've been using, you might be tired of remembering to hard-reload all of the time. Let's learn about a better development server.