Skip to content

安全

本章彙整散落在認證沙盒錯誤回調各章的整合安全指引。當你準備上線、執行安全審查,或稽核人員詢問 Payment Gateway 在傳輸層提供哪些保證、你這端又該做什麼時,請閱讀本章。安全是共同責任模型:Payment Gateway 強化平台、你強化整合,任一方獨自都不足夠。

威脅模型

下表列出商戶 API 介面所要抵禦的威脅,以及每項緩解措施的責任歸屬。「雙方」代表兩側都存在控制機制,缺一不可。

威脅緩解措施責任歸屬
對入站請求的重放攻擊伺服器端強制 X-Timestamp ±5 分鐘視窗;單靠簽章無法防止重放。Payment Gateway
對入站回調的重放攻擊你的回調處理器套用相同的 ±5 分鐘視窗;並搭配 (order_no, status) 冪等。商戶
對入站請求的主體竄改每筆請求都必須帶 X-Signatureverify_md5_signature 會在任何業務邏輯之前執行。Payment Gateway
對入站回調的主體竄改你的回調處理器在套用任何副作用之前先驗證 X-Signature商戶
對 API 的 IP 偽造來源 IP 由 Cloudflare 邊緣(WAF / Access)統一管控。Payment Gateway
憑證外洩(api_key可依需求提供輪替程序;你需把金鑰存在 Secret Manager(密碼管理工具)中並在日誌中遮罩。雙方
DNS 污染/MITM 攻擊端對端強制 TLS 1.2+;閘道在正式環境中拒絕 HTTP 回調 URL 與商戶端自簽憑證。雙方
暴力破解憑證猜測Cloudflare 在邊緣對 API 主機名稱套用速率限制;你也應對自身的認證失敗做速率限制。雙方
日誌檔遭竊導致憑證外洩下方記錄遮罩規範;由你在日誌器中實作。商戶
並發重放與你的處理器搶跑(order_no, status) 做冪等鍵;在套用副作用前先去重。商戶

清單上沒有任何一項是純粹閘道的責任。即使由 Payment Gateway 負責的控制項,商戶端也有對應的責任不可省略。

憑證生命週期

發放

你的 api_key 在開通階段由客戶經理配發。你會拿到兩把金鑰:一把沙盒、一把正式環境。兩者彼此獨立——沙盒金鑰無法用於正式環境,反之亦然。金鑰是由平台端透過 secrets.token_hex(16) 產生的 32 字元 hex token,並透過外部管道交付。

輪替觸發時機

下列任一情況發生時立即輪替:

  • 金鑰已知或疑似外洩(提交到原始碼控制、貼進工單、以明文記錄、透過不受信任管道傳送)。
  • 持有金鑰存取權限的員工離職或調任至無需知情的職位。
  • 持有金鑰的供應商或整合商結束合作。
  • 排程輪替週期到期。建議最低週期為每季一次。

如何輪替

輪替由維運人員協作。聯絡你的客戶經理。流程如下:

  1. 客戶經理在你的商戶記錄上新增第二把有效金鑰。
  2. 你部署新金鑰並與舊金鑰並存,驗證流量正常。
  3. 客戶經理在短暫的重疊視窗(約 5 分鐘)後撤銷舊金鑰,期間兩把金鑰皆可接受,允許熱切換而不中斷請求。

目前商戶後台不提供自助輪替。請在你的事件回應手冊中為維運人員交接預留流程。

儲存

api_key 等同於長期使用的密碼憑證。請依此規格儲存:

  • 使用 Secret Manager(密碼管理工具):AWS Secrets Manager、GCP Secret Manager、HashiCorp Vault 或同等方案。
  • 在執行時從 Secret Manager 注入應用程式的程序環境或記憶體。
  • 絕對不要提交到原始碼控制,即使加密過也不行。Secret Manager 端的靜態加密不等同於「在 repo 中加密」——後者可從任何歷史 clone 還原。
  • 絕對不要烘焙進容器映像;映像層會洩漏。
  • 絕對不要包進客戶端程式碼、行動 App 或任何送到瀏覽器的東西。商戶 API 介面僅限伺服器對伺服器。

團隊內部分發

把正式環境 api_key 的知情範圍限制在維運與待命工程師。在平台支援的情境下使用即時授權。稽核每一次對該機密的直接讀取。將「誰知道正式環境金鑰」清單視為需要與輪替同樣週期審視的項目。

傳輸

出站(你的服務 → Payment Gateway)

所有商戶 API 主機名稱只接受 HTTPS。閘道在邊緣終止 TLS 並拒絕明文 HTTP。可接受的 TLS 最低版本為 TLS 1.2;TLS 1.0、TLS 1.1 與 SSLv3 不會被協商。將你的 client 鎖定為 TLS 1.2 或以上;不要設定回退到較舊套件。

入站(Payment Gateway → 你的回調 URL)

  • 正式環境強制 HTTPS。callback_url 若不以 https:// 開頭,會在建單時被拒。沒有 opt-out 選項。
  • **必須提供有效的憑證鏈。**閘道會以公開 CA bundle 驗證你的 TLS 憑證。自簽憑證會被拒絕(webhook_ssl_verify = True 為預設)。請使用任何公開 CA 簽發的真實憑證。
  • **你端也要 TLS 1.2+。**停用回調端點的 TLS 1.0 與 1.1。閘道不會與其協商。

加密套件與現代安全規範

停用 export 加密、RC4、3DES 與任何標記為「EXPORT」或「anonymous」的套件。啟用前向安全(ECDHE)。任何當代 TLS 函式庫的現代預設值都是正確的。

網路層存取控制

商戶 API 的入站請求會在 Cloudflare 邊緣(WAF / Access)通過來源 IP 允許清單。完整模型與其失敗模式請見沙盒。正式環境同樣套用此邊緣管控。把自己從 Cloudflare Access 允許清單移除,是「昨天還能用」工單最常見的單一原因。

請求完整性

簽章演算法

每筆入站商戶請求與每筆出站回調都會以 MD5(sorted_params + api_key) 簽章,輸出為小寫 hex。完整的逐步演算法與四種參考實作見認證與簽章。本章不再重新推導。

此處選用 MD5 反映支付通道的業界慣例;它被當作搭配共享密鑰的 MAC 風格完整性檢查使用,而不是抗碰撞雜湊。由於 api_key 只串接但從不在傳輸中送出,即使攻擊者擁有任意 MD5 碰撞能力,沒有金鑰也無法偽造簽章。

未來可能會宣布遷移到 HMAC-SHA256。屆時會在變更紀錄中公告,附穩定的過渡計畫、並行接受視窗與棄用日期。不要預先重新實作;契約以本文件所載為準。

時間戳視窗

X-Timestamp 為 Unix epoch 秒數。伺服器端視窗為閘道時鐘的 ±5 分鐘(300 秒),在簽章驗證之前強制執行。超出視窗則回應 401 INVALID_TIMESTAMP(見錯誤)。

在你的回調處理器中鏡像這項檢查。閘道在每次派送與重試時都會送出新的 X-Timestamp;拒絕任何時間戳與你的時鐘相差超過 5 分鐘的回調。把你的時鐘同步到 NTP——不要靠信任閘道送來的時間戳來「修正」時鐘漂移。

常數時間比較

驗證入站回調簽章時,使用常數時間相等函式:

  • Python:hmac.compare_digest(expected, received)
  • PHP:hash_equals($expected, $received)
  • Node.js:crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))
  • Go:subtle.ConstantTimeCompare([]byte(expected), []byte(received)) == 1

一般字串比較(==)會透過時間變化洩漏首個不同位元組的位置。樣本累積足夠後,攻擊者可逐位元組推回偽造簽章。閘道在伺服器端使用 hmac.compare_digest;你這端也應採用相同方式。認證章節隨附的 verify 範例在各語言中皆使用正確原語。

時間戳視窗內的重放

有效簽章加新鮮的時間戳,本身並不能防止 5 分鐘視窗內的重放。攔截到回調的網路攻擊者可在 5 分鐘內重放。能擊敗此問題的控制是商戶端(order_no, status) 做冪等——在下一節說明。簽章加時間戳視窗縮小了重放機會;冪等則徹底化解。

把冪等當成安全屬性

合法網路重試所產生的重複回調(閘道對同一筆 NotificationLog 列最多再派送 5 次)與惡意重放嘗試,在應用層是無法區分的。兩者送達的都是相同主體上的合法簽章。兩者皆由同一控制處理:在套用任何副作用之前,以 (order_no, status) 去重。

這就是為什麼回調建議在 (order_no, status) 上使用主鍵資料表,並採取 insert-or-no-op 的處理器。從安全角度看,那張資料表是你的重放防禦層,不只是重試正確性層。

為什麼不用其他鍵:

  • notification_id——回調主體中並未提供 per-attempt 事件 id。閘道在所有重試中重用同一筆 NotificationLog 列。
  • X-Timestamp——每次重試都會變動,因此以它去重會讓同一事件被合法套用 5 次(合法情境)以及任意次數(惡意情境)。
  • (order_no, status)——跨重試穩定,跨合法狀態轉移又有區別(例如 MATCHEDCOMPLETED 兩者都是真實事件、都該處理)。

日誌規範

當機密以明文記錄時,洩漏的日誌檔就等同於洩漏的憑證。下列紀律請在日誌器層落實——而不是呼叫端,否則會被跳過。

該記錄的(搭配遮罩)

  • HTTP method、請求路徑、回應狀態。
  • 回應信封中的 code(見錯誤)。
  • 你自己的 merchant_order_no 與閘道的 order_no
  • 回調中的 notification_type / event 欄位。
  • 簽章驗證結果的布林值(passfail)。
  • 你內部的 trace ID 或 request ID 以利關聯。
  • X-Timestamp 值以及你觀測到相對於本機時鐘的偏移。

不該記錄的(等同憑證或屬於 PII)

  • **完整的 api_key。**只記錄前 8 個字元加省略號:pre_xxxxxxxx...。絕不記錄末尾位元組;絕不把整串接到 query string 裡。
  • **完整的 X-Signature 值。**只記錄驗證通過或失敗。Hex 值沒有除錯價值——若它是錯的,你也無法從日誌反推應有的值。
  • **完整的 bank_card_number。**遮蔽除末 4 碼之外的所有位元組。
  • **完整的 account_no。**遮蔽除末 4 碼之外的所有位元組。
  • **PII 組合。**收款人姓名 + 銀行 + 帳號合起來就能對應到某銀行的真實人員。請遮罩、省略,或在獨立存取控制下加密儲存。
  • **簽章驗證前的原始請求主體。**尚未驗證的主體屬攻擊者可控內容。只在 X-Signature 通過後,並套用上述遮罩,才記錄它。

日誌的存放位置

  • 靜態加密。多數受管理日誌儲存預設會這麼做——確認你的也是。
  • 對日誌儲存實施存取控制:讀取權是特權角色,而非開發者預設。
  • 保留期:合理預設為 熱存 90 天、冷存 1 年。依你所在的法規環境調整。
  • 備份沿用同樣的控制。洩漏的備份就是洩漏的日誌。

SIEM 整合

將下列項目作為安全事件轉發到你的 SIEM:

  • 來自閘道的 401 回應(AUTHENTICATION_ERRORINVALID_SIGNATUREINVALID_TIMESTAMP)。
  • 來自閘道的 403 回應(AUTHORIZATION_ERROR)。
  • 你的回調處理器上的簽章驗證失敗。
  • 任何持續對上述項目產生大量爆發的來源 IP。

在一段乾淨期間後出現 401 模式通常代表時鐘漂移或部分金鑰輪替。403 模式通常代表商戶停用或缺少權限;來源 IP 漂移則改在 Cloudflare 邊緣顯現(TLS 握手失敗或 HTML 挑戰/封鎖頁,而非 JSON 403)。兩者都是維運上的緊急事件。

常見陷阱

下列錯誤皆已在真實商戶整合中上到正式環境。每一項都附帶你會看到的錯誤代碼或症狀。

  1. **在任何地方記錄完整 api_key。**Debug 日誌、例外回報器、應用程式指標、錯誤追蹤 SaaS 的 payload。只記錄前 8 個字元加省略號。任何日誌列中出現的 api_key 子字串都視為憑證外洩並啟動輪替。
  2. **記錄 X-Signature 值。**該值沒有除錯用途。只記錄布林 pass/fail。被記錄的簽章配上被記錄的主體,會洩漏 chosen-message 攻擊的輸入。
  3. **正式環境使用 HTTP 的 callback_url。**建單時即被拒。你會看到 400 VALIDATION_ERROR 或相當訊息(見錯誤)。請使用 HTTPS。
  4. **正式環境的回調端點使用自簽憑證。**閘道會拒絕連線(webhook_ssl_verify = True)。請使用公開 CA。
  5. **跨沙盒與正式環境重用同一把 api_key。**設計上不可能(沙盒金鑰會被正式環境拒絕、反之亦然),但若重用處理模式——例如同一個 Secret Manager 條目、同一個日誌器、同一條事件路徑——就會把兩個環境耦合。請從頭到尾保持分離。
  6. **把 api_key 存在原始碼控制裡。**即使加密、即使用 git-crypt、即使在私有 repo。加密層在原始碼控制層之下游;歷史 clone 會跨越輪替倖存。
  7. **跳過「內部」回調的簽章驗證。**沙盒回調由閘道以與正式環境相同的演算法簽署。如果你在沙盒跳過驗證,會把那段程式碼路徑送到正式環境。兩邊都要驗證。見回調
  8. message 文字而非 code 做分支判斷。message 為資訊性質、措辭可能變動。code 才是穩定契約。見錯誤
  9. **在 client 端把 5 分鐘時間戳視窗寫死。**該視窗是伺服器端政策。若閘道把它收緊,以 now() - 4m59s 簽章的 client 會開始失敗。用 now() 簽章,把視窗判斷交給伺服器。
  10. **驗證簽章時忘記使用常數時間比較。**字串相等會洩漏。依語言使用 hmac.compare_digesthash_equalscrypto.timingSafeEqual
  11. **Cloudflare Access 允許清單漂移。**你的 NAT 出口 IP 變動(新資料中心、新 CI runner pool、雲端供應商 IP 刷新)但 Cloudflare Access 允許清單沒跟上。由於來源 IP 只在 Cloudflare 邊緣管控,被擋的請求不會回傳 JSON 信封——你會看到 TLS 握手失敗或 Cloudflare 的 HTML 挑戰/封鎖頁,而非帶 AUTHORIZATION_ERROR403。請把 NAT pool 範圍同步到 Cloudflare Access 允許清單。見沙盒——正式環境套用相同模型。
  12. **在回調持久化層以未加密形式儲存 PII(bank_card_numberaccount_no、收款人姓名)。**靜態加密不等同於資料庫欄位本身的加密。如果你的維運人員或唯讀副本能看到明文,你的稽核姿態就錯了。
  13. 未驗證鏈路就信任 CDN 邊緣標頭。X-Forwarded-For 會被鏈路中每個 proxy 附加。清單中第一個 IP 是直接 client 寫的;只有最右側、由你自己的可信 proxy 附加的 IP 才有權威性。請以可信跳數計算來驗證,而不是信任最左邊的值。
  14. **持有金鑰存取權的員工離職後忘記輪替 api_key。**輪替是離職流程的一部分,不是季度後事。離職手冊必須含此步驟。
  15. **捕捉簽章驗證例外後繼續執行。**在 verify 呼叫外套 try/except 而吞掉失敗、繼續處理主體,就是權限提升攻擊向量。回 401 並停止。見回調
  16. **在驗證前記錄原始請求主體。**攻擊者可控的內容會落入你的日誌儲存,包含可能與日誌遮罩規則模式匹配的任意 JSON key,讓機密溜走。先驗證,再記錄。

上線安全檢查清單

把正式環境部署切到 api.open-pay.co 之前,逐項確認:

  • [ ] 回調 URL 為 HTTPS 且憑證鏈可被公開 CA 驗證。
  • [ ] api_key 存放在 Secret Manager 中,不在 repo 的 env 檔、不在映像層、不在客戶端程式碼。
  • [ ] **每個回調處理器的請求路徑上都有簽章驗證。**不藏在 feature flag 後面、不在「內部」環境略過、不會在帶 debug 標頭時被繞過。
  • [ ] (order_no, status) 為鍵的冪等資料表或快取已部署,並以重複回調情境測試過。
  • [ ] **日誌遮罩已測試。**用 grep 在 staging 日誌中搜尋 api_key=pre_X-Signature: 與其他憑證樣式。grep 應回傳零筆。
  • [ ] Cloudflare Access 允許清單與你的正式環境 NAT 出口 IP 對齊。有文件、有負責人、每季審視。
  • [ ] SIEM 收到認證失敗事件:來自閘道的 401/403,以及你處理器上的簽章驗證失敗。
  • [ ] **金鑰輪替週期已與客戶經理協定。**最低每季一次。輪替手冊已寫成,並由作者以外的人讀過。
  • [ ] **事件回應手冊涵蓋 api_key 外洩情境。**該打給誰、要求什麼、重疊視窗多長、如何確認舊金鑰已失效。
  • [ ] **沙盒與正式環境憑證在部署設定中明確分離。**不同的 Secret Manager 路徑、不同的環境變數、不同的日誌前綴。沒有在正式環境未設時 fall-through 到沙盒的預設值。
  • [ ] **每台簽署請求或驗證回調的主機都設定了時鐘同步(NTP)。**漂移超過 5 分鐘是硬性失敗。
  • [ ] **雙向皆強制 TLS 1.2+。**不回退至 1.0/1.1。

交叉參考

  • 認證與簽章——完整簽章演算法、5 分鐘時間戳視窗,以及四種語言的參考實作。
  • 沙盒——Cloudflare 邊緣來源 IP 允許清單模型,以及如何辨識請求是否被邊緣拒絕。
  • 錯誤——安全相關錯誤代碼(401 INVALID_SIGNATURE401 INVALID_TIMESTAMP403 AUTHORIZATION_ERROR)與回應信封契約。
  • 回調——回調簽章驗證、以 (order_no, status) 做冪等,以及重試政策。
  • 快速開始——從第一筆請求到第一筆驗證通過的回調,15 分鐘上手。
  • API 參考——完整端點清單、請求與回應 schema。