From b68d8449775151956d055bc3a396b71d62480d33 Mon Sep 17 00:00:00 2001 From: Miyo Sho <135030944+yuri-kiss@users.noreply.github.com> Date: Thu, 20 Nov 2025 09:39:47 -0500 Subject: [PATCH 1/6] refactor poor code & fix critical vuln --- static/extensions/TheShovel/extexp.js | 158 +++++++++++++++++++++----- 1 file changed, 130 insertions(+), 28 deletions(-) diff --git a/static/extensions/TheShovel/extexp.js b/static/extensions/TheShovel/extexp.js index 749b5180..88b885a1 100644 --- a/static/extensions/TheShovel/extexp.js +++ b/static/extensions/TheShovel/extexp.js @@ -3,15 +3,16 @@ * @author TheShovel https://github.com/TheShovel/ * @author 0znzw https://scratch.mit.edu/users/0znzw/ * @author Faunksys https://github.com/faunks/ - * @version 1.5 + * @version 2.0 * @copyright MIT License * Do not remove this comment */ (function(Scratch) { + 'use strict'; if (!Scratch.extensions.unsandboxed) { throw new Error(`"Extension Exposer" must be ran unsandboxed.`); } - const { Cast, BlockType, ArgumentType, vm } = Scratch, { runtime } = vm, + const { Cast, BlockType, ArgumentType, vm } = Scratch, { runtime, extensionManager } = vm, extId = 'jodieextexp', runText = 'run function [FUNCNAME] from [EXTLIST] with inputs [INPUT]', getFunctionsText = 'get blocks from [EXTLIST]', defaultArguments = { @@ -21,13 +22,41 @@ }, getBlocksArgument = { EXTLIST: { type: ArgumentType.STRING, menu: 'EXTLIST', defaultValue: extId }, - }; + }, + bannedExtensions = new Set([]), + bannedBlocks = new Set([`${extId}/_toggleStrict`]); + + if (runtime.ext_pm_liveTests) { + // Some built in extensions come with XSS that this extension can bypass + // the restrictions for; this only affects strict mode. + bannedExtensions.add('jgJavascript'); + bannedExtensions.add('SPjavascriptV2'); + + bannedBlocks.add('scratch3_control/runJavascript'); + bannedBlocks.add('jgPrism/evaluate'); + bannedBlocks.add('jgPrism/evaluate2'); + bannedBlocks.add('jgPrism/evaluate3'); + } + + const isPackaged = ('scaffolding' in globalThis) && !Scratch.gui; class jodieextexp { + constructor() { + this._strict = [false, null]; + // Packaged projects can do whatever, strict mode is mainly for in the editor anyways. + if (!isPackaged) this._toggleStrict(true); + } + static exports = { + bannedExtensions, bannedBlocks, + }; getInfo() { return { id: extId, name: 'Extension Exposer', blocks: [{ + func: '_toggleStrict', + blockType: BlockType.BUTTON, + text: `${this._strict[0] ? 'Disable' : 'Enable'} strict mode`, + }, '---', { func: 'getBlocks', opcode: 'getfunctions', blockType: BlockType.REPORTER, @@ -64,50 +93,123 @@ }; } _parseJSON(obj) { - if (Array.isArray(obj)) return {}; - if (typeof obj === 'object') return obj; + if (typeof obj === 'object') { + if ( + obj === null || + !Object.is(Object.getPrototypeOf(obj), Object.prototype) + ) return {}; + return obj; + } try { obj = JSON.parse(obj); - if (Array.isArray(obj)) return {}; - if (typeof obj === 'object') return obj; - return {}; + if ( + obj === null || + !Object.is(Object.getPrototypeOf(obj), Object.prototype) + ) return {}; + return obj; } catch { return {}; } } _extensions() { - const arr = Array.from(vm.extensionManager._loadedExtensions.keys()); + const arr = Array.from(extensionManager._loadedExtensions.keys()).map(id => String(id)); if (typeof arr[0] !== 'string') arr.push(''); return arr; } - test(args) { - return Cast.toString(args.INPUT || ''); + _getExtensionObject(id) { + // TurboWarp and PenguinMod export style. + let ext = runtime[`ext_${id}`]; + if (ext) return ext; + // Unsandboxed (mod) export style. + ext = runtime[`cext_${id}`]; + if (ext) return ext; + // Allows built in extensions that are not loaded fully to also work. + ext = ( + Object.prototype.hasOwnProperty.call(extensionManager.builtinExtensions, id) && + extensionManager.builtinExtensions[id] + ); + if (ext) return ext; + return null; } run({ FUNCNAME, EXTLIST, INPUT }, util, blockJSON) { EXTLIST = Cast.toString(EXTLIST); FUNCNAME = Cast.toString(FUNCNAME); - // If the function does not exist then it is not referenced as a real block, or the extension is not global (fallback) - return (runtime._primitives[`${EXTLIST}_${FUNCNAME}`] || runtime[`ext_${EXTLIST}`][FUNCNAME])(this._parseJSON(Cast.toString(INPUT)), util, blockJSON); + + // Blocks have priority over built in functions. + let fn = runtime._primitives[`${EXTLIST}_${FUNCNAME}`]; + if (!fn) fn = this._getExtensionObject(EXTLIST)[FUNCNAME]; + + // If the function does not exist then the function doesn not exist on the target extension. + return fn(this._parseJSON(Cast.toString(INPUT)), util, blockJSON); } - getBlocks({ EXTLIST }, util, blockJSON) { - const ext = runtime[`ext_${EXTLIST}`]; - const blocks = []; - // Check if the extension implements the standard extension API - if (ext && (typeof ext.getInfo === 'function')) { - const info = ext.getInfo().blocks; - if (!info) return blocks; - for (let index = 0; index < info.length; index++) { - blocks.push(info[index].opcode); - } + getBlocks({ EXTLIST }) { + const ext = this._getExtensionObject(Cast.toString(EXTLIST)); + if (!ext) return []; + if (typeof ext.getInfo === 'function') { + return (ext.getInfo().blocks || []).flatMap(block => ( + block && (typeof block.opcode === 'string') ? [block.opcode] : [] + )); + } else if (typeof ext.getPrimitives === 'function') { + return Object.getOwnPropertyNames(ext.getPrimitives()).map(opcode => String(opcode)); } - return blocks; + return []; } - runcommand() {} - runreporter() {} - runboolean() {} + runcommand() { return ''; } + runreporter() { return ''; } + runboolean() { return ''; } - getfunctions() {} + getfunctions() { return ''; } + + test({ INPUT }) { + return Cast.toString(INPUT); + } + + // Custom strict mode that disables blocks known to allow XSS or that enable a path to XSS. + // It also disables errors on platforms that do not support error returns, and null coalshes values for safety. + // This does not affect any packaged projects, current projects will function the same unless they are abusing the vulnerabilites. + // You can disable strict mode in the editor if you want, it does not save to the project and should only be used for say... testing. + // If a package project calls this then it is their own fault, not mine. + async _toggleStrict(skipRefresh) { + if (this._strict[0]) { + if (!isPackaged) { + const confirmation = await confirm('Disabling strict mode only works in the editor and packaged projects, it may also expose you to unsafe JavaScript, are you SURE you want to disable strict mode?'); + if (!confirmation) return; + } + this.run = this._strict[1]; + this._strict[0] = false; + this._strict[1] = null; + } else { + this._strict[0] = true; + this._strict[1] = this.run; + this.run = function run({ FUNCNAME, EXTLIST, INPUT }, util, blockJSON) { + EXTLIST = Cast.toString(EXTLIST); + FUNCNAME = Cast.toString(FUNCNAME); + + if ( + bannedExtensions.has(EXTLIST) || + bannedBlocks.has(`${EXTLIST}/${FUNCNAME}`) + ) { + console.error('The block or extension a block tried to use is restricted.', EXTLIST, FUNCNAME); + if (runtime.ext_pm_liveTests) { + throw new ReferenceError('The block or extension a block tried to use is restricted.'); + } else { + return ''; + } + } + + if (runtime.ext_pm_liveTests) return this._strict[1].apply(this, arguments); + try { + return this._strict[1].apply(this, arguments) ?? ''; + } catch(error) { + console.error('The block or extension a block tried to use failed to run.', EXTLIST, FUNCNAME, error); + return ''; + } + }; + } + // Doing !== true here because some mods pass special Blockly arguments to buttons. + if (skipRefresh !== true) extensionManager.refreshBlocks(extId); + } } Scratch.extensions.register(runtime[`ext_${extId}`] = new jodieextexp()); })(Scratch); From 9c724d273b0c17629465b3425cdef8c9ddd8245e Mon Sep 17 00:00:00 2001 From: Miyo Sho <135030944+yuri-kiss@users.noreply.github.com> Date: Thu, 20 Nov 2025 09:46:52 -0500 Subject: [PATCH 2/6] FIX: clean-up some comments I wrote (do be bad at english fr) --- static/extensions/TheShovel/extexp.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/static/extensions/TheShovel/extexp.js b/static/extensions/TheShovel/extexp.js index 88b885a1..8b0bb1c5 100644 --- a/static/extensions/TheShovel/extexp.js +++ b/static/extensions/TheShovel/extexp.js @@ -42,7 +42,8 @@ class jodieextexp { constructor() { this._strict = [false, null]; - // Packaged projects can do whatever, strict mode is mainly for in the editor anyways. + // Packaged projects can do whatever, strict mode is mainly for when the user + // is in the editor or on the project page anyways. if (!isPackaged) this._toggleStrict(true); } static exports = { @@ -123,7 +124,7 @@ // Unsandboxed (mod) export style. ext = runtime[`cext_${id}`]; if (ext) return ext; - // Allows built in extensions that are not loaded fully to also work. + // Allow built-in extensions that are not loaded fully to also be ran. ext = ( Object.prototype.hasOwnProperty.call(extensionManager.builtinExtensions, id) && extensionManager.builtinExtensions[id] @@ -135,11 +136,11 @@ EXTLIST = Cast.toString(EXTLIST); FUNCNAME = Cast.toString(FUNCNAME); - // Blocks have priority over built in functions. + // Blocks have priority over built-in functions. let fn = runtime._primitives[`${EXTLIST}_${FUNCNAME}`]; if (!fn) fn = this._getExtensionObject(EXTLIST)[FUNCNAME]; - // If the function does not exist then the function doesn not exist on the target extension. + // If the function does not exist then the function does not exist on the target extension, or the extension lied about existing. return fn(this._parseJSON(Cast.toString(INPUT)), util, blockJSON); } getBlocks({ EXTLIST }) { @@ -165,9 +166,10 @@ return Cast.toString(INPUT); } - // Custom strict mode that disables blocks known to allow XSS or that enable a path to XSS. - // It also disables errors on platforms that do not support error returns, and null coalshes values for safety. - // This does not affect any packaged projects, current projects will function the same unless they are abusing the vulnerabilites. + // Custom strict mode that disables blocks known to allow XSS or that enable a path to XSS, it also disables errors on platforms + // that do not support error returns, and null coalescing values to empty strings for safety. + // + // This does not affect any packaged projects, current projects will function the same unless they are abusing the vulnerabilities. // You can disable strict mode in the editor if you want, it does not save to the project and should only be used for say... testing. // If a package project calls this then it is their own fault, not mine. async _toggleStrict(skipRefresh) { @@ -207,7 +209,7 @@ } }; } - // Doing !== true here because some mods pass special Blockly arguments to buttons. + // We are doing `!== true` here because some mods pass special Blockly arguments to buttons. if (skipRefresh !== true) extensionManager.refreshBlocks(extId); } } From 2fb35f18ee683ff452af73bafae4d5f253869a82 Mon Sep 17 00:00:00 2001 From: Miyo Sho <135030944+yuri-kiss@users.noreply.github.com> Date: Thu, 20 Nov 2025 14:39:27 -0500 Subject: [PATCH 3/6] fix the bind bug, and remove the useless "run when unloaded" --- static/extensions/TheShovel/extexp.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/static/extensions/TheShovel/extexp.js b/static/extensions/TheShovel/extexp.js index 8b0bb1c5..ae97d57a 100644 --- a/static/extensions/TheShovel/extexp.js +++ b/static/extensions/TheShovel/extexp.js @@ -3,7 +3,7 @@ * @author TheShovel https://github.com/TheShovel/ * @author 0znzw https://scratch.mit.edu/users/0znzw/ * @author Faunksys https://github.com/faunks/ - * @version 2.0 + * @version 2.1 * @copyright MIT License * Do not remove this comment */ @@ -124,12 +124,6 @@ // Unsandboxed (mod) export style. ext = runtime[`cext_${id}`]; if (ext) return ext; - // Allow built-in extensions that are not loaded fully to also be ran. - ext = ( - Object.prototype.hasOwnProperty.call(extensionManager.builtinExtensions, id) && - extensionManager.builtinExtensions[id] - ); - if (ext) return ext; return null; } run({ FUNCNAME, EXTLIST, INPUT }, util, blockJSON) { @@ -138,7 +132,10 @@ // Blocks have priority over built-in functions. let fn = runtime._primitives[`${EXTLIST}_${FUNCNAME}`]; - if (!fn) fn = this._getExtensionObject(EXTLIST)[FUNCNAME]; + if (!fn) { + const ext = this._getExtensionObject(EXTLIST); + fn = (typeof ext[FUNCNAME] === 'function') && ext[FUNCNAME].bind(ext); + } // If the function does not exist then the function does not exist on the target extension, or the extension lied about existing. return fn(this._parseJSON(Cast.toString(INPUT)), util, blockJSON); From 459f2b6d027f6a7cf43898b9854fd14ca2608f71 Mon Sep 17 00:00:00 2001 From: Miyo Sho <135030944+yuri-kiss@users.noreply.github.com> Date: Thu, 20 Nov 2025 17:34:07 -0500 Subject: [PATCH 4/6] simpler strict mode and add built in extensions --- static/extensions/TheShovel/extexp.js | 65 +++++++++++---------------- 1 file changed, 25 insertions(+), 40 deletions(-) diff --git a/static/extensions/TheShovel/extexp.js b/static/extensions/TheShovel/extexp.js index ae97d57a..a6c22769 100644 --- a/static/extensions/TheShovel/extexp.js +++ b/static/extensions/TheShovel/extexp.js @@ -3,7 +3,7 @@ * @author TheShovel https://github.com/TheShovel/ * @author 0znzw https://scratch.mit.edu/users/0znzw/ * @author Faunksys https://github.com/faunks/ - * @version 2.1 + * @version 2.2 * @copyright MIT License * Do not remove this comment */ @@ -22,21 +22,7 @@ }, getBlocksArgument = { EXTLIST: { type: ArgumentType.STRING, menu: 'EXTLIST', defaultValue: extId }, - }, - bannedExtensions = new Set([]), - bannedBlocks = new Set([`${extId}/_toggleStrict`]); - - if (runtime.ext_pm_liveTests) { - // Some built in extensions come with XSS that this extension can bypass - // the restrictions for; this only affects strict mode. - bannedExtensions.add('jgJavascript'); - bannedExtensions.add('SPjavascriptV2'); - - bannedBlocks.add('scratch3_control/runJavascript'); - bannedBlocks.add('jgPrism/evaluate'); - bannedBlocks.add('jgPrism/evaluate2'); - bannedBlocks.add('jgPrism/evaluate3'); - } + }; const isPackaged = ('scaffolding' in globalThis) && !Scratch.gui; class jodieextexp { @@ -46,9 +32,6 @@ // is in the editor or on the project page anyways. if (!isPackaged) this._toggleStrict(true); } - static exports = { - bannedExtensions, bannedBlocks, - }; getInfo() { return { id: extId, @@ -114,8 +97,18 @@ } _extensions() { const arr = Array.from(extensionManager._loadedExtensions.keys()).map(id => String(id)); - if (typeof arr[0] !== 'string') arr.push(''); - return arr; + return [ + // Built in categories. + `scratch3_motion`, + `scratch3_looks`, + `scratch3_sound`, + `scratch3_event`, + `scratch3_control`, + `scratch3_sensing`, + `scratch3_operators`, + `scratch3_data`, + `scratch3_procedures`, + ].concat(arr); } _getExtensionObject(id) { // TurboWarp and PenguinMod export style. @@ -163,12 +156,7 @@ return Cast.toString(INPUT); } - // Custom strict mode that disables blocks known to allow XSS or that enable a path to XSS, it also disables errors on platforms - // that do not support error returns, and null coalescing values to empty strings for safety. - // - // This does not affect any packaged projects, current projects will function the same unless they are abusing the vulnerabilities. - // You can disable strict mode in the editor if you want, it does not save to the project and should only be used for say... testing. - // If a package project calls this then it is their own fault, not mine. + // Strict mode disables any non-block related functions. async _toggleStrict(skipRefresh) { if (this._strict[0]) { if (!isPackaged) { @@ -181,20 +169,14 @@ } else { this._strict[0] = true; this._strict[1] = this.run; - this.run = function run({ FUNCNAME, EXTLIST, INPUT }, util, blockJSON) { + this.run = function run({ FUNCNAME, EXTLIST }) { EXTLIST = Cast.toString(EXTLIST); FUNCNAME = Cast.toString(FUNCNAME); - - if ( - bannedExtensions.has(EXTLIST) || - bannedBlocks.has(`${EXTLIST}/${FUNCNAME}`) - ) { - console.error('The block or extension a block tried to use is restricted.', EXTLIST, FUNCNAME); - if (runtime.ext_pm_liveTests) { - throw new ReferenceError('The block or extension a block tried to use is restricted.'); - } else { - return ''; - } + + if (!runtime._primitives[`${EXTLIST}_${FUNCNAME}`]) { + console.error('The block a block tried to use does not exist.', EXTLIST, FUNCNAME); + if (runtime.ext_pm_liveTests) throw new ReferenceError('The block a block tried to use does not exist.'); + return ''; } if (runtime.ext_pm_liveTests) return this._strict[1].apply(this, arguments); @@ -210,5 +192,8 @@ if (skipRefresh !== true) extensionManager.refreshBlocks(extId); } } - Scratch.extensions.register(runtime[`ext_${extId}`] = new jodieextexp()); + + const instance = (runtime[`ext_${extId}`] = new jodieextexp()); + runtime._primitives[`${extId}_test`] = instance.test.bind(instance); + Scratch.extensions.register(instance); })(Scratch); From 3e69f94e5402298491ae48709cb7cd8122848b64 Mon Sep 17 00:00:00 2001 From: Miyo Sho <135030944+yuri-kiss@users.noreply.github.com> Date: Fri, 21 Nov 2025 17:50:27 -0500 Subject: [PATCH 5/6] fix some of what was mentioned --- static/extensions/TheShovel/extexp.js | 82 +++++++++++++-------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/static/extensions/TheShovel/extexp.js b/static/extensions/TheShovel/extexp.js index a6c22769..4d016a76 100644 --- a/static/extensions/TheShovel/extexp.js +++ b/static/extensions/TheShovel/extexp.js @@ -3,7 +3,7 @@ * @author TheShovel https://github.com/TheShovel/ * @author 0znzw https://scratch.mit.edu/users/0znzw/ * @author Faunksys https://github.com/faunks/ - * @version 2.2 + * @version 2.3 * @copyright MIT License * Do not remove this comment */ @@ -16,12 +16,12 @@ extId = 'jodieextexp', runText = 'run function [FUNCNAME] from [EXTLIST] with inputs [INPUT]', getFunctionsText = 'get blocks from [EXTLIST]', defaultArguments = { - FUNCNAME: { type: ArgumentType.STRING, defaultValue: 'test' }, - EXTLIST: { type: ArgumentType.STRING, menu: 'EXTLIST', defaultValue: extId }, - INPUT: { type: ArgumentType.STRING, defaultValue: '{"INPUT":"Hello World!"}' }, + FUNCNAME: { type: ArgumentType.STRING, defaultValue: 'movesteps' }, + EXTLIST: { type: ArgumentType.STRING, menu: 'EXTLIST' }, + INPUT: { type: ArgumentType.STRING, defaultValue: '{"STEPS":10}' }, }, getBlocksArgument = { - EXTLIST: { type: ArgumentType.STRING, menu: 'EXTLIST', defaultValue: extId }, + EXTLIST: { type: ArgumentType.STRING, menu: 'EXTLIST' }, }; const isPackaged = ('scaffolding' in globalThis) && !Scratch.gui; @@ -76,7 +76,8 @@ }, }; } - _parseJSON(obj) { + // NOTE: This is only meant to return Object values and nothing else (no Arrays or null) + _convertToObject(obj) { if (typeof obj === 'object') { if ( obj === null || @@ -95,26 +96,29 @@ return {}; } } + static _BUILT_IN_CATEGORIES = [ + // Built in categories. + // The "scratch3_" prefix can be added if you want to access direct functions on the classes. + `motion`, + `looks`, + `sound`, + `event`, + `control`, + `sensing`, + `operators`, + `data`, + `procedures`, + ]; _extensions() { const arr = Array.from(extensionManager._loadedExtensions.keys()).map(id => String(id)); - return [ - // Built in categories. - `scratch3_motion`, - `scratch3_looks`, - `scratch3_sound`, - `scratch3_event`, - `scratch3_control`, - `scratch3_sensing`, - `scratch3_operators`, - `scratch3_data`, - `scratch3_procedures`, - ].concat(arr); + return jodieextexp._BUILT_IN_CATEGORIES.concat(arr); } _getExtensionObject(id) { // TurboWarp and PenguinMod export style. let ext = runtime[`ext_${id}`]; if (ext) return ext; // Unsandboxed (mod) export style. + // NOTE: This is only added because a large amount of extensions use it. ext = runtime[`cext_${id}`]; if (ext) return ext; return null; @@ -123,27 +127,21 @@ EXTLIST = Cast.toString(EXTLIST); FUNCNAME = Cast.toString(FUNCNAME); - // Blocks have priority over built-in functions. + // Real blocks have priority over class functions. let fn = runtime._primitives[`${EXTLIST}_${FUNCNAME}`]; if (!fn) { + // "scratch3_" will fall through to here, so no if check is needed. const ext = this._getExtensionObject(EXTLIST); fn = (typeof ext[FUNCNAME] === 'function') && ext[FUNCNAME].bind(ext); } // If the function does not exist then the function does not exist on the target extension, or the extension lied about existing. - return fn(this._parseJSON(Cast.toString(INPUT)), util, blockJSON); + return fn(this._convertToObject(Cast.toString(INPUT)), util, blockJSON); } getBlocks({ EXTLIST }) { - const ext = this._getExtensionObject(Cast.toString(EXTLIST)); - if (!ext) return []; - if (typeof ext.getInfo === 'function') { - return (ext.getInfo().blocks || []).flatMap(block => ( - block && (typeof block.opcode === 'string') ? [block.opcode] : [] - )); - } else if (typeof ext.getPrimitives === 'function') { - return Object.getOwnPropertyNames(ext.getPrimitives()).map(opcode => String(opcode)); - } - return []; + EXTLIST = `${Cast.toString(EXTLIST)}_`; + // We use the primitives list here to make sure we aren't getting say.. buttons or labels, which are not really "blocks" in the traditional sense. + return Object.getOwnPropertyNames(runtime._primitives).filter(opcode => opcode.startsWith(EXTLIST)); } runcommand() { return ''; } @@ -152,6 +150,7 @@ getfunctions() { return ''; } + // @depricated test({ INPUT }) { return Cast.toString(INPUT); } @@ -169,21 +168,24 @@ } else { this._strict[0] = true; this._strict[1] = this.run; - this.run = function run({ FUNCNAME, EXTLIST }) { - EXTLIST = Cast.toString(EXTLIST); - FUNCNAME = Cast.toString(FUNCNAME); + this.run = function run(args) { + args.EXTLIST = Cast.toString(args.EXTLIST); + args.FUNCNAME = Cast.toString(args.FUNCNAME); - if (!runtime._primitives[`${EXTLIST}_${FUNCNAME}`]) { - console.error('The block a block tried to use does not exist.', EXTLIST, FUNCNAME); - if (runtime.ext_pm_liveTests) throw new ReferenceError('The block a block tried to use does not exist.'); + // Force old "scratch3_" style inputs into valid primitive categories. + if (jodieextexp._BUILT_IN_CATEGORIES.includes(args.EXTLIST)) args.EXTLIST = args.EXTLIST.replace('scratch3_', ''); + + if (!runtime._primitives[`${args.EXTLIST}_${args.FUNCNAME}`]) { + console.error('The block a block tried to use does not exist.', args.EXTLIST, args.FUNCNAME); + if (Scratch.extensions.isPenguinMod) throw new ReferenceError('The block a block tried to use does not exist.'); return ''; } - if (runtime.ext_pm_liveTests) return this._strict[1].apply(this, arguments); + if (Scratch.extensions.isPenguinMod) return this._strict[1].apply(this, arguments); try { return this._strict[1].apply(this, arguments) ?? ''; } catch(error) { - console.error('The block or extension a block tried to use failed to run.', EXTLIST, FUNCNAME, error); + console.error('The block or extension a block tried to use failed to run.', args.EXTLIST, args.FUNCNAME, error); return ''; } }; @@ -193,7 +195,5 @@ } } - const instance = (runtime[`ext_${extId}`] = new jodieextexp()); - runtime._primitives[`${extId}_test`] = instance.test.bind(instance); - Scratch.extensions.register(instance); + Scratch.extensions.register(runtime[`ext_${extId}`] = new jodieextexp()); })(Scratch); From cb96900413c98c80ae4fc7877fddb500fe6ebf96 Mon Sep 17 00:00:00 2001 From: Miyo Sho <135030944+yuri-kiss@users.noreply.github.com> Date: Fri, 21 Nov 2025 18:20:22 -0500 Subject: [PATCH 6/6] weaken toObject converter --- static/extensions/TheShovel/extexp.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/static/extensions/TheShovel/extexp.js b/static/extensions/TheShovel/extexp.js index 4d016a76..612349ba 100644 --- a/static/extensions/TheShovel/extexp.js +++ b/static/extensions/TheShovel/extexp.js @@ -76,21 +76,17 @@ }, }; } - // NOTE: This is only meant to return Object values and nothing else (no Arrays or null) + // NOTE: This is only meant to return object values and nothing else (no Arrays or null as they don't count here) _convertToObject(obj) { if (typeof obj === 'object') { - if ( - obj === null || - !Object.is(Object.getPrototypeOf(obj), Object.prototype) - ) return {}; + if (obj === null || Array.isArray(obj)) return {}; + // NOTE: Unlike the "JSON.parse" this is not guarenteed to actually be a safe object to pass around. return obj; } try { obj = JSON.parse(obj); - if ( - obj === null || - !Object.is(Object.getPrototypeOf(obj), Object.prototype) - ) return {}; + // "JSON.parse" only returns Object, Array and null as "object-like" values. + if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) return {}; return obj; } catch { return {};