前幾天,我一直提到一個問題:後台顯示使用者的 Email,算不算資安漏洞?
答案不是單純的「算」或「不算」。
我們要先確認四件事:
Email 出現在只有授權客服能使用的帳號查詢畫面,可能符合業務需求。Email 如果出現在公開 API、一般使用者的回應或所有工程師都能查看的 Log,風險就完全不同。
今天會建立一個本機 API,觀察個人資料如何從三個出口流出:
後端常把資料庫物件直接轉成 JSON:
sendJson(response, 200, userRecord);
假設 userRecord 包含以下欄位:
{
uid: "user-123",
displayName: "Mickey",
email: "mickey@example.test",
recoveryEmail: "backup@example.test",
role: "user",
internalRiskNote: "Manual review required"
}
前端可能只需要顯示名稱,API 卻回傳整個物件。畫面即使沒有顯示其他欄位,使用者仍能從瀏覽器開發者工具或網路請求中看到完整回應。
OWASP API Security Top 10 將這類問題列入 Broken Object Property Level Authorization。API 必須確認使用者可以讀取哪些物件,也要確認使用者可以讀取物件中的哪些欄位。
安全版本只挑選這個功能需要的資料:
sendJson(response, 200, {
uid: userRecord.uid,
displayName: userRecord.displayName
});
這種寫法稱為輸出 Allowlist。新增資料庫欄位時,API 不會自動把新欄位一起傳給使用者。
今天的程式放在:
demo-app/privacy-demo/
├── server.js
└── scenario.js
server.js 提供四個 Endpoint:
| Endpoint | 行為 |
|---|---|
GET /unsafe/profile |
回傳完整使用者物件 |
GET /safe/profile |
只回傳 UID 與顯示名稱 |
POST /unsafe/projects |
回傳內部錯誤並記錄完整請求 |
POST /safe/projects |
回傳錯誤代碼與 Request ID |
所有 Email 與 Token 都是假資料。這個 Demo 只能在本機執行。
進入專案:
cd /media/mickey/777/ithome/demo-app
執行情境:
npm run privacy:demo
不安全版本回傳:
{
"uid": "user-123",
"displayName": "Mickey",
"email": "mickey@example.test",
"recoveryEmail": "backup@example.test",
"role": "user",
"internalRiskNote": "Manual review required"
}
安全版本回傳:
{
"uid": "user-123",
"displayName": "Mickey"
}
兩個 Endpoint 都成功回應,但暴露的資料量不同。
不安全版本有三項問題:
這裡的修正不是在前端隱藏欄位。資料一旦進入 HTTP Response,就已經交給使用者。
不安全版本捕捉錯誤後,把 error.message 與 error.stack 直接傳回 Client:
sendJson(response, 500, {
message: error.message,
stack: error.stack
});
實際回應包含:
{
"message": "Database rejected owner email mickey@example.test in projects table",
"stack": "Error: Database rejected owner email ... at Server..."
}
這個回應暴露 Email、內部元件名稱與程式執行位置。
Stack Trace 不一定能單獨形成可利用漏洞,但它會增加攻擊者掌握的系統資訊。如果錯誤訊息還包含 SQL、檔案內容、連線字串或 Secret,影響會更高。
安全版本只回傳固定錯誤代碼與 Request ID:
sendJson(response, 500, {
code: "INTERNAL_ERROR",
requestId
});
實際回應如下:
{
"code": "INTERNAL_ERROR",
"requestId": "e35c1d79-1942-47bc-af9d-570027caccea"
}
使用者可以把 Request ID 提供給客服或工程師。工程師再使用相同 ID 查詢內部 Log。
這個方法同時達成兩個目標:
開發者遇到難以重現的錯誤時,常直接記錄整個 Request:
logger.error("UNSAFE_LOG", {
headers: request.headers,
body,
stack: error.stack
});
這段程式會把 Headers、Request Body 與 Stack Trace 全部交給 Logger。
Headers 可能包含:
Request Body 可能包含:
Log 通常會集中保存,也可能被更多工程師、維運工具或外部服務存取。資料進入 Log 後,刪除與追蹤會比刪除資料庫欄位更困難。
OWASP Logging Cheat Sheet 建議不要直接記錄 Access Token、Session ID、密碼、連線字串、加密金鑰及敏感個人資料。Email 等資料也可能需要刪除、遮罩或去識別化。
安全版本只記錄處理事件所需的欄位:
{
event: "project.create.failed",
requestId,
method: request.method,
path: request.url,
email: maskEmail(body.email)
}
實際輸出如下:
{
"event": "project.create.failed",
"requestId": "e35c1d79-1942-47bc-af9d-570027caccea",
"method": "POST",
"path": "/safe/projects",
"email": "mi***@example.test"
}
遮罩後的 Email 仍可能屬於個人資料。團隊仍要限制 Log 存取權限、設定保存期限,並確認這個欄位是否真的有助於排查問題。
如果 Request ID 已足夠找到相關資料,Log 就不需要 Email。
測試送出以下 Header:
Authorization: Bearer demo-token-not-a-real-secret
Terminal 輸出把值顯示成 ******。這是執行環境提供的顯示遮罩,不是我們的應用程式完成了資料清理。
不安全程式仍執行:
headers: request.headers
也就是說,應用程式把完整 Headers 交給 Logger。正式環境使用的 Logging Library、傳輸工具與儲存平台未必會自動遮罩。
我們不能看到星號就判定程式安全。真正的修正是不要把 Authorization Header 傳給 Logger。
Agent 不能只搜尋 email、password 或 token。同一個欄位在不同情境中可能有不同用途。
Agent 應沿著資料流回答五個問題:
以下 Finding 才具有可操作性:
問題:Profile API 回傳功能不需要的個人與內部欄位
證據:GET /unsafe/profile 直接序列化完整 userRecord
接收者:任何能呼叫 Endpoint 的使用者
暴露資料:email、recoveryEmail、internalRiskNote
影響:個人資料與內部註記遭未授權揭露
修正:建立 Response DTO,只允許 uid 與 displayName
驗證:比較 unsafe 與 safe Endpoint 的 JSON Response
如果 Agent 只說「程式使用 Email,可能違反隱私」,我們不能直接把它列為漏洞。Agent 必須指出 Email 從哪裡流向哪裡,以及誰能取得。
個人資料不只會從資料庫權限外洩。API Response、錯誤訊息與 Log 都可能成為出口。
今天建立了三項規則:
「後台顯示 Gmail」是不是漏洞,仍要根據使用者權限、業務必要性與資料流判斷。
Production Readiness Agent 的工作不是看到 Email 就發出警報。它要找出資料來源、出口、接收者與實際影響。
明天,我們會檢查另一種常見外洩:開發者把 Secret 放進 Repository 或前端 JavaScript。