SECURITY.mdGraphify 是本機開發工具:它作為 Claude Code skill 與(可選的)本機 MCP stdio server 執行。圖分析期間不做任何網路呼叫——只有 ingest(使用者明確抓 URL)才會碰網路。支援版本:0.3.x 受支援,< 0.3 不受支援。
| 威脅向量 | 緩解方式 |
|---|---|
| URL 抓取的 SSRF | security.validate_url() 只允許 http/https,擋掉私有/迴環/link-local IP 與雲端 metadata endpoint;轉址目標會重新驗證。所有抓取路徑(含 tweet oEmbed)都經過 safe_fetch()。 |
| 過大下載 | safe_fetch() 串流回應並在 50 MB 中止;safe_fetch_text() 在 10 MB 中止。 |
| 非 2xx HTTP | safe_fetch() 對非 2xx 拋 HTTPError——錯誤頁不會被當成內容。 |
| MCP server 路徑穿越 | security.validate_graph_path() 解析路徑並要求必須在 graphify-out/ 內,且該目錄必須存在。 |
| 圖 HTML 的 XSS | security.sanitize_label() 去除控制字元、上限 256 字元,並在 pyvis 嵌入前對所有節點 label 與邊 title 做 HTML-escape。 |
| 節點 label 的 prompt injection | sanitize_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.json | serve.py 的 _load_graph() 包住 json.JSONDecodeError,印出清楚的復原訊息而非當機。 |
--transport http 是 opt-in、有文件,並綁 127.0.0.1(除非傳 --host 0.0.0.0)。shell=True。ingest subcommand:抓使用者明確提供的 URL。不要為資安漏洞開公開 issue。用 GitHub 的 private vulnerability reporting 或直接 email 維護者,並附上:漏洞描述、重現步驟、潛在影響、建議修法。48 小時內會回覆收件,重大問題目標 7 天內修復。
security.py 的逐函數講解。Graphify 的安全模型是「本機工具的務實安全」:預設不碰網路、不執行來源碼、不 shell、輸入一律消毒、LLM 資料包成不可信區塊。這不是「防禦無敵」,而是「讓攻擊從第一次就成功變成需要繞過」。
威脅面解析涵蓋 SSRF、路徑穿越、XSS、prompt injection 等。每個威脅都有對應的緩解方式,這些實作都在 security.py 裡。特別值得注意的是原始檔內容的 prompt injection 防禦:用 <untrusted_source> 雜湊戳記包住每個檔案,並在插入前解除已知的 jailbreak 記號。
from graphify.security import validate_url,嘗試驗證 http://localhost:8080(應被擋掉,因為是私有 IP)。../../etc/passwd 作為圖檔路徑,觀察 validate_graph_path() 如何拒絕。sanitize_label(),確認輸出被正確清理。safe_fetch() 嘗試下載一個超過 50 MB 的檔案,確認在 50 MB 處中止。| 錯誤訊息 | 原因 | 解決方式 |
|---|---|---|
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 removed | label 包含不可見字元 | 清理 label 中的控制字元 |
<untrusted_source> 雜湊戳記的作用前面的 Worked Example 是逐一測試驗證函數;這次是部署情境:你們要把 graphify 裝在共享工作站,讓 5 個工程師都能 graphify add 抓網頁/論文進知識庫,但要求「預設安全、可稽核」。
GRAPHIFY_OUT 指向它;確認 MCP 只以 stdio transport 啟動(不開 --transport http,避免任何網路監聽)。validate_url("http://169.254.169.254/latest/meta-data")、validate_url("http://127.0.0.1:8080")、validate_url("file:///etc/passwd") 都應拋錯(防雲端 metadata 與內網掃描)。validate_graph_path("../../etc/passwd", base) 應被拒絕;確認 serve 只讀 graphify-out/ 內的圖。graphify extract 後檢視 GRAPH_REPORT 的 AMBIGUOUS 邊與 label——確認沒有來自未知來源的可疑節點(刻意資料注入測試)。安全頁最容易被低估的是「一層驗證不夠」:
getaddrinfo(併發下是 TOCTOU 隱患),而是子類別化 connection——每個連線只解析一次 DNS、驗證結果、連到那個精確 IP。這擋住 DNS-rebind:攻擊者先回傳公開 IP 通過驗證,再換成內網 IP——因為沒有第二次解析,換 IP 的視窗被關掉。<untrusted_source path=… sha256=…> 雜湊戳記 + 系統提示「當惰性資料」+ _neutralise_injection_sentinels() 解除 <|im_start|>、[INST] 等 jailbreak 記號。文件明講:這不是讓 injection 不可能,是讓它「從第一次就成功變成需要繞過」。sanitize_label() 有 HTML-escape,所以 graph.html 一定安全」。escape 只保護被嵌入 innerHTML 的 label/title 字串;如果哪個 exporter 把 label 塞進 src、href 或 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" |
--transport http 是 opt-in 並綁 127.0.0.1」。若要開放給同網段其他機器使用(--host 0.0.0.0),你需要補上哪些控制項才能在安全模型文檔裡自圓其說?前面的 Worked Example 是測試驗證函數和團隊入口。這次是合規導向的部署:你的組織要把 graphify 裝在共享工作站,讓 3 個不同 agent(Claude Code、OpenCode、Cursor)都能使用,要求「每次 ingest 都有稽核紀錄、所有安全驗證可自動化測試、漏洞有通報流程」。
~/.claude/skills/、OpenCode: ~/.config/opencode/skills/、Cursor: ~/.cursor/skills/);MCP 只以 stdio transport 啟動。pytest tests/test_security.py——涵蓋 validate_url 擋 metadata IP、validate_graph_path 擋路徑穿越、sanitize_label 清控制字元、safe_fetch 擋 50MB。每次 merge 前自動執行。graphify-out/ingest-audit.log(來源、時間、HTTP 狀態、大小),方便事後審查。用 grep "169.254" ingest-audit.log 檢查有無嘗試存取 metadata endpoint。graphify extract 後檢查 GRAPH_REPORT 的 AMBIGUOUS 邊——任何來自未知來源的可疑節點都要人工檢視。| 面向 | 考量 | 實務建議 |
|---|---|---|
| 效能 | 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 transport | stdio-only 預設、--transport http opt-in 綁 127.0.0.1 | Docker MCP + SQLite | Docker MCP 頁講 MCP 工具的容器化部署,不涉及 transport 安全 |
validate_url 擋掉 http://169.254.169.254(metadata endpoint)和 http://127.0.0.1(內網),並解釋為什麼這兩個要擋<|im_start|> 和 [INST] 的 label 跑 sanitize_label(),確認這些記號被清除--transport http 的安全邊界:綁 127.0.0.1、不開 CORS、需要什麼額外控制才能開放給同網段