Day 9,我們把 Gemini API Key 留在後端,避免 Secret 進入 Git 與瀏覽器。
但 Secret 沒有外洩,不代表系統就安全。只要程式把使用者輸入當成指令、查詢或 HTML,攻擊者仍可能改變原本預期的行為。
今天會用一個本機 Demo 回答三個問題:
Injection 發生在程式把資料送進另一種語言或直譯器,卻沒有保留「資料」與「指令」的界線。
常見情境包含:
問題不只是輸入包含分號、引號或角括號。真正的問題是接收資料的元件如何解讀這些字元。
例如分號在一般專案名稱中只是文字,在 Shell 中卻可以分隔兩個指令。<img> 在純文字中只是標記字串,交給瀏覽器的 HTML Parser 後則可能建立真正的元素。
因此,安全處理不能只靠一份「危險字元黑名單」。
今天的程式放在:
demo-app/input-demo/scenario.js
進入 Demo 專案:
cd /media/mickey/777/ithome/demo-app
執行:
npm run input:demo
這個腳本不連線到外部服務,也不修改檔案。它會把受控的測試字串分別送進 Shell、參數化 Process API 與 HTML Template,比較安全與不安全的結果。
假設系統要執行一個檢查工具,開發者直接把名稱拼進 Shell Command:
const unsafeCommand = `printf 'Checking %s\n' ${projectName}`;
const result = await exec(unsafeCommand);
測試輸入不是普通名稱,而是:
release-notes; printf 'INJECTED\n'
Shell 會把分號解讀成指令分隔符。實際輸出為:
Unsafe command output: "Checking release-notes\nINJECTED"
原本只想顯示專案名稱,結果卻執行了輸入中的第二個 printf。
這個 Demo 使用無害指令,但正式系統若以高權限執行,攻擊者可能讀取檔案、呼叫內部服務、修改資料或取得執行環境中的 Secret。
問題不在 printf,而在 exec() 會把整段字串交給 Shell 解讀。
Node.js 的 execFile() 可以把執行檔與參數分開傳遞:
const result = await execFile("printf", [
"Checking %s\n",
projectName
]);
同一筆輸入的結果為:
Safe argument output: "Checking release-notes; printf 'INJECTED\\n'"
分號、空白與引號仍然存在,但它們只是單一參數中的文字。Process API 沒有請 Shell 把它們當成另一個指令。
同樣原則也適用於 SQL。不要自行加入引號再拼接字串:
const sql = `SELECT * FROM projects WHERE owner_id = '${ownerId}'`;
應使用資料庫 Driver 提供的 Parameterized Query:
const result = await db.query(
"SELECT * FROM projects WHERE owner_id = $1",
[ownerId]
);
參數化不是把危險字元刪掉,而是由 API 明確區分 SQL 結構與資料值。
避免字串拼接可以阻止 Injection,但系統仍要確認資料是否符合業務規則。
假設部署 API 的 channel 只允許三種值:
const allowedChannels = new Set([
"dev",
"staging",
"production"
]);
驗證函式採用 Allowlist:
function validateChannel(value) {
if (!allowedChannels.has(value)) {
throw new Error(
"channel must be dev, staging, or production"
);
}
return value;
}
Demo 結果如下:
"production" -> ACCEPTED as production
"release-notes; printf 'INJECTED\\n'" -> REJECTED: channel must be dev, staging, or production
Allowlist 適合狀態、角色、排序欄位、檔案類型與其他有限集合。它比列出所有危險字元可靠,因為攻擊語法與編碼方式會持續變化。
對自由文字則不能只允許少數固定值。系統可以驗證:
驗證失敗時應回傳明確的 4xx 錯誤,不要默默改成另一個值,也不要讓資料一路進入資料庫或其他服務後才失敗。
HTML 提供 required、maxlength 與 type="email" 等功能。這些檢查可以改善使用體驗,卻不能取代後端驗證。
攻擊者不必使用我們提供的表單。他可以透過 curl、瀏覽器開發者工具或自己的程式直接呼叫 API。
因此:
Browser validation -> 提早提醒正常使用者
Server validation -> 保護系統的信任邊界
Database constraint -> 保護資料最終狀態
三層可以同時存在,但後端不能因為前端已經檢查就相信輸入。
有些內容本來就允許自由輸入,例如專案名稱、留言或文章標題。這些資料即使符合長度限制,輸出到不同位置時仍要安全處理。
Demo 使用以下文字:
<img src=x onerror="alert('XSS')">
不安全版本直接插入 HTML:
const html = `<p>Project: ${projectName}</p>`;
產生結果:
<p>Project: <img src=x onerror="alert('XSS')"></p>
瀏覽器會把輸入解析成元素,事件屬性可能執行 JavaScript。這就是 Stored XSS 或 Reflected XSS 的常見起點,取決於資料是先保存再顯示,還是立即反射到 Response。
安全版本在放入 HTML Text Context 前執行編碼:
function escapeHtml(value) {
return value
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
輸出變成:
<p>Project: <img src=x onerror="alert('XSS')"></p>
瀏覽器會顯示原始文字,而不是建立 <img> 元素。
實際產品應優先使用框架預設的文字綁定,例如 DOM 的 textContent。如果產品允許部分 HTML,則需要採用經過維護的 HTML Sanitizer 與明確的標籤、屬性 Allowlist,不要自行用 Regex 清理 HTML。
HTML Text、HTML Attribute、URL、CSS 與 JavaScript 是不同的解析情境。適用於 HTML 文字的編碼方式,不一定能安全地放進 JavaScript:
<script>
const project = "使用者輸入";
</script>
也不能因為 URL 經過 HTML Encoding,就允許任意 javascript: Scheme。
更安全的原則是:
Content Security Policy 可以降低部分 XSS 的影響,但它是額外防線,不是直接插入不可信 HTML 的理由。
| 控制措施 | 回答的問題 | 範例 |
|---|---|---|
| 驗證 Validation | 這筆資料符合業務允許的格式嗎? | Channel 只能是 dev、staging、production |
| 參數化 Parameterization | 資料與指令是否保持分離? | SQL Placeholder、execFile() 參數 |
| 編碼 Encoding | 資料放進特定輸出情境後,會不會被當成語法? | HTML Entity Encoding |
| 清理 Sanitization | 允許部分主動內容時,哪些結構必須移除? | HTML 標籤與屬性 Allowlist |
它們不能互相取代。
一個有效的專案名稱仍然要用參數化查詢寫入資料庫,也要在顯示時安全編碼。反過來說,HTML Encoding 不會讓不合法的部署 Channel 突然符合業務規則。
Agent 不應看到 req.body 就直接回報 Injection。使用者輸入本來就是許多功能的必要資料。
Agent 要沿著資料流找到真正的 Interpreter 或輸出位置:
可操作的 Finding 應包含:
問題:專案名稱被拼接後交給 Shell 執行
來源:API Request 的 projectName
Sink:child_process.exec(unsafeCommand)
證據:Template Literal 直接建立完整 Shell Command
攻擊輸入:release-notes; printf 'INJECTED\n'
驗證:npm run input:demo
結果:輸出出現第二個指令產生的 INJECTED
影響:攻擊者可能以服務身分執行任意指令
修正:改用 execFile 並以獨立參數傳值
如果程式已使用參數化 API,Agent 就不能只因輸入包含特殊字元而判定存在 Injection。報告必須證明不可信資料可以跨越資料與指令的界線。
輸入永遠不可信,不代表所有使用者都懷有惡意,而是系統不能把信任建立在 Client 會乖乖使用表單。
今天建立了三層防線:
刪除幾個特殊字元無法解決所有 Injection。真正可靠的設計,是從 API 層就保留資料與程式語法的界線。
明天,我們會檢查相依套件與供應鏈風險。即使自己的程式碼安全,下載進專案的套件、版本與安裝流程仍可能成為攻擊入口。