Is Blocklive: Scratch Realtime Collaboration! safe?

Medium risk

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.

45Risk
Who publishes it

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.

status.uptime-monitor.io
Also called by 1 other listing: Livescratch: Live Scratch Collabs!
assets.scratch.mit.edu
Also called by 2 other listings: Livescratch: Live Scratch Collabs!, ScratchTools
cdn2.scratch.mit.edu
Also called by 2 other listings: Livescratch: Live Scratch Collabs!, ScratchTools
pngall.com
Also called by 4 other listings, including ProTab | Arbfonts
uploads.scratch.mit.edu
Also called by 5 other listings, including Livescratch: Live Scratch Collabs!, ScratchTools, Scratch Addons

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
What actually happens
You did this

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 did this

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 data assembled for the save upload
  • Project targets
    targets: [{"isStage":true,"name":"Stage"},{"isStage":false,"name":"Sprite1"}]

    This represents the stage and sprite objects that make up your Scratch project.

  • Blocks and scripts
    blocks: {"event_whenflagclicked":{"opcode":"event_whenflagclicked"}}

    This can include the saved code blocks and script structure in your project.

  • Project assets
    costumes: [{"name":"costume1","assetId":"bcf454acf82e4504149f7ffe07081dbc"}]

    This can include costumes and sounds referenced by the project file.

  • Variables and lists
    variables: {"score":["score",0]}

    This can include saved variable and list names and values from the project.

  • Blocklive project id
    projectSavedJSON/67239418/31

    This ties the uploaded JSON to a specific Blocklive collaboration project.

  • Blocklive account headers
    headers: {"authorization":"<redacted>","uname":"scratch_user_17"}

    These can associate the upload with the signed-in Blocklive user and session.

Captured request
POSThttps://blocklive.micahp.me/projectSavedJSON/${request.blId}/${request.version}

Dynamic analysis observed Blocklive WebSocket connections but did not capture this save POST.

Headers
uname
Blocklive username
Content-Type
application/json
authorization
<redacted>
The code that does this

Save detection and upload path in the shipped extension

Readable version

The content script injects the editor script on Scratch project pages

injectors/editor.js
console.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.js
store.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.js
let 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;    });
Destination named by the shipped code
    • blocklive.micahp.me

    Receives Blocklive collaboration API requests, including the projectSavedJSON save upload from the extension service worker.

Updated 30 September 2026gelkmljpoacdjkjkcfekkmgkpnmeomlk