From 668c3d6f87db8de14cd6a9e1b44416ed97d13d2a Mon Sep 17 00:00:00 2001 From: kim jeong yong Date: Fri, 29 May 2026 17:15:22 +0900 Subject: [PATCH] =?UTF-8?q?[fix]=20=EB=B8=94=EB=A1=9D=20prototype=20pollut?= =?UTF-8?q?ion=20=EA=B8=B0=EB=B0=98=20Stored=20XSS=20=EC=B7=A8=EC=95=BD?= =?UTF-8?q?=EC=A0=90=20=EC=B0=A8=EB=8B=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 특정 코드가 포함된 작품 실행 시 prototype pollution을 이용해 Stored XSS가 실행될 수 있는 취약점을 다계층으로 차단한다. - Entry.Scope.filterReservedKeywords: 키로 사용 시 "__proto__"로 강제 변환되는 배열/객체 형태(예: ["__proto__"])까지 차단하도록 보강 - block_KKMOO: kkmoo_set_frame_time / kkmoo_set_frame / kkmoo_set_motor_degree의 bracket-write 싱크에 정수 인덱스 범위 검증 추가 (NaN/범위초과 키 거부) - 하드웨어 5종(davinci/microbit/armz/asomebot/asomekit): _merge(Entry.hw.sendQueue, {[blockId]: data})를 직접 대입으로 교체해 lodash merge의 prototype write-through 가젯 제거. blockId가 매번 고유 해시라 동작은 동일. (asomebot/asomekit는 lodash 미import로 ReferenceError 상태이던 잠재 버그도 수리) - lodash 의존성 플로어를 ^4.17.21로 상향해 merge/set 내부 가드가 항상 보장되도록 함 Co-Authored-By: Claude Opus 4.8 (1M context) --- package.json | 2 +- pnpm-lock.yaml | 2 +- src/playground/blocks/hardware/block_KKMOO.js | 34 ++++++++++++++++--- src/playground/blocks/hardware/block_armz.js | 8 +---- .../blocks/hardware/block_asomebot.js | 4 +-- .../blocks/hardware/block_asomekit.js | 4 +-- .../blocks/hardware/block_davinci.js | 5 +-- .../blocks/hardware/block_microbit.js | 5 +-- src/playground/scope.js | 5 ++- 9 files changed, 40 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index ca3fc4de56..edc65650c1 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "jest": "^24.9.0", "khaiii": "^0.0.2", "libsvm-js": "^0.2.1", - "lodash": "^4.17.15", + "lodash": "^4.17.21", "mathjs": "^7.1.0", "ml-cart": "^2.1.1", "pixi.js": "5.3.12", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7de17ddfcb..a8bbc770f5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -78,7 +78,7 @@ importers: specifier: ^0.2.1 version: 0.2.1 lodash: - specifier: ^4.17.15 + specifier: ^4.17.21 version: 4.17.23 mathjs: specifier: ^7.1.0 diff --git a/src/playground/blocks/hardware/block_KKMOO.js b/src/playground/blocks/hardware/block_KKMOO.js index 972b5a4af2..6d16651f71 100644 --- a/src/playground/blocks/hardware/block_KKMOO.js +++ b/src/playground/blocks/hardware/block_KKMOO.js @@ -653,15 +653,23 @@ Entry.kkmoo.getBlocks = function () { func: function (sprite, script) { const motnum = script.getField('MOTNUM', script); const angle = script.getValue('ANGLE', script); + const index = Number(motnum); + if ( + !Number.isInteger(index) || + index < 0 || + index >= Entry.kkmoo.motData.length + ) { + return script.callReturn(); + } Entry.hw.update(); if (script.isStart != true) { script.isStart = true; if (angle >= -90 && angle <= 90) { - Entry.kkmoo.motData[motnum].angle = angle; + Entry.kkmoo.motData[index].angle = angle; } else if (angle > 90) { - Entry.kkmoo.motData[motnum].angle = 90; + Entry.kkmoo.motData[index].angle = 90; } else { - Entry.kkmoo.motData[motnum].angle = -90; + Entry.kkmoo.motData[index].angle = -90; } return script; } else { @@ -854,11 +862,19 @@ Entry.kkmoo.getBlocks = function () { isNotFor: ['kkmoo'], func: function (sprite, script) { const motnum = script.getField('FRAME', script); + const index = Number(motnum); + if ( + !Number.isInteger(index) || + index < 0 || + index >= Entry.kkmoo.motionFrame.length + ) { + return script.callReturn(); + } Entry.hw.update(); if (script.isStart != true) { script.isStart = true; var data = Entry.kkmoo.copyObj(Entry.kkmoo.motData); - Entry.kkmoo.motionFrame[motnum].data = data; + Entry.kkmoo.motionFrame[index].data = data; return script; } else { delete script.isStart; @@ -974,10 +990,18 @@ Entry.kkmoo.getBlocks = function () { func: function (sprite, script) { const motnum = script.getField('FRAME', script); const time = script.getValue('TIME', script); + const index = Number(motnum); + if ( + !Number.isInteger(index) || + index < 0 || + index >= Entry.kkmoo.motionFrame.length + ) { + return script.callReturn(); + } Entry.hw.update(); if (script.isStart != true) { script.isStart = true; - Entry.kkmoo.motionFrame[motnum].time = time; + Entry.kkmoo.motionFrame[index].time = time; return script; } else { delete script.isStart; diff --git a/src/playground/blocks/hardware/block_armz.js b/src/playground/blocks/hardware/block_armz.js index b8637d93b5..568e5d6a63 100644 --- a/src/playground/blocks/hardware/block_armz.js +++ b/src/playground/blocks/hardware/block_armz.js @@ -1,9 +1,5 @@ 'use strict'; -const _set = require('lodash/set'); -const _get = require('lodash/get'); -const _merge = require('lodash/merge'); - Entry.Armz = new class Armz { constructor() { // this.id = 'FF.FF'; @@ -104,9 +100,7 @@ Entry.Armz = new class Armz { postSendQueue({ script, data }, scope) { const blockId = this.getHashKey(); - _merge(Entry.hw.sendQueue, { - [blockId]: data, - }); + Entry.hw.sendQueue[blockId] = data; } postCallReturn(args) { diff --git a/src/playground/blocks/hardware/block_asomebot.js b/src/playground/blocks/hardware/block_asomebot.js index 88ebfa193c..4e4809e867 100644 --- a/src/playground/blocks/hardware/block_asomebot.js +++ b/src/playground/blocks/hardware/block_asomebot.js @@ -58,9 +58,7 @@ Entry.AsomeBot = { scope.timeFlag = 1; this.nowBlockId = blockId; this.blockIds[blockId] = false; - _merge(Entry.hw.sendQueue, { - [blockId]: data, - }); + Entry.hw.sendQueue[blockId] = data; Entry.hw.update(); setTimeout(() => { scope.timeFlag = 0; diff --git a/src/playground/blocks/hardware/block_asomekit.js b/src/playground/blocks/hardware/block_asomekit.js index c3ff687491..49d060dbed 100644 --- a/src/playground/blocks/hardware/block_asomekit.js +++ b/src/playground/blocks/hardware/block_asomekit.js @@ -58,9 +58,7 @@ Entry.AsomeKit = { scope.timeFlag = 1; this.nowBlockId = blockId; this.blockIds[blockId] = false; - _merge(Entry.hw.sendQueue, { - [blockId]: data, - }); + Entry.hw.sendQueue[blockId] = data; Entry.hw.update(); setTimeout(() => { scope.timeFlag = 0; diff --git a/src/playground/blocks/hardware/block_davinci.js b/src/playground/blocks/hardware/block_davinci.js index 4444bb4bfc..3dd3ef6b4c 100644 --- a/src/playground/blocks/hardware/block_davinci.js +++ b/src/playground/blocks/hardware/block_davinci.js @@ -2,7 +2,6 @@ const _set = require('lodash/set'); const _get = require('lodash/get'); -const _merge = require('lodash/merge'); Entry.Davinci = new class Davinci { constructor() { @@ -73,9 +72,7 @@ Entry.Davinci = new class Davinci { scope.timeFlag = 1; this.nowBlockId = blockId; this.blockIds[blockId] = false; - _merge(Entry.hw.sendQueue, { - [blockId]: data, - }); + Entry.hw.sendQueue[blockId] = data; Entry.hw.update(); setTimeout(() => { scope.timeFlag = 0; diff --git a/src/playground/blocks/hardware/block_microbit.js b/src/playground/blocks/hardware/block_microbit.js index e15a89f7c2..c4f52bd368 100644 --- a/src/playground/blocks/hardware/block_microbit.js +++ b/src/playground/blocks/hardware/block_microbit.js @@ -2,7 +2,6 @@ const _set = require('lodash/set'); const _get = require('lodash/get'); -const _merge = require('lodash/merge'); Entry.Microbit = new (class Microbit { constructor() { @@ -73,9 +72,7 @@ Entry.Microbit = new (class Microbit { scope.timeFlag = 1; this.nowBlockId = blockId; this.blockIds[blockId] = false; - _merge(Entry.hw.sendQueue, { - [blockId]: data, - }); + Entry.hw.sendQueue[blockId] = data; Entry.hw.update(); setTimeout(() => { scope.timeFlag = 0; diff --git a/src/playground/scope.js b/src/playground/scope.js index bcf714db76..78d758428e 100644 --- a/src/playground/scope.js +++ b/src/playground/scope.js @@ -27,7 +27,10 @@ class Scope { static _reservedKeywords = new Set(['__proto__']); filterReservedKeywords(param) { - return Scope._reservedKeywords.has(param) ? '' : param; + // 배열/객체를 키로 쓸 때 toString 변환(예: ["__proto__"] → "__proto__")으로 + // 예약어를 우회하는 것을 차단한다. + const normalized = typeof param === 'object' && param !== null ? String(param) : param; + return Scope._reservedKeywords.has(normalized) ? '' : param; } getParams() {