安全模型

威脅面逐條解析——Graphify 保護什麼、不保護什麼
來源:SECURITY.md

定位先講清楚

Graphify 是本機開發工具:它作為 Claude Code skill 與(可選的)本機 MCP stdio server 執行。圖分析期間不做任何網路呼叫——只有 ingest(使用者明確抓 URL)才會碰網路。支援版本:0.3.x 受支援,< 0.3 不受支援。

威脅面解析

威脅向量緩解方式
URL 抓取的 SSRFsecurity.validate_url() 只允許 http/https,擋掉私有/迴環/link-local IP 與雲端 metadata endpoint;轉址目標會重新驗證。所有抓取路徑(含 tweet oEmbed)都經過 safe_fetch()
過大下載safe_fetch() 串流回應並在 50 MB 中止;safe_fetch_text() 在 10 MB 中止。
非 2xx HTTPsafe_fetch() 對非 2xx 拋 HTTPError——錯誤頁不會被當成內容。
MCP server 路徑穿越security.validate_graph_path() 解析路徑並要求必須在 graphify-out/ 內,且該目錄必須存在。
圖 HTML 的 XSSsecurity.sanitize_label() 去除控制字元、上限 256 字元,並在 pyvis 嵌入前對所有節點 label 與邊 title 做 HTML-escape。
節點 label 的 prompt injectionsanitize_label() 也套用於 MCP 文字輸出——使用者控制的檔案 label 無法破壞回傳給代理的文字格式。
原始檔內容的 prompt injection語意 pass 期間原始檔是混進 LLM context 的攻擊者可控文字。_read_files()llm.py<untrusted_source path=... sha256=...> 雜湊戳記包住每個檔案;抽取 system prompt 要求模型把該區塊當惰性資料、絕不當指令;_neutralise_injection_sentinels() 在插入前解除 <|im_start|>[INST]<<SYS>>、偽造 </untrusted_source> 等已知 chat-template/jailbreak 記號。這是 table-stakes 防禦(issue #1210):不是讓 injection 不可能,而是讓它從「第一次就成功」變成「需要繞過」。
YAML frontmatter injection_yaml_str() 在嵌入使用者控制字串(網頁標題、查詢問題)前 escape 反斜線、雙引號與換行。
來源檔編碼當機所有 tree-sitter byte slice 用 errors="replace" 解碼——非 UTF-8 來源檔優雅降級而非中止抽取。
Symlink 穿越detect.py 全境用 os.walk(..., followlinks=False)
損壞的 graph.jsonserve.py_load_graph() 包住 json.JSONDecodeError,印出清楚的復原訊息而非當機。

Graphify 不做的事

可選的網路呼叫

回報漏洞

不要為資安漏洞開公開 issue。用 GitHub 的 private vulnerability reporting 或直接 email 維護者,並附上:漏洞描述、重現步驟、潛在影響、建議修法。48 小時內會回覆收件,重大問題目標 7 天內修復。

本站觀點:這份威脅面模型是「本機工具的務實安全」——重點不在防禦無敵,而在「預設不碰網路、不執行、不 shell、輸入一律消毒、LLM 資料包成不可信區塊」。想讀實作,程式碼對照 · ops 裡有 security.py 的逐函數講解。

教學解說

Graphify 的安全模型是「本機工具的務實安全」:預設不碰網路、不執行來源碼、不 shell、輸入一律消毒、LLM 資料包成不可信區塊。這不是「防禦無敵」,而是「讓攻擊從第一次就成功變成需要繞過」。

威脅面解析涵蓋 SSRF、路徑穿越、XSS、prompt injection 等。每個威脅都有對應的緩解方式,這些實作都在 security.py 裡。特別值得注意的是原始檔內容的 prompt injection 防禦:用 <untrusted_source> 雜湊戳記包住每個檔案,並在插入前解除已知的 jailbreak 記號。

Worked Example:測試安全驗證

  1. 測試 URL 驗證:在 Python 中呼叫 from graphify.security import validate_url,嘗試驗證 http://localhost:8080(應被擋掉,因為是私有 IP)。
  2. 測試路徑穿越:嘗試用 ../../etc/passwd 作為圖檔路徑,觀察 validate_graph_path() 如何拒絕。
  3. 測試 label 消毒:用包含控制字元的 label 呼叫 sanitize_label(),確認輸出被正確清理。
  4. 測試安全 fetch:用 safe_fetch() 嘗試下載一個超過 50 MB 的檔案,確認在 50 MB 處中止。
關鍵觀察:每個驗證函數都有明確的錯誤訊息,不會默默失敗。這是「fail loud」的安全設計原則。

常見錯誤與診斷

錯誤訊息原因解決方式
validate_url: only http/https allowed嘗試用 file:// 或 ftp:// 協議改用 http:// 或 https://
validate_graph_path: must be inside graphify-out/嘗試存取 graphify-out/ 以外的路徑確保路徑在 graphify-out/ 目錄內
safe_fetch: response too large下載內容超過 50 MB減少下載範圍或用 streaming 處理
sanitize_label: control characters removedlabel 包含不可見字元清理 label 中的控制字元

練習與驗收清單

① 進階真實情境 Worked Example:為「允許 ingest 的團隊入口」建立受控環境

前面的 Worked Example 是逐一測試驗證函數;這次是部署情境:你們要把 graphify 裝在共享工作站,讓 5 個工程師都能 graphify add 抓網頁/論文進知識庫,但要求「預設安全、可稽核」。

  1. 固定安裝與路徑:graphify-out 統一放共享 volume;GRAPHIFY_OUT 指向它;確認 MCP 只以 stdio transport 啟動(不開 --transport http,避免任何網路監聽)。
  2. 先測 SSRF 防護:在 CI 加一組 security smoke test——validate_url("http://169.254.169.254/latest/meta-data")validate_url("http://127.0.0.1:8080")validate_url("file:///etc/passwd") 都應拋錯(防雲端 metadata 與內網掃描)。
  3. 驗證路徑界線validate_graph_path("../../etc/passwd", base) 應被拒絕;確認 serve 只讀 graphify-out/ 內的圖。
  4. 審查抽取內容:跑 graphify extract 後檢視 GRAPH_REPORT 的 AMBIGUOUS 邊與 label——確認沒有來自未知來源的可疑節點(刻意資料注入測試)。
  5. 建立漏洞通報流程:把 SECURITY.md 的「private vulnerability reporting、48h 回覆」貼進團隊章程,規定不開公開 issue。
為什麼選這條路徑:團隊共享環境的安全問題不是「單一漏洞」,而是「預設邊界 + 可驗證 + 通報管道」三件套。stdio-only、URL 白名單、路徑強制、內容審查,每一件都能用一行測試驗證——這讓安全不只是宣稱,而是 CI 可稽核的事實。

② 深入原理擴充:SSRF 的兩道防線、與 prompt injection 的「table-stakes」哲學

安全頁最容易被低估的是「一層驗證不夠」:

大家以為建圖正確、但其實有誤的案例:以為「sanitize_label() 有 HTML-escape,所以 graph.html 一定安全」。escape 只保護被嵌入 innerHTML 的 label/title 字串;如果哪個 exporter 把 label 塞進 srchref 或 URL 參數(沒有對應的 URL-escape),或某個屬性忘了走 esc(),XSS 仍可能發生。同理,「safe_fetch 擋 50MB」不保證下游處理安全——記憶體放大、zip bomb 都還可能。安全是「每一層都對」,不是「有一個 sanitizer 就夠」。

③ 診斷式疑難排解表

症狀可能原因解決方案
ingest 抓正常網站卻被擋網站 DNS 解析出 CDN 的內網 IP、或用 IP 直連的私有位址確認不是私有網段;若是合法服務,評估風險後加到允許清單並記錄理由
圖 HTML 出現「彈跳視窗/腳本執行」某條 label 路徑沒走 HTML-escape追 exporter 的 esc() 覆蓋;測 <img src=x onerror=…> 作為 label 跑 export
語意抽取把檔內容當成指令執行injection sentinel 沒被解除(自訂 prompt 移除了 _neutralise_injection_sentinels確認抽取 prompt 保留 sentinel 清理;檢查 <untrusted_source> 包覆是否完整
MCP server 讀到圖外的檔案client 傳了絕對路徑且 validate_graph_path 被繞過確認所有 server 工具都走 validate_graph_path;不要直接 join 使用者輸入
非 UTF-8 來源檔讓抽取中斷tree-sitter byte slice 沒用 errors="replace" 解碼確認走標準 decode 路徑;自行包 decode 時加 errors="replace"

④ 進階挑戰題

  1. DNS-rebind 防禦靠「連線時只解析一次」。請畫出攻擊時序(validate → DNS1 → connect → DNS2 換 IP)並指出 `_SSRFGuardedHTTPConnection` 在哪一步切斷了攻擊。
  2. 「table-stakes」的意思是防禦只能提高攻擊成本。請設計一個「第二層」防禦:如果注入文字成功混進抽取 prompt,你如何在結果端偵測「這條邊可能是注入產物」?
  3. --transport http 是 opt-in 並綁 127.0.0.1」。若要開放給同網段其他機器使用(--host 0.0.0.0),你需要補上哪些控制項才能在安全模型文檔裡自圓其說?

① 專案級端到端 Worked Example:為多 agent 共享環境建立安全審計管線

前面的 Worked Example 是測試驗證函數和團隊入口。這次是合規導向的部署:你的組織要把 graphify 裝在共享工作站,讓 3 個不同 agent(Claude Code、OpenCode、Cursor)都能使用,要求「每次 ingest 都有稽核紀錄、所有安全驗證可自動化測試、漏洞有通報流程」。

  1. 環境隔離:graphify-out 放在共享 volume;三個 agent 的 skill 安裝在各自 scope(Claude Code: ~/.claude/skills/、OpenCode: ~/.config/opencode/skills/、Cursor: ~/.cursor/skills/);MCP 只以 stdio transport 啟動。
  2. 安全 smoke test CI:在 CI 跑一組 pytest tests/test_security.py——涵蓋 validate_url 擋 metadata IP、validate_graph_path 擋路徑穿越、sanitize_label 清控制字元、safe_fetch 擋 50MB。每次 merge 前自動執行。
  3. Ingest 稽核:所有 ingest 的 URL 記錄到 graphify-out/ingest-audit.log(來源、時間、HTTP 狀態、大小),方便事後審查。用 grep "169.254" ingest-audit.log 檢查有無嘗試存取 metadata endpoint。
  4. Content 審查:跑 graphify extract 後檢查 GRAPH_REPORT 的 AMBIGUOUS 邊——任何來自未知來源的可疑節點都要人工檢視。
  5. 漏洞通報:把 SECURITY.md 的「private vulnerability reporting、48h 回覆」貼進團隊章程,規定不開公開 issue。每季演練一次通報流程。
為什麼選這條路徑:多 agent 共享環境的安全不是「裝一次就夠」——每個 agent 的 skill 安裝位置不同、hook 機制不同、always-on 配置不同。安全審計管線確保「所有 agent 都走同一套驗證」,而稽核紀錄讓你能在事後追溯「哪個 agent 在什麼時候抓了什麼 URL」。

② 效能/品質/安全深度

面向考量實務建議
效能validate_url 的 DNS 解析 + 逐 IP 檢查有延遲;SSRFGuardedHTTPConnection 每個連線只解析一次 DNS不要 monkey-patch 全域 getaddrinfo(併發下是 TOCTOU 隱患);用子類別化 connection 確保每個連線驗證一次
品質prompt injection 防禦是「table-stakes」——讓攻擊從第一次就成功變成需要繞過不要移除 _neutralise_injection_sentinels();確認 <untrusted_source> 包覆完整;定期更新已知 jailbreak 記號清單
安全圖 HTML 的 XSS 只被 sanitize_label() 的 HTML-escape 保護;如果 exporter 忘了走 esc(),XSS 仍可能<img src=x onerror=alert(1)> 作為 label 跑 export;追蹤所有 exporter 的 esc() 覆蓋

③ 文件間比較對照表

面向本文(安全模型)相關文差異說明
威脅面10 個威脅向量的逐條解析架構總覽架構頁只簡述 security.py 四個驗證函數,不深入威脅面
Prompt injection<untrusted_source> + sentinel 清理 + 系統提示運作原理運作原理頁講信任標籤(EXTRACTED/INFERRED),不涉及 injection 防禦
SSRF 防禦validate_url + SSRFGuardedHTTPConnection 雙道防線程式碼對照 · ops程式碼對照頁逐函數講 security.py,含 DNS-rebind 防禦的時序
MCP transportstdio-only 預設、--transport http opt-in 綁 127.0.0.1Docker MCP + SQLiteDocker MCP 頁講 MCP 工具的容器化部署,不涉及 transport 安全

④ 互動式檢核清單