Skip to content

feat: [UEPR-230] add sanitization when loading a project #245

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jun 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/scratch-vm/src/serialization/deserialize-assets.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const JSZip = require('jszip');
const log = require('../util/log');
const {sanitizeSvg} = require('@scratch/scratch-svg-renderer');

/**
* Deserializes sound from file into storage cache so that it can
Expand Down Expand Up @@ -156,6 +157,11 @@ const deserializeCostume = function (costume, runtime, zip, assetFileName, textL

return Promise.all([textLayerFilePromise,
costumeFile.async('uint8array')
.then(data =>
(costumeFormat === 'svg' ?
sanitizeSvg.sanitizeByteStream(data) :
data)
)
.then(data => storage.createAsset(
assetType,
// TODO eventually we want to map non-png's to their actual file types?
Expand Down
Binary file modified packages/scratch-vm/test/fixtures/corrupt_svg.sb2
Binary file not shown.
Binary file modified packages/scratch-vm/test/fixtures/corrupt_svg.sb3
Binary file not shown.
Binary file modified packages/scratch-vm/test/fixtures/corrupt_svg.sprite2
Binary file not shown.
Binary file modified packages/scratch-vm/test/fixtures/corrupt_svg.sprite3
Binary file not shown.
40 changes: 15 additions & 25 deletions packages/scratch-vm/test/integration/monitors_sb2_to_sb3.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,25 @@ const makeTestStorage = require('../fixtures/make-test-storage');
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
const VirtualMachine = require('../../src/index');

let vm;
const projectUri = path.resolve(__dirname, '../fixtures/monitors.sb2');
const project = readFileToBuffer(projectUri);

tap.beforeEach(() => {
const projectUri = path.resolve(__dirname, '../fixtures/monitors.sb2');
const project = readFileToBuffer(projectUri);

vm = new VirtualMachine();
vm.attachStorage(makeTestStorage());

// TODO figure out why running threads doesn't work in this test
// vm.start();
vm.clear();
vm.setCompatibilityMode(false);
vm.setTurboMode(false);

return vm.loadProject(project);
});
const test = tap.test;

test('saving and loading sb2 project with monitors preserves sliderMin and sliderMax', t => {
const vm = new VirtualMachine();
vm.attachStorage(makeTestStorage());

vm.on('playgroundData', e /* eslint-disable-line no-unused-vars */ => {
// TODO related to above TODO, comment these back in when we figure out
// why running threads doesn't work with this test

// const threads = JSON.parse(e.threads);
const threads = JSON.parse(e.threads);
// All monitors should create threads that finish during the step and
// are revoved from runtime.threads.
// t.equal(threads.length, 0);
t.equal(threads.length, 0);

// we care that the last step updated the right number of monitors
// we don't care whether the last step ran other threads or not
// const lastStepUpdatedMonitorThreads = vm.runtime._lastStepDoneThreads.filter(thread => thread.updateMonitor);
// t.equal(lastStepUpdatedMonitorThreads.length, 8);
const lastStepUpdatedMonitorThreads = vm.runtime._lastStepDoneThreads.filter(thread => thread.updateMonitor);
t.equal(lastStepUpdatedMonitorThreads.length, 8);

// There should be one additional hidden monitor that is in the monitorState but
// does not start a thread.
Expand Down Expand Up @@ -139,13 +124,18 @@ test('saving and loading sb2 project with monitors preserves sliderMin and slide
t.equal(monitorRecord.spriteName, null);
t.equal(monitorRecord.targetId, null);

vm.quit();
t.end();
});

// Start VM, load project, and run
t.doesNotThrow(() => {
const sb3ProjectJson = vm.toJSON();
return vm.loadProject(sb3ProjectJson).then(() => {
vm.start();
vm.clear();
vm.setCompatibilityMode(false);
vm.setTurboMode(false);
return vm.loadProject(project).then(() => {
vm.greenFlag();
setTimeout(() => {
vm.getPlaygroundData();
vm.stopAll();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const test = require('tap').test;
const AdmZip = require('adm-zip');
const ScratchStorage = require('scratch-storage').ScratchStorage;
const VirtualMachine = require('../../src/index');
const {sanitizeByteStream} = require('../../../scratch-svg-renderer/src/sanitize-svg');

const projectUri = path.resolve(__dirname, '../fixtures/offline-custom-assets.sb2');
const projectZip = AdmZip(projectUri);
Expand Down Expand Up @@ -54,7 +55,7 @@ test('offline-custom-assets', t => {

const storedCostume = customCostume.asset;
t.type(storedCostume, 'object');
t.deepEquals(storedCostume.data, costumeData);
t.same(storedCostume.data, sanitizeByteStream(costumeData));

const sounds = vm.runtime.targets[1].sprite.sounds;
t.equals(sounds.length, 1);
Expand Down
5 changes: 3 additions & 2 deletions packages/scratch-vm/test/integration/sb2_corrupted_svg.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const FakeBitmapAdapter = require('../fixtures/fake-bitmap-adapter');
const {extractAsset, readFileToBuffer} = require('../fixtures/readProjectFile');
const VirtualMachine = require('../../src/index');
const {serializeCostumes} = require('../../src/serialization/serialize-assets');
const {sanitizeByteStream} = require('../../../scratch-svg-renderer/src/sanitize-svg');

const projectUri = path.resolve(__dirname, '../fixtures/corrupt_svg.sb2');
const project = readFileToBuffer(projectUri);
Expand All @@ -23,7 +24,7 @@ const originalCostume = extractAsset(projectUri, costumeFileName);
// We need to get the actual md5 because we hand modified the svg to corrupt it
// after we downloaded the project from Scratch
// Loading the project back into the VM will correct the assetId and md5
const brokenCostumeMd5 = md5(originalCostume);
const brokenCostumeMd5 = md5(sanitizeByteStream(originalCostume));

global.Image = function () {
const image = {
Expand Down Expand Up @@ -57,7 +58,7 @@ tap.beforeEach(() => {
// Mock renderer breaking on loading a corrupt costume
FakeRenderer.prototype.createSVGSkin = function (svgString) {
// Look for text added to costume to make it a corrupt svg
if (svgString.includes('<here is some')) {
if (svgString.includes('<rect width="100 height=100 fill=">')) {
throw new Error('mock createSVGSkin broke');
}
return FakeRenderer._nextSkinId++;
Expand Down
5 changes: 3 additions & 2 deletions packages/scratch-vm/test/integration/sb3_corrupted_svg.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const FakeRenderer = require('../fixtures/fake-renderer');
const {extractAsset, readFileToBuffer} = require('../fixtures/readProjectFile');
const VirtualMachine = require('../../src/index');
const {serializeCostumes} = require('../../src/serialization/serialize-assets');
const {sanitizeByteStream} = require('../../../scratch-svg-renderer/src/sanitize-svg');

const projectUri = path.resolve(__dirname, '../fixtures/corrupt_svg.sb3');
const project = readFileToBuffer(projectUri);
Expand All @@ -22,7 +23,7 @@ const originalCostume = extractAsset(projectUri, costumeFileName);
// We need to get the actual md5 because we hand modified the svg to corrupt it
// after we downloaded the project from Scratch
// Loading the project back into the VM will correct the assetId and md5
const brokenCostumeMd5 = md5(originalCostume);
const brokenCostumeMd5 = md5(sanitizeByteStream(originalCostume));

let vm;
let defaultVectorAssetId;
Expand All @@ -37,7 +38,7 @@ tap.beforeEach(() => {
// Mock renderer breaking on loading a corrupt costume
FakeRenderer.prototype.createSVGSkin = function (svgString) {
// Look for text added to costume to make it a corrupt svg
if (svgString.includes('<here is some')) {
if (svgString.includes('<rect width="100 height=100 fill=">')) {
throw new Error('mock createSVGSkin broke');
}
return FakeRenderer._nextSkinId++;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const FakeBitmapAdapter = require('../fixtures/fake-bitmap-adapter');
const {extractAsset, readFileToBuffer} = require('../fixtures/readProjectFile');
const VirtualMachine = require('../../src/index');
const {serializeCostumes} = require('../../src/serialization/serialize-assets');
const {sanitizeByteStream} = require('../../../scratch-svg-renderer/src/sanitize-svg');

const projectUri = path.resolve(__dirname, '../fixtures/default.sb3');
const project = readFileToBuffer(projectUri);
Expand All @@ -28,7 +29,7 @@ const originalCostume = extractAsset(spriteUri, costumeFileName);
// We need to get the actual md5 because we hand modified the svg to corrupt it
// after we downloaded the project from Scratch
// Loading the project back into the VM will correct the assetId and md5
const brokenCostumeMd5 = md5(originalCostume);
const brokenCostumeMd5 = md5(sanitizeByteStream(originalCostume));

global.Image = function () {
const image = {
Expand Down Expand Up @@ -61,7 +62,7 @@ tap.beforeEach(() => {
// Mock renderer breaking on loading a corrupt costume
FakeRenderer.prototype.createSVGSkin = function (svgString) {
// Look for text added to costume to make it a corrupt svg
if (svgString.includes('<here is some')) {
if (svgString.includes('<rect width="100 height=100 fill=">')) {
throw new Error('mock createSVGSkin broke');
}
return FakeRenderer.prototype._nextSkinId++;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const FakeRenderer = require('../fixtures/fake-renderer');
const {extractAsset, readFileToBuffer} = require('../fixtures/readProjectFile');
const VirtualMachine = require('../../src/index');
const {serializeCostumes} = require('../../src/serialization/serialize-assets');
const {sanitizeByteStream} = require('../../../scratch-svg-renderer/src/sanitize-svg');

const projectUri = path.resolve(__dirname, '../fixtures/default.sb3');
const project = readFileToBuffer(projectUri);
Expand All @@ -27,7 +28,7 @@ const originalCostume = extractAsset(spriteUri, costumeFileName);
// We need to get the actual md5 because we hand modified the svg to corrupt it
// after we downloaded the project from Scratch
// Loading the project back into the VM will correct the assetId and md5
const brokenCostumeMd5 = md5(originalCostume);
const brokenCostumeMd5 = md5(sanitizeByteStream(originalCostume));

let vm;
let defaultVectorAssetId;
Expand All @@ -42,7 +43,7 @@ tap.beforeEach(() => {
// Mock renderer breaking on loading a corrupt costume
FakeRenderer.prototype.createSVGSkin = function (svgString) {
// Look for text added to costume to make it a corrupt svg
if (svgString.includes('<here is some')) {
if (svgString.includes('<rect width="100 height=100 fill=">')) {
throw new Error('mock createSVGSkin broke');
}
return FakeRenderer.prototype._nextSkinId++;
Expand Down
Loading