# Streamlit Security Audit

> 對 Streamlit（尤其部署在 Streamlit Community Cloud）＋ Supabase／Postgres 的應用做資安稽核，並把發現完整修補到上線。涵蓋威脅建模、逐項驗證、修補策略、部署後確認，以及這個技術堆疊特有的陷阱（雲端拿不到 Cookie 與 client IP、session_state 撐不過 OAuth 來回、共用單一 DB 連線造成跨使用者交易污染、Supabase public schema 預設對外、自訂元件的非同步回傳）。**使用者提到資安、安全性、漏洞、稽核、掃描、注入、XSS、SSRF、RLS、權限、速率限制、濫用防護、OAuth 登入安全、上傳檔案安全、祕密外洩，或說「這樣安全嗎」「有沒有漏洞」「上線前幫我檢查」時就使用。** 在 Streamlit 專案裡新增資料表、外部請求、使用者輸入欄位或身分機制之後也應主動使用。

- Skill: `lml679939-cmyk/streamlit-security-audit` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add lml679939-cmyk/streamlit-security-audit`
- Raw SKILL.md: https://api.skillmd.com/api/skills/lml679939-cmyk/streamlit-security-audit/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- License: MIT
- Author: lml679939-cmyk (https://skillmd.com/u/lml679939-cmyk)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/lml679939-cmyk/streamlit-security-audit

---


# Streamlit + Supabase 應用的資安稽核

## 這份 skill 的來歷與定位

來自一次對真實上線專案的完整稽核：11 個發現全部修掉並部署。過程中最貴的一課不是
找漏洞，而是**修好之後把正式站弄壞了**——「Streamlit Cloud 拿得到 XSRF cookie」
這個沒驗證過的假設被寫進了程式註解，於是「拿不到祕密就拒絕登入」的修法直接把
所有人的登入擋死。

所以這份 skill 的重點不是漏洞清單（那種東西 OWASP 寫得比誰都好），而是
**怎麼把發現變成真的有效、而且不會弄壞東西的修改**——以及這個技術堆疊裡哪些
「看起來合理的假設」其實是錯的。

**適用**：Streamlit 應用，特別是部署在 Streamlit Community Cloud、且後端用
Supabase 或直連 Postgres 的。多數判斷對任何多使用者 Python web app 都成立。

---

## 起手式：先確認有沒有威脅模型

看專案裡有沒有 `SECURITY.md` 之類的文件，而且**內容跟程式碼對得上**。

有的話，裡面的「安全不變條件」就是最好的掃描依據——**直接拿每一條去對程式碼找
違反處**，比漫無目的地讀程式有效得多。例如「身分只能由伺服器端決定」這條，
就直接對應到「grep 所有 DB 查詢，確認使用者識別碼都是伺服器端算出來的」。

沒有、或內容過時的話，**先建立／修正它再開始掃描**——拿過時的威脅模型去掃描，
會剛好略過最敏感的部分。做法見 `references/threat-model.md`（含可直接套用的架構）。

> 實際案例：那次稽核前的 `SECURITY.md` 寫著「IP 定位送到 ip-api.com」，但程式早就
> 改用別的服務了，而且整個資料庫持久化層與訪客身分機制**完全沒被提到**。拿那份去
> 做 AI 資安掃描，會直接略過最敏感的資產。

---

## 流程

### 1. 盤點：確認威脅模型仍然成立

對照文件逐項確認，重點放在上次稽核之後的新增功能：

- 新的資料表？→ 一定要一起開 RLS 並 revoke（見 `references/stack-traps.md` §5）
- 新的外部請求？→ 資料裡帶來的網址一律要白名單
- 新的使用者輸入欄位或身分機制？→ 那是新的信任邊界穿越點
- 新的祕密？→ 補進資產清單，並確認沒進版控（連歷史都要查）

### 2. 掃描

跑 `references/sweeps.md` 的檢查。那份按面向分組（祕密、注入、XSS、SSRF、
身分與授權、節流、日誌、依賴、資料庫權限、上傳），每組都寫了「看到什麼算有問題」
——因為多數指令的乾淨結果是**沒有輸出**，不知道要看什麼的話很容易誤讀。

### 3. 排序：嚴重度是假設，不是結論

寫下發現時先給初判嚴重度，但**在驗證之前那只是假設**。

> 實際案例：一個「資料庫沒開 RLS」的發現，初判為 High（「anon key 一外流＝全站
> 資料可讀寫刪」）。實際查下去發現 PostgREST 需要的四個權限 `anon` 一個都沒有，
> 攻擊路徑本來就是斷的——降為 Medium。**先驗證再定調，不然力氣會花在錯的地方。**

### 4. 驗證：每個發現都要能重現

這是整套流程裡最重要的一步。`references/verify-recipes.md` 有四種做法：
攻擊模擬、對真實資料庫的唯讀對照、把前端 JS 抽出來用 Node 實跑、在本機重現雲端條件。

兩條紀律：

- **證明迴歸測試不是空過的。** 寫完測試後把**舊版邏輯**也跑一次，確認它真的會失敗。
  一條「本來就會過」的測試等於沒有測試，而且會給人虛假的安全感。
- **跟部署環境有關的判斷，要在部署環境上量過。** 本機跟 Streamlit Cloud 的差異大到
  會推翻結論（`references/stack-traps.md` §1）。量不到就想辦法在本機重現那個條件。

### 5. 修補：在最低層擋，讓不變條件無法被繞過

能在底層擋就不要在呼叫點加 `if`。

> 實際案例：簽章函式在「拿不到金鑰」時直接**拋例外**，而不是在呼叫端檢查一次。
> 這樣「用空金鑰簽章」在**任何**程式路徑上都不可能發生，包括以後新增的呼叫點。
> 空金鑰的 HMAC 攻擊者也算得出來，兩端都空時比對會通過——看起來驗過了、實際上零
> 綁定效果，而且完全無聲。這種失效模式只有在底層擋才能根除。

其他反覆用到的判斷：

- **fail closed，但要分清楚「還沒好」與「不可能好」。** 兩者混為一談的話，正常
  使用者會在首輪就被誤判成攻擊。用一個明確的哨符值把兩者分開。
- **淘汰／降級的順序本身可能是安全決策。** 快取或額度表滿了要丟哪一個？直覺的 LRU
  常常是錯的（見 `references/stack-traps.md` §6）。問「攻擊者能不能利用這個行為」。
- **修完要確認功能還在。** 資安修改最常見的失敗不是沒擋住，是把正常路徑也擋掉了。

### 6. 上線並在正式站確認

跑完測試、照專案的部署規則走，然後**一定要讀正式站的日誌**。

> 實際案例：登入故障只在雲端日誌裡看得到（一行 `[AUTH]` 警告不斷洗版），
> 本機和全部測試都完全正常。沒去讀日誌的話，會以為修好了。

Streamlit Cloud 的部署細節（何時要手動 Reboot、日誌裡哪些是雜訊）見
`references/stack-traps.md` §10。

---

## 收尾

- **改了行為就更新威脅模型文件。** 它是下次掃描的依據。
- 專案的開發筆記要寫「**症狀 → 真正原因 → 量法**」，不要只寫結論——
  結論沒有可驗證性，下一個人（或下一個你）沒辦法判斷它還成不成立。
- commit 訊息把**驗證數字**寫進去（例如「10,800 次攻擊嘗試 → 400 次通過」）。
  沒有數字的資安修改，三個月後沒人知道它到底有沒有效。

## 參考檔案

| 檔案 | 什麼時候讀 |
|---|---|
| `references/threat-model.md` | 專案還沒有威脅模型、或內容過時時 |
| `references/sweeps.md` | 步驟 2，實際執行掃描時 |
| `references/stack-traps.md` | 步驟 1／4／5。**動到 OAuth、身分、資料庫連線、節流、上傳之前先讀** |
| `references/verify-recipes.md` | 步驟 4，要證明發現為真或修法有效時 |

