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.
Browser-based web exploitation challenge platform powered by WASM
Translation pending
SecurityHTML · JavaScriptGitHub @BrowserLaboratoryOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
A serverless, browser-only template for building WebAssembly-powered web-exploitation CTF challenges — VitePress + Vue 3 + Pyodide + php-wasm.
Translation pending
Taken from the repository README
Loading the full README.
Read the full 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