BLA
BLA (Blind LLM Agents) is the open-source Blue Defense MCP and CLI built to protect software artifacts against automated AI reverse-engineering workflows.
翻譯準備中
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證A serverless, browser-only template for building WebAssembly-powered web-exploitation CTF challenges — VitePress + Vue 3 + Pyodide + php-wasm.
翻譯準備中
取自儲存庫 README
正在載入完整 README。
檢視完整 README ↗繁體中文 | English
完全跑在瀏覽器裡、以 WebAssembly 驅動的 web 資安練習平台,不需要任何後端伺服器。
Web eXploitation Laboratory(WXL) 是 CTF 風格的 web 資安練習平台。每一題都完全在瀏覽器裡執行:WebAssembly 模擬出真實的後端環境,所以不必架設任何伺服器就能部署和使用。
它的目標是讓老師不用架伺服器就能備課,也讓學生回家可以繼續練習。fork 這個 repo、執行 pnpm fork:init,或執行 npm create wxl,就能把自己的靶場發布到 Cloudflare Pages 或 GitHub Pages。這個 repo 附有四道示範題,線上網站在 https://wxl.browserlab.online。
| 層級 | 技術 |
|---|---|
| 文件框架 | VitePress 2.0.0-alpha.16 |
| UI 框架 | Vue 3 3.5 + UnoCSS |
| 狀態管理 | Pinia 3 |
| Python runtime | Pyodide 0.29 |
| PHP runtime | php-wasm |
| WASM 模組 | Rust 2021 + wasm-pack |
| 攻擊紀錄保存 | IndexedDB(idb 套件) |
| 套件管理 | pnpm 10.28 |
Node.js >= 22.6:challenge:keygen、create:challenge、challenge:validate、challenge:analyze 透過 node --experimental-strip-types 執行,較舊的版本沒有這個功能。其餘 TypeScript 腳本走內建的 tsx,沒有版本下限。CI 用 Node 24 建置。
pnpm >= 10(npm install -g pnpm)
Rust 工具鏈(用 rustup 安裝)
wasm-pack(cargo install wasm-pack --version 0.14.0 --locked):版本與 CI 相同。它不是套件相依,要安裝到 Rust 工具鏈裡
wasm-tools(cargo install wasm-tools --version 1.249.0 --locked,或 pnpm wasm:tools):版本與 CI 相同。pnpm challenge:verify 的 L2 階段要用它對產生的 payload 執行 wasm-tools validate。pnpm challenge:keygen 的 strip 與 mutate 也會用到,沒有安裝時只會各印一則警告。
Playwright 用的 Chromium:第一次執行 pnpm challenge:verify 或 pnpm test:smoke 之前要裝好。pnpm install 之後執行一次:
pnpm exec playwright install chromium
# 1. 下載專案
git clone https://github.com/BrowserLaboratory/wxl-platform.git
cd wxl-platform
# 2. 安裝 Node.js 相依套件
pnpm install
# 3. 建置 WASM 模組並啟動開發伺服器
pnpm dev
開發伺服器預設在 http://localhost:5173。
| 指令 | 說明 |
|---|---|
pnpm dev | 建置 WASM 模組並啟動開發伺服器 |
pnpm build | 建置 WASM 模組並輸出靜態網站 |
pnpm docs:dev | 只啟動 VitePress 開發伺服器(跳過 WASM 建置) |
pnpm docs:build | 只建置 VitePress 靜態網站 |
pnpm docs:preview | 預覽建置好的靜態網站 |
pnpm test | 執行 TypeScript/JavaScript 單元測試(Vitest)。直接執行會停在 watch 模式,跑一次就結束請用 pnpm test --run |
pnpm test:smoke | 對建置好的網站執行 Playwright smoke 測試 |
pnpm wasm:build | 建置所有 Rust WASM 模組 |
pnpm wasm:test | 執行 Rust 單元測試(cargo test) |
pnpm wasm:tools | 嘗試把 wasm-tools 裝進 Rust 工具鏈;失敗也不會報錯、一律以 0 結束,請用 wasm-tools --version 確認 |
pnpm fork:init | fork 之後改寫專案身分(作者、GitHub 網址、靶場網址、網站名稱,以及依 --host 決定的部署設定)。必須帶 --author 和 --repo;每次執行都要帶齊參數,--host 與 --base 互斥;套件名稱只在帶 --name 或 --rebrand 時才會改 |
pnpm challenge:keygen | 為每一題產生加密的 WASM 模組 |
pnpm create:challenge | 依參數建立新題目的骨架;必須帶 --name <slug>,不帶參數會印出用法並以 1 結束 |
pnpm challenge:validate | 檢查每一題的 frontmatter 與檔案結構 |
pnpm challenge:analyze | 列出一題的內容與設定 |
pnpm challenge:retype | 修改既有題目的 backend/難度/tags/分類 |
pnpm challenge:verify | 對一題執行分層驗證閘門(L1 lint、L2 build、L3 Playwright e2e) |
pnpm challenge:verify:blind | 單獨執行 L4 盲解(也可以用 pnpm challenge:verify <slug> --blind) |
pnpm challenge:verify:cross | L4 多 agent 交叉驗證:用 claude,codex,gemini 各跑一次盲解並彙整結果 |
pnpm prepare | 安裝 git hooks(simple-git-hooks); 之後會自動執行 |
Browser
├── VitePress site (Vue 3 + UnoCSS)
│ ├── Challenge pages (Markdown + YAML frontmatter)
│ └── IndexedDB (attack-session persistence + tool state)
├── Service Worker (platform/public/challenge-sw.js)
│ └── Intercepts HTTP requests and routes them to the matching WASM runtime
└── WASM runtimes
├── virtual-fs Encrypted virtual filesystem (Rust)
├── asgi-bridge Python ASGI/WSGI bridge layer (Rust)
├── python-bridge Pyodide integration (TypeScript)
└── php-bridge php-wasm integration (TypeScript)
兩個 Rust crate 放在 chall-wasm/,由 pnpm wasm:build 建置。
每一題是一個 Markdown 檔,用 YAML frontmatter 宣告設定:
---
title: Need to Know # 必填
layout: challenge # 套用題目介面;沒有這行會顯示成一般文件頁
backend: fastapi # 必填:flask | fastapi | php
app: app.py # 必填:相對於該題 src/ 的路徑
difficulty: easy
category: web
packages: [] # 透過 micropip 額外安裝的 Python 套件
tools: [ browser, network, repeater, code ] # 省略時就是這四個;
# 明確列出可以縮小範圍,
# 但拿掉 browser 也會被加回來
source_visible: false # true = 白箱題,false = 黑箱題(預設)
wasmModule: /challenge/door-is-open/runtime.wasm # 由 keygen 產生
---
題目的原始碼由掃描該題的 src/ 目錄取得。舊的 fs: 對應寫法為了相容仍可使用,但驗證工具會發出棄用警告,新題目一律用 src/ 掃描。
分支策略、PR 流程與 commit 慣例請見 CONTRIBUTE.md。
每個改到題目或 platform/public/_redirects 的 commit,pre-commit hook 都會重新產生這個檔;hook 擋下 commit 時,訊息會說明怎麼做。以下幾種情況,光是重新 commit 不夠:
_redirects 的第一個 commit,只要 platform/ 底下除了 _redirects 以外還有沒 stage 或沒追蹤的檔案就會被擋,因為第一個 commit 之前 git 沒辦法 stash。把它們 stage(或移除不該放進來的檔案),再 commit 一次。platform/ 底下還沒 stage 的變更和未追蹤的檔案留在一個 stash entry,並印出它的 commit id。照訊息執行 git stash apply <id>。apply 拒絕時,訊息印出的 git restore 指令會用 entry 裡的版本覆寫 platform/ 底下的檔案,所以要在改動那裡的任何東西之前執行。apply 回報衝突時(兩處變更改到同一行或相鄰的行就會這樣),用 git checkout <id> -- <path> 從 entry 取回那個檔案。這時 git 會把那個檔案,以及 entry 裡其他已經套用的變更,都留在 stage 的狀態;執行 git restore --staged --source=<id>^2 -- ':(top)platform/' ':(top,exclude)platform/public/_redirects' 可以放回 commit 之前 stage 的內容,所以 commit 之前先看一下 git status。git restore --staged --source=<id>^2 -- …。這個指令會用 commit 之前 stage 的內容覆寫 platform/ 底下已 stage 的內容,所以要等檔案都放回來、而且還沒 stage 那裡的任何東西時執行。訊息只印這個指令時,你沒 stage 的檔案已經放回來了;否則先照上一項的指令做。_redirects:pnpm redirects:generate 會讀整個工作目錄,所以未追蹤的題目草稿在 frontmatter 裡還沒有 number 時會拒絕,訊息會指出是哪一份草稿。先修好或移走草稿;或是 stage 一個題目變更再 commit,hook 會依 stage 的內容重新產生這個檔。--no-verify commit 進去,之後每個題目 commit 都會被擋,直到把英文題目 commit 進來,或用 git rm -r platform/zh-TW/challenge/<slug>/ 移除這份說明頁。前面兩項「沒辦法放回去」的情況裡,hook 的 stash entry 會一直留在 git stash list 裡。檔案和 index 都放回來之後,用 git stash list --format='%gd %H' 找出 id 和訊息印出的一樣的那一行,再用那一行的 git stash drop stash@{<n>} 刪掉它;git stash drop 不接受 id 本身。
建置結果放在 .vitepress/dist/,是純靜態檔案,可以部署到任何靜態網站服務。
# 1. 安裝相依套件
pnpm install
# 2. 完整建置(WASM + keygen + VitePress)
pnpm build
npm create wxl 建立靶場不想 fork 整個 repo,也可以用這個指令直接建立一個全新的靶場資料夾(需要 Node.js >= 22.6):
npm create wxl <資料夾>
# 或
pnpm create wxl <資料夾>
互動模式會依序詢問作者、GitHub repository、部署主機、網站網址、網站名稱,套用設定之後會自動執行
git init 並建立第一個 commit;靶場裡會多一個 range.json,記下它是從哪一版上游建立的設定紀錄。
不想互動回答,也可以直接帶齊參數:
npm create wxl <資料夾> -- --author "<你的名字>" --repo <owner>/<repo> --host github-pages
只想要還沒套用任何設定的檔案、不建立 git repository,可以加 --no-setup:
npm create wxl <資料夾> -- --no-setup
create-wxl從 3.0.0 起發布在 npm 上;指令會下載與套件同一版的上游來建立靶場。
帶必要參數執行 fork:init:
# 部署到 GitHub Pages
pnpm fork:init --author "<你的名字>" --repo <owner>/<repo> --host github-pages --site-name "<你的靶場名稱>"
# 部署到 Cloudflare Pages
pnpm fork:init --author "<你的名字>" --repo <owner>/<repo> --host cloudflare-pages --origin https://<你的專案>.pages.dev --site-name "<你的靶場名稱>"
它會改寫 package.json 的身分欄位、把上游的 GitHub 網址換成你的、依 --site-name 設定網站標題與兩個首頁的靶場名稱、依 --origin(或由 --host github-pages 推導)設定靶場網址(複製出來的每一條連結都從它開始),並依下表決定要不要從 scripts/templates/deploy.yml.template 把 .github/workflows/deploy.yml 裝上——這個 repo 本身沒有 deploy workflow(它透過下一節的 Cloudflare Pages 發布),所以新 fork 在那個位置沒有檔案,安裝會成功;如果你已經放了自己的 workflow,腳本不會覆蓋,只會發出警告。
既有 .github/workflows/deploy.yml | --host github-pages | --host cloudflare-pages | 不帶 --host |
|---|---|---|---|
| 不存在 | 安裝目前範本,並提示補 tag 規則 | 不動 | 安裝目前範本,並提示補 tag 規則 |
| 與目前範本相同 | 不動,不警告 | 刪除,並警告 | 不動,不警告 |
| 與已知舊範本輸出相同 | 取代為目前範本,並警告它是舊版輸出 | 刪除,並警告 | 保持原樣,警告內容不同、需要手動合併 |
| 其他(兩者皆非,可能是你自己客製的版本) | 能推導出 SITE_BASE 就保持並警告;不能就保持並警告改用 --host github-pages | 保持原樣,並警告 | 保持原樣,警告內容不同、需要手動合併 |
fork:init 不保留任何狀態,每次執行都要帶齊參數:它不會記得你上一次用了什麼設定,這一次沒帶的參數就當作「這次不改」,而不是「沿用上一次」——但有兩個例外,不管這次帶了什麼都照樣會動:(a) 不帶 --host 時,只要 .github/workflows/deploy.yml 還不存在就會裝回(見上表);(b) --host github-pages 沒帶 --origin 時,靶場網址每次都會依 --repo 重新推導並覆寫,覆寫一個不是上游預設值的舊網址時會警告新舊網址。Cloudflare Pages 靶場每次都要帶 --host cloudflare-pages——如果某一次忘了帶、改用不帶 --host 的指令,不帶 --host 執行會重新裝回 GitHub Pages workflow,而這份 workflow 對一個 Cloudflare Pages 靶場毫無用處;自訂網域的 GitHub Pages 靶場同理每次都要帶 --origin,否則下一次沒帶 --origin 的 --host github-pages 執行會把靶場網址覆寫回推導值。rebrand 模式(--rebrand)不能和 --host/--origin/--site-name 一起用,部署設定要另外手動處理,細節見 wxl-fork-init skill。
--host 與 --base 互斥:帶 --host(不論哪一種)時,.vitepress/config.mts 的 base 定義一律回到上游這一行:
const siteBase = process.env.SITE_BASE || '/'
https://<owner>.github.io/<repo>/)→ --host github-pages。裝好的 workflow 會在部署時先執行 configure-pages,由 GitHub 自己算出這個網站的 base path 再交給建置流程,不管是專案網站、使用者網站還是未來換了自訂網域都一樣,原始碼裡再也沒有任何路徑寫死。沒有帶 --origin 時,靶場網址會依 --repo 推導成 https://<owner>.github.io/<repo>(使用者網站 <owner>.github.io 則省略路徑);用了自訂網域要自己帶 --origin。--host cloudflare-pages,必須帶 --origin https://<你的專案>.pages.dev(或你的自訂網域,不能帶路徑);這個指令不會裝 GitHub Pages 的 workflow。短連結只在 Cloudflare Pages 上有效。 短連結是
platform/public/_redirects裡的規則,只有 Cloudflare Pages 會讀這個檔,所以在 GitHub Pages 的靶場上,短連結開不到題目。每道題目旁的複製按鈕,只有在 Cloudflare Pages 建置的靶場上才會複製短連結;其他靶場複製的是題目頁的網址。題目改名之後這個網址就會失效;而且只有.vitepress/challenge/shortLink.ts的SITE_ORIGIN是靶場的網址時,它才會開到這個靶場。
platform/public/_redirects由題目自動產生,不要手改;pnpm create:challenge與 pre-commit hook 會在題目變動時重新產生它。它要跟題目一起 commit:GitHub Pages 不讀這個檔,但 CI 會檢查它和題目是否一致。如果 CI 回報它過期,執行pnpm redirects:generate再 commit 結果。
不帶 --host 時,這一行只依 --base 改寫;沒帶 --base 就維持原狀,可能還是上游原本那一行,也可能是前一次 --base 留下的字面值。--base /<repo>/ 把這段路徑寫死進 .vitepress/config.mts;--base none 寫成字面值 const siteBase = '/'(不是清掉這一行)。用 --base 的靶場,CI 的 site smoke 與 challenge 測試會失敗——這兩種測試都從 / 出發操作頁面,寫死的子路徑會讓它們 404;改用 --host github-pages 把 base 交給部署當下決定,本機與 CI 的每一次建置都停在 /,就不會遇到這個問題。
--site-name 同時設定網站標題與兩個首頁的靶場名稱,接受大部分符號(包含引號、反斜線與 $),但拒絕 <、>、&,長度上限 60 個 Unicode code point(不是位元組或字母數);部分不可見字元與控制字元(Cc、Cs、Zl、Zp 類別,以及大多數 Cf 類別)也會被拒絕,只有零寬連接符 U+200C/U+200D 例外。
GitHub Pages 的 fork 還有兩個設定在 GitHub 介面上,不在 repo 裡:
github-pages environment 除了允許 main 分支(workflow_dispatch 需要它),還要允許從 v* tag 部署。沒有這條規則,由 tag 觸發的部署 job 會被拒絕。同一個 owner 的 GitHub Pages 靶場共用瀏覽器儲存空間:同一個 GitHub 帳號底下的每個 GitHub Pages 靶場,學習者的瀏覽器看到的是同一個瀏覽器 origin(scheme 加 host;跟 GLOSSARY.md 另一個標著攻擊紀錄是本機還是匯入來的 origin 詞條只是同名),一個靶場留下的紀錄,另一個靶場也讀得到、也清得掉;詳見 .proj.specs/OPEN-QUESTIONS.md 的 OQ-018。
https://wxl.browserlab.online 就是用這個方式發布的。
在 Cloudflare Pages 建立新專案,連結 GitHub repo。
設定建置指令:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal \
&& . "$HOME/.cargo/env" \
&& cargo install wasm-pack --version 0.14.0 --locked \
&& pnpm install \
&& pnpm build
輸出目錄設成 .vitepress/dist。
環境變數加上 NODE_VERSION=24。
Cloudflare Pages 靶場部署在網域根目錄:只要是用 --host cloudflare-pages 設定出來的靶場,base 定義已經回到上游那行 process.env.SITE_BASE || '/'——Cloudflare Pages 的建置環境本身不會自動設定 SITE_BASE,所以這裡不需要加任何環境變數。但如果你先前在 Cloudflare Pages 專案設定裡手動加過 SITE_BASE,請現在刪除:留著的話它照樣會蓋掉這行字面值,而 .vitepress/config.mts 要求這個值要嘛是 /、要嘛是以 / 結尾的路徑,少了結尾斜線會讓建置直接丟出錯誤中止。
注意:Cloudflare Pages 預設沒有 Rust 工具鏈。
wasm-pack是 Rust 執行檔,不是套件相依,pnpm install不會提供它,所以上面的建置指令會先裝最小的 Rust 工具鏈和wasm-pack再建置。wasm-pack固定為 CI 使用的 0.14.0,並加上--locked使用 wasm-pack 自己的 lockfile:不加的話 cargo 會重新解析最新的相容相依版本,某個相依的相容版本若全部被撤回(yanked),建置就會失敗(2026-10-16 的zip2.x 就是如此)。這裡沒有安裝wasm-tools。pnpm build會執行pnpm challenge:keygen,它的 strip 與 mutate 會用到wasm-tools,但沒有這個工具時只會各印一則警告,建置仍會成功,只是少了這兩個步驟。如果希望正式建置也做 strip 與 mutate,請在安裝那行加上cargo install wasm-tools --version 1.249.0 --locked(與 CI 相同的版本)。
本專案採用 Educational Community License, Version 2.0(ECL-2.0) 授權。
pnpm install