安全
本章彙整散落在認證、沙盒、錯誤與回調各章的整合安全指引。當你準備上線、執行安全審查,或稽核人員詢問 Payment Gateway 在傳輸層提供哪些保證、你這端又該做什麼時,請閱讀本章。安全是共同責任模型:Payment Gateway 強化平台、你強化整合,任一方獨自都不足夠。
威脅模型
下表列出商戶 API 介面所要抵禦的威脅,以及每項緩解措施的責任歸屬。「雙方」代表兩側都存在控制機制,缺一不可。
| 威脅 | 緩解措施 | 責任歸屬 |
|---|---|---|
| 對入站請求的重放攻擊 | 伺服器端強制 X-Timestamp ±5 分鐘視窗;單靠簽章無法防止重放。 | Payment Gateway |
| 對入站回調的重放攻擊 | 你的回調處理器套用相同的 ±5 分鐘視窗;並搭配 (order_no, status) 冪等。 | 商戶 |
| 對入站請求的主體竄改 | 每筆請求都必須帶 X-Signature;verify_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,並透過外部管道交付。
輪替觸發時機
下列任一情況發生時立即輪替:
- 金鑰已知或疑似外洩(提交到原始碼控制、貼進工單、以明文記錄、透過不受信任管道傳送)。
- 持有金鑰存取權限的員工離職或調任至無需知情的職位。
- 持有金鑰的供應商或整合商結束合作。
- 排程輪替週期到期。建議最低週期為每季一次。
如何輪替
輪替由維運人員協作。聯絡你的客戶經理。流程如下:
- 客戶經理在你的商戶記錄上新增第二把有效金鑰。
- 你部署新金鑰並與舊金鑰並存,驗證流量正常。
- 客戶經理在短暫的重疊視窗(約 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)——跨重試穩定,跨合法狀態轉移又有區別(例如MATCHED後COMPLETED兩者都是真實事件、都該處理)。
日誌規範
當機密以明文記錄時,洩漏的日誌檔就等同於洩漏的憑證。下列紀律請在日誌器層落實——而不是呼叫端,否則會被跳過。
該記錄的(搭配遮罩)
- HTTP method、請求路徑、回應狀態。
- 回應信封中的
code(見錯誤)。 - 你自己的
merchant_order_no與閘道的order_no。 - 回調中的
notification_type/event欄位。 - 簽章驗證結果的布林值(
pass或fail)。 - 你內部的 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_ERROR、INVALID_SIGNATURE、INVALID_TIMESTAMP)。 - 來自閘道的
403回應(AUTHORIZATION_ERROR)。 - 你的回調處理器上的簽章驗證失敗。
- 任何持續對上述項目產生大量爆發的來源 IP。
在一段乾淨期間後出現 401 模式通常代表時鐘漂移或部分金鑰輪替。403 模式通常代表商戶停用或缺少權限;來源 IP 漂移則改在 Cloudflare 邊緣顯現(TLS 握手失敗或 HTML 挑戰/封鎖頁,而非 JSON 403)。兩者都是維運上的緊急事件。
常見陷阱
下列錯誤皆已在真實商戶整合中上到正式環境。每一項都附帶你會看到的錯誤代碼或症狀。
- **在任何地方記錄完整
api_key。**Debug 日誌、例外回報器、應用程式指標、錯誤追蹤 SaaS 的 payload。只記錄前 8 個字元加省略號。任何日誌列中出現的api_key子字串都視為憑證外洩並啟動輪替。 - **記錄
X-Signature值。**該值沒有除錯用途。只記錄布林 pass/fail。被記錄的簽章配上被記錄的主體,會洩漏 chosen-message 攻擊的輸入。 - **正式環境使用 HTTP 的
callback_url。**建單時即被拒。你會看到400 VALIDATION_ERROR或相當訊息(見錯誤)。請使用 HTTPS。 - **正式環境的回調端點使用自簽憑證。**閘道會拒絕連線(
webhook_ssl_verify = True)。請使用公開 CA。 - **跨沙盒與正式環境重用同一把
api_key。**設計上不可能(沙盒金鑰會被正式環境拒絕、反之亦然),但若重用處理模式——例如同一個 Secret Manager 條目、同一個日誌器、同一條事件路徑——就會把兩個環境耦合。請從頭到尾保持分離。 - **把
api_key存在原始碼控制裡。**即使加密、即使用git-crypt、即使在私有 repo。加密層在原始碼控制層之下游;歷史 clone 會跨越輪替倖存。 - **跳過「內部」回調的簽章驗證。**沙盒回調由閘道以與正式環境相同的演算法簽署。如果你在沙盒跳過驗證,會把那段程式碼路徑送到正式環境。兩邊都要驗證。見回調。
- 以
message文字而非code做分支判斷。message為資訊性質、措辭可能變動。code才是穩定契約。見錯誤。 - **在 client 端把 5 分鐘時間戳視窗寫死。**該視窗是伺服器端政策。若閘道把它收緊,以
now() - 4m59s簽章的 client 會開始失敗。用now()簽章,把視窗判斷交給伺服器。 - **驗證簽章時忘記使用常數時間比較。**字串相等會洩漏。依語言使用
hmac.compare_digest、hash_equals或crypto.timingSafeEqual。 - **Cloudflare Access 允許清單漂移。**你的 NAT 出口 IP 變動(新資料中心、新 CI runner pool、雲端供應商 IP 刷新)但 Cloudflare Access 允許清單沒跟上。由於來源 IP 只在 Cloudflare 邊緣管控,被擋的請求不會回傳 JSON 信封——你會看到 TLS 握手失敗或 Cloudflare 的 HTML 挑戰/封鎖頁,而非帶
AUTHORIZATION_ERROR的403。請把 NAT pool 範圍同步到 Cloudflare Access 允許清單。見沙盒——正式環境套用相同模型。 - **在回調持久化層以未加密形式儲存 PII(
bank_card_number、account_no、收款人姓名)。**靜態加密不等同於資料庫欄位本身的加密。如果你的維運人員或唯讀副本能看到明文,你的稽核姿態就錯了。 - 未驗證鏈路就信任 CDN 邊緣標頭。
X-Forwarded-For會被鏈路中每個 proxy 附加。清單中第一個 IP 是直接 client 寫的;只有最右側、由你自己的可信 proxy 附加的 IP 才有權威性。請以可信跳數計算來驗證,而不是信任最左邊的值。 - **持有金鑰存取權的員工離職後忘記輪替
api_key。**輪替是離職流程的一部分,不是季度後事。離職手冊必須含此步驟。 - **捕捉簽章驗證例外後繼續執行。**在 verify 呼叫外套 try/except 而吞掉失敗、繼續處理主體,就是權限提升攻擊向量。回
401並停止。見回調。 - **在驗證前記錄原始請求主體。**攻擊者可控的內容會落入你的日誌儲存,包含可能與日誌遮罩規則模式匹配的任意 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。