Is Blocklive: Scratch Realtime Collaboration! safe?
Blocklive is medium risk. Saving a Scratch project with Blocklive active serializes the VM project; the worker posts that JSON to blocklive.micahp.me. It can include targets, blocks, sprites, costumes, and metadata. Dynamic analysis saw sockets but not this POST.
Who publishes itNo other listings under this identity, 5 shared hostnames
No other listings under this identity, 5 shared hostnames
What this publisher told the store about itself, and the other listings that told it the same thing.
Shared hosts - 5 hostnames
Hostnames hardcoded in this extension that few other listings call. That can mean one operator behind both, and it can equally mean a small shared vendor, so it is context rather than a conclusion. Hosts that many listings call are left out: they are services, not connections.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Scratch Project JSON Posted on Save
Saving a Scratch project with Blocklive active serializes the VM project; the worker posts that JSON to blocklive.micahp.me.
It can include targets, blocks, sprites, costumes, and metadata.
Dynamic analysis saw sockets but not this POST.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-200
- Source
- Dynamic sandbox
You save a Scratch project while Blocklive is active.
The code checks Scratch's project loading state and looks for a state ending in UPDATING.
The extension serializes the project and sends that JSON to Blocklive's server.
The message includes the Blocklive project id, the JSON from vm.toJSON(), and the Blocklive version.
- Project targetstargets: [{"isStage":true,"name":"Stage"},{"isStage":false,"name":"Sprite1"}]
This represents the stage and sprite objects that make up your Scratch project.
- Blocks and scriptsblocks: {"event_whenflagclicked":{"opcode":"event_whenflagclicked"}}
This can include the saved code blocks and script structure in your project.
- Project assetscostumes: [{"name":"costume1","assetId":"bcf454acf82e4504149f7ffe07081dbc"}]
This can include costumes and sounds referenced by the project file.
- Variables and listsvariables: {"score":["score",0]}
This can include saved variable and list names and values from the project.
- Blocklive project idprojectSavedJSON/67239418/31
This ties the uploaded JSON to a specific Blocklive collaboration project.
- Blocklive account headersheaders: {"authorization":"<redacted>","uname":"scratch_user_17"}
These can associate the upload with the signed-in Blocklive user and session.
Dynamic analysis observed Blocklive WebSocket connections but did not capture this save POST.
- uname
- Blocklive username
- Content-Type
- application/json
- authorization
- <redacted>
Save detection and upload path in the shipped extension
The content script injects the editor script on Scratch project pages
injectors/editor.jsconsole.log("injecting editor.js");// alert(chrome.runtime.id)let scriptElem = document.createElement("script");scriptElem.dataset.exId = chrome.runtime.idscriptElem.classList.add("blocklive-ext")let srcThign = chrome.runtime.getURL("/scripts/editor.js");scriptElem.src = srcThign;// document.body.append(scriptElem)if (!!document.head) { document.head.appendChild(scriptElem);} else { document.documentElement.appendChild(scriptElem);}The editor script serializes the project when saving begins
scripts/editor.jsstore.subscribe(function() { // HANDLE PROJECT SAVE let state = store.getState().scratchGui.projectState.loadingState if(lastProjectState != state) { // If state changed lastProjectState = store.getState().scratchGui.projectState.loadingState console.log('state '+state) if(state.endsWith('UPDATING')) { console.log('๐ข๐ข๐ข๐ข๐ข๐ข๐ข๐ข๐ข๐ข๐ข๐ข๐ข๐ข๐ข๐ข๐ข๐ข๐ข๐ข') chrome.runtime.sendMessage(exId,{meta:'projectSavedJSON',blId,json:vm.toJSON(),version:blVersion,}) // chrome.runtime.sendMessage(exId,{meta:'projectSaved',blId,scratchId,version:blVersion}) } try{ if(state == 'SHOWING_WITH_ID') { finishedSavingCB.forEach((func)=>{func()}) }}catch(e){console.error(e)} } // HANDLE TITLE CHANGE let title = store.getState().preview.projectInfo.title if(title != lastTitle && !titleYetUnset) { console.log(`title changed from ${lastTitle} to ${title}`) lastTitle = title if(title != settingTitle) { liveMessage({meta:'setTitle',blId,title}) } }})The service worker host and external message handler
background.jslet apiUrl = 'https://blocklive.micahp.me'; // Proxy project update messages chrome.runtime.onMessageExternal.addListener( function (request, sender, sendResponse) { (async () => { console.log("external message:", request); if (request.meta == 'getBlId') { if (!request.scratchId || request.scratchId == '.') { return '' } sendResponse((await (await fetch(`${apiUrl}/blId/${request.scratchId}/${uname}`, { headers: { authorization: currentBlToken } })).text()).replaceAll('"', '')) // } else if(request.meta =='getInpoint') { // sendResponse(await (await fetch(`${apiUrl}/projectInpoint/${request.blId}`)).json()) } else if (request.meta == 'getJson') { try { sendResponse(await (await fetch(`${apiUrl}/projectJSON/${request.blId}?username=${uname}`, { headers: { authorization: currentBlToken } })).json()) } catch (e) { sendResponse({ err: 'blocklive id does not exist' }) } } else if (request.meta == 'getChanges') { sendResponse(await (await fetch(`${apiUrl}/changesSince/${request.blId}/${request.version}`, { headers: { authorization: currentBlToken, uname } })).json()) } else if (request.meta == 'getUsername') { sendResponse(uname) } else if (request.meta == 'getUsernamePlus') { console.log('sending response') console.log({ uname, signedin, currentBlToken, apiUrl, verifyBypass }) sendResponse({ uname, signedin, currentBlToken, apiUrl, verifyBypass }); } else if (request.meta == 'callback') { tabCallbacks[sender.tab.id] = sendResponse } else if (request.meta == 'projectSaved') { // {meta:'projectSaved',blId,scratchId,version:blVersion} fetch(`${apiUrl}/projectSaved/${request.scratchId}/${request.version}`, { method: 'POST', headers: { authorization: currentBlToken } }) } else if (request.meta == 'projectSavedJSON') { // {meta:'projectSaved',blId,scratchId,version:blVersion} fetch(`${apiUrl}/projectSavedJSON/${request.blId}/${request.version}`, { method: 'POST', body: request.json, headers: { 'Content-Type': 'application/json', authorization: currentBlToken, uname } }) } else if (request.meta == 'myStuff') { sendResponse(await (await fetch(`${apiUrl}/userProjectsScratch/${await makeSureUsernameExists()}`, { headers: { authorization: currentBlToken } })).json()) } else if (request.meta == 'create') { // sendResponse(await(await fetch(`${apiUrl}/newProject/${request.scratchId}/${await refreshUsername()}?title=${encodeURIComponent(request.title)}`)).json()) sendResponse(await (await fetch(`${apiUrl}/newProject/${request.scratchId}/${await refreshUsername()}?title=${encodeURIComponent(request.title)}`, { method: 'POST', body: request.json, headers: { 'Content-Type': 'application/json', authorization: currentBlToken } }).then(res => res.json()).catch(e => ({ err: e.toString() })))) } else if (request.meta == 'shareWith') { fetch(`${apiUrl}/share/${request.id}/${request.username}/${uname}?pk=${request.pk}`, { method: 'PUT', headers: { authorization: currentBlToken } }) } else if (request.meta == 'unshareWith') { fetch(`${apiUrl}/unshare/${request.id}/${request.user}`, { method: 'PUT', headers: { authorization: currentBlToken, uname } }) } else if (request.meta == 'getShared') { sendResponse(await (await fetch(`${apiUrl}/share/${request.id}`, { headers: { authorization: currentBlToken, uname } })).json()) } else if (request.meta == 'getTitle') { sendResponse((await (await fetch(`${apiUrl}/projectTitle/${request.blId}`, { headers: { authorization: currentBlToken, uname } })).json()).title) } else if (request.meta == 'leaveScratchId') { fetch(`${apiUrl}/leaveScratchId/${request.scratchId}/${await refreshUsername()}`, { method: 'PUT', headers: { authorization: currentBlToken } }) } else if (request.meta == 'leaveBlId') { fetch(`${apiUrl}/leaveBlId/${request.blId}/${await refreshUsername()}`, { method: 'PUT', headers: { authorization: currentBlToken } }) } else if (request.meta == 'getActive') { sendResponse(await (await fetch(`${apiUrl}/active/${request.id}`, { headers: { authorization: currentBlToken, uname } })).json()) } else if (request.meta == 'getPingUrl') { sendResponse(await chrome.runtime.getURL("sounds/ping.mp3")) } else if (request.meta == 'isPingEnabled') { sendResponse((await chrome.storage.local.get(['ping'])).ping) } else if (request.meta == 'userExists') { sendResponse(await testUserExists(request.username)) } else if (request.meta == 'badges?') { sendResponse({badges:(await chrome.storage.local.get(['badges'])).badges}) } })(); return true; });- blocklive.micahp.me
Receives Blocklive collaboration API requests, including the projectSavedJSON save upload from the extension service worker.