iT邦幫忙

2026 iThome 鐵人賽

DAY 8
0
Build on Google AI

從 Vibe Coding 到 Production:用 Google AI 打造上線守門員系列 第 8

Day 8|API、錯誤訊息與 Log:個人資料是如何不小心外洩的?

  • 分享至 

  • xImage
  •  

前幾天,我一直提到一個問題:後台顯示使用者的 Email,算不算資安漏洞?

答案不是單純的「算」或「不算」。

我們要先確認四件事:

  1. 這個畫面或 API 是否需要 Email。
  2. 哪些使用者可以取得 Email。
  3. 系統是否把 Email 傳到其他地方。
  4. 團隊如何保存、保護與刪除這些資料。

Email 出現在只有授權客服能使用的帳號查詢畫面,可能符合業務需求。Email 如果出現在公開 API、一般使用者的回應或所有工程師都能查看的 Log,風險就完全不同。

今天會建立一個本機 API,觀察個人資料如何從三個出口流出:

  • API Response
  • Error Response
  • Application Log

資料存在,不等於資料應該被傳出去

後端常把資料庫物件直接轉成 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 不會自動把新欄位一起傳給使用者。

建立 Day 8 的本機情境

今天的程式放在:

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

比較 API Response

不安全版本回傳:

{
  "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 都成功回應,但暴露的資料量不同。

不安全版本有三項問題:

  1. 回傳功能不需要的 Email 與備援 Email。
  2. 回傳內部使用的風險註記。
  3. 資料庫以後新增欄位時,Endpoint 可能自動暴露新欄位。

這裡的修正不是在前端隱藏欄位。資料一旦進入 HTTP Response,就已經交給使用者。

錯誤訊息為什麼也會洩漏資料?

不安全版本捕捉錯誤後,把 error.messageerror.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。

這個方法同時達成兩個目標:

  • Client 不會取得內部錯誤細節。
  • 團隊仍能追蹤同一次請求。

Log 不是私人垃圾桶

開發者遇到難以重現的錯誤時,常直接記錄整個 Request:

logger.error("UNSAFE_LOG", {
  headers: request.headers,
  body,
  stack: error.stack
});

這段程式會把 Headers、Request Body 與 Stack Trace 全部交給 Logger。

Headers 可能包含:

  • Authorization Token
  • Cookie
  • Session ID
  • 內部追蹤資訊

Request Body 可能包含:

  • Email
  • 電話
  • 地址
  • 使用者輸入內容
  • 付款或身分資料

Log 通常會集中保存,也可能被更多工程師、維運工具或外部服務存取。資料進入 Log 後,刪除與追蹤會比刪除資料庫欄位更困難。

OWASP Logging Cheat Sheet 建議不要直接記錄 Access Token、Session ID、密碼、連線字串、加密金鑰及敏感個人資料。Email 等資料也可能需要刪除、遮罩或去識別化。

安全 Log 要留下什麼?

安全版本只記錄處理事件所需的欄位:

{
  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。

為什麼測試中的 Authorization 顯示成星號?

測試送出以下 Header:

Authorization: Bearer demo-token-not-a-real-secret

Terminal 輸出把值顯示成 ******。這是執行環境提供的顯示遮罩,不是我們的應用程式完成了資料清理。

不安全程式仍執行:

headers: request.headers

也就是說,應用程式把完整 Headers 交給 Logger。正式環境使用的 Logging Library、傳輸工具與儲存平台未必會自動遮罩。

我們不能看到星號就判定程式安全。真正的修正是不要把 Authorization Header 傳給 Logger。

Production Readiness Agent 要如何檢查資料外洩?

Agent 不能只搜尋 emailpasswordtoken。同一個欄位在不同情境中可能有不同用途。

Agent 應沿著資料流回答五個問題:

  1. 來源:資料來自使用者、資料庫、Header,還是外部服務?
  2. 出口:資料進入 API Response、Log、錯誤訊息,還是第三方服務?
  3. 接收者:一般使用者、管理員、工程師或外部廠商能否取得資料?
  4. 必要性:目前功能是否需要這個欄位?
  5. 保護措施:系統是否執行授權、遮罩、加密、保存期限與存取限制?

以下 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 都可能成為出口。

今天建立了三項規則:

  1. API 只回傳目前功能需要的欄位。
  2. Client Error 只提供固定代碼與 Request ID。
  3. Log 記錄事件與排錯資訊,不記錄完整 Request。

「後台顯示 Gmail」是不是漏洞,仍要根據使用者權限、業務必要性與資料流判斷。

Production Readiness Agent 的工作不是看到 Email 就發出警報。它要找出資料來源、出口、接收者與實際影響。

明天,我們會檢查另一種常見外洩:開發者把 Secret 放進 Repository 或前端 JavaScript。

參考資料


上一篇
Day 7|認證不等於授權:從登入功能找出越權風險
下一篇
Day 9|別把 Secret 推上 GitHub:正式環境的密鑰管理入門
系列文
從 Vibe Coding 到 Production:用 Google AI 打造上線守門員10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言