積木遊戲工坊 網站儲存型跨站腳本 Stored XSS - HITCON ZeroDay

Vulnerability Detail Report

Vulnerability Overview

  • ZDID: ZD-2026-00912
  •  發信 Vendor: 積木遊戲工坊
  • Title: 積木遊戲工坊 網站儲存型跨站腳本 Stored XSS
  • Introduction: 攻擊者無須登入即可透過分享作品功能,把 JavaScript 惡意程式碼儲存在作品的角色造型(`costume`)欄位中;當任何使用者開啟該分享連結 `https://ai-scratch.zeabur.app/play/<id>` 時,惡意程式碼會在網站來源(origin)下自動執行,且全程無需點擊互動。

處理狀態

目前狀態

公開
Last Update : 2026/09/09
  • 新提交
  • 已審核
  • 已通報
  • 未回報修補狀況
  • 未複測
  • 公開

處理歷程

  • 2026/07/10 21:41:22 : 新提交 (由 Ianon 更新此狀態)
  • 2026/07/10 21:47:18 : 新提交 (由 Ianon 更新此狀態)
  • 2026/07/12 03:05:52 : 審核完成 (由 HITCON ZeroDay 服務團隊 更新此狀態)
  • 2026/07/23 16:44:39 : 審核完成 (由 HITCON ZeroDay 服務團隊 更新此狀態)
  • 2026/07/23 16:44:39 : 通報未回應 (由 HITCON ZeroDay 服務團隊 更新此狀態)
  • 2026/07/23 16:44:39 : 通報未回應 (由 HITCON ZeroDay 服務團隊 更新此狀態)
  • 2026/09/09 03:00:03 : 公開 (由 HITCON ZeroDay 平台自動更新)

詳細資料

  • ZDID:ZD-2026-00912
  • 通報者:Ianon (Ianon)
  • 風險:中
  • 類型:基於 DOM 的 XSS (DOM-based Cross-Site Scripting)

參考資料

攻擊者可經由該漏洞竊取使用者身份,或進行掛碼、轉址等攻擊行為。

漏洞說明: OWASP - Cross-site Scripting (XSS)
https://www.owasp.org/index.php/Cross-site_Scripting_(XSS)

防護原則:
https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet

XSS 防禦繞過方式:
https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet
(本欄位資訊由系統根據漏洞類別自動產生,做為漏洞參考資料。)

相關網址

https://ai-scratch.zeabur.app/api/projects (儲存惡意 payload 的端點,POST)
https://ai-scratch.zeabur.app/play/<id> (觸發執行的分享連結)
https://ai-scratch.zeabur.app/#p=<壓縮資料> (純前端 hash 變體,無需後端儲存)

敘述

網站前端在 js/app.js 的 renderSpriteList()(第 154–157 行)繪製角色卡片時,會把角色造型欄位 costume 的值未經跳脫(escape)直接寫入 innerHTML:

const faceContent = s.costume?.startsWith('img:')
  ? `<img src="/api/images/${s.costume.slice(4)}.png" alt="${escapeHtml(s.name)}" ...>`
  : s.costume;                                   // ← 原始字串,未跳脫
card.innerHTML = `<div class="face">${faceContent}</div>` +
                 `<div class="name">${escapeHtml(s.name)}</div>...`;

角色名稱 name 有經過 escapeHtml 處理,但 costume 沒有。當 costume 不是以 img: 開頭時,其原始內容會被當成 HTML 直接插入頁面。

作品資料來自未經身分驗證的分享功能:使用者開啟 /play/<id> 時,前端(js/app.js 第 66–84 行)會呼叫 Storage.loadFromServer(id) 取回作品並立即渲染;#p= hash 變體則完全在前端還原。兩條路徑都會把攻擊者可控的 sprites[].costume 送進上述 sink,在頁面載入時自動執行,無需受害者任何互動。網站回應標頭中沒有 Content-Security-Policy,因此沒有任何機制可阻擋 payload 執行。

重現步驟

1. 植入惡意作品(無需登入)

  • 對 POST /api/projects 送出含惡意 costume 的作品。
  • 伺服器回傳分享 id,且 payload 原封不動被儲存(以 GET /api/projects/<id> 驗證)。
    
    POST /api/projects HTTP/2
    Host: ai-scratch.zeabur.app
    Content-Type: application/json

{"name":"poc","sprites":[{"id":"s1","name":"poc",
"costume":"<img src=x onerror=alert(document.domain)>",
"x":0,"y":0,"dir":90,"size":100,"visible":true,"workspace":null}]}

回應:{"id":"-ls_gM6"}


(實測回應與儲存內容見證據截圖三,payload 原封不動存回。)

**2. 誘使受害者開啟分享連結**
- 將 `https://ai-scratch.zeabur.app/play/-ls_gM6` 傳給受害者。
- 預期結果:頁面一載入即執行注入的 `onerror`,跳出 `alert` 對話框顯示
  `ai-scratch.zeabur.app`,證明惡意 JavaScript 已在網站來源(origin)下執行。
- 實測(2026-07-10):於一般瀏覽器開啟即彈出 `ai-scratch.zeabur.app` 的 alert
  (證據截圖一);DevTools 可見注入的 `<img src="x" onerror="alert(document.domain)">`
  節點位於 `div.sprite-card > div.face` 內(證據截圖二),確認成功執行。

修補建議

1. **不要把 `costume` 未經處理放進 `innerHTML`**:
將 `costume` 視為「白名單內的 emoji」或「`img:<id>` 參照」兩種合法型態;其餘一律拒絕或跳脫。
最佳做法是改用 DOM API(`document.createElement`、`textContent`、以驗證過的 id 設定 `img.src`)
建立角色卡片,避免以字串串接產生 HTML:
```js
const face = document.createElement('div');
face.className = 'face';
if (s.costume?.startsWith('img:') && /^[A-Za-z0-9_-]+$/.test(s.costume.slice(4))) {
const img = document.createElement('img');
img.src = `/api/images/${s.costume.slice(4)}.png`;
img.alt = s.name;
face.appendChild(img);
} else {
face.textContent = s.costume ?? ''; // emoji 以純文字呈現,不會變成標記
}
```

2. **後端輸入驗證**:`POST /api/projects` 應對 `sprites[].costume` 做正規化/驗證
(限定為允許的 emoji 集合或 `img:<合法 id>`),使分享快照無法夾帶 HTML 標記。

擷圖

留言討論

聯絡組織

 發送私人訊息
您也可以透過私人訊息的方式與組織聯繫,討論有關於這個漏洞的相關資訊。
;