簽章驗證與安全性
Webhook
簽章驗證
HMAC
安全性
重放攻擊
本文的對象
適用於實作 Webhook 端點的開發者。說明確認送來的請求是否真的由 ReceiptRoller 發出的簽章驗證方法,與安全性上的注意點。
適用於實作 Webhook 端點的開發者。說明確認送來的請求是否真的由 ReceiptRoller 發出的簽章驗證方法,與安全性上的注意點。
Webhook 端點是公開 URL,因此第三方原理上可能送入假的 POST。為防止此事,ReceiptRoller 為所有 Webhook 請求附帶 HMAC-SHA256 基底的簽章。接收端請務必驗證簽章後再處理。沒驗證的端點對攻擊而言毫無防備。
簽章的機制
ReceiptRoller 在發送前以下列步驟計算簽章。
- 製作簽章對象字串:
{timestamp}.{request_body} - 以 Webhook Secret 為鍵計算 HMAC-SHA256
- 將結果 hex 編碼
- 附加至 HTTP 標頭發送
發送的相關標頭
| 標頭名 | 內容 |
|---|---|
X-RR-Signature |
HMAC-SHA256 的 hex 字串(64 字) |
X-RR-Timestamp |
投放時刻(Unix 秒) |
X-RR-Event-Id |
事件 ID(冪等性鍵) |
X-RR-Signature-Version |
簽章版本(現行為 v1) |
接收端的驗證步驟
- 取出
X-RR-Timestamp與原始請求主體(不是解析後的物件,而是接收到的位元組序列本身) - 組立簽章對象字串
{timestamp}.{body} - 以保管的 Webhook Secret 計算 HMAC-SHA256 並 hex 編碼
- 將計算結果與
X-RR-Signature以定時比較照合(計時攻擊對策) - 確認時間戳在現在時刻的 ±5 分以內(重放攻擊對策)
範例程式碼
Node.js(Express)
const crypto = require("crypto");
const express = require("express");
const app = express();
const SECRET = process.env.RR_WEBHOOK_SECRET;
const TOLERANCE_SEC = 300; // 5分
// 重要:為保持原始主體而使用 express.raw
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const sig = req.header("X-RR-Signature");
const ts = req.header("X-RR-Timestamp");
const body = req.body; // Buffer
// 時間戳驗證
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(ts, 10)) > TOLERANCE_SEC) {
return res.status(401).send("timestamp out of tolerance");
}
// 簽章驗證
const expected = crypto
.createHmac("sha256", SECRET)
.update(`${ts}.${body.toString("utf8")}`)
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.status(401).send("invalid signature");
}
// 驗證OK:處理事件
const event = JSON.parse(body.toString("utf8"));
// ... 冪等性檢查、業務處理 ...
res.status(200).send("ok");
});
Python(Flask)
import hmac, hashlib, time, os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["RR_WEBHOOK_SECRET"].encode()
TOLERANCE_SEC = 300
@app.route("/webhook", methods=["POST"])
def webhook():
sig = request.headers.get("X-RR-Signature", "")
ts = request.headers.get("X-RR-Timestamp", "")
body = request.get_data() # 原始位元組序列
# 時間戳驗證
if abs(int(time.time()) - int(ts)) > TOLERANCE_SEC:
abort(401)
# 簽章驗證
payload = f"{ts}.".encode() + body
expected = hmac.new(SECRET, payload, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected):
abort(401)
# 驗證OK:處理事件
event = request.get_json()
# ... 冪等性檢查、業務處理 ...
return "ok", 200
C#(ASP.NET Core)
[HttpPost("/webhook")]
public async Task<IActionResult> Receive()
{
Request.EnableBuffering();
using var reader = new StreamReader(Request.Body);
var body = await reader.ReadToEndAsync();
var sig = Request.Headers["X-RR-Signature"].ToString();
var ts = Request.Headers["X-RR-Timestamp"].ToString();
// 時間戳驗證
var nowSec = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
if (Math.Abs(nowSec - long.Parse(ts)) > 300) return Unauthorized();
// 簽章驗證
var secret = Encoding.UTF8.GetBytes(_config["RR_WEBHOOK_SECRET"]);
using var h = new HMACSHA256(secret);
var expected = Convert.ToHexString(
h.ComputeHash(Encoding.UTF8.GetBytes($"{ts}.{body}"))
).ToLowerInvariant();
if (!CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(sig),
Encoding.UTF8.GetBytes(expected)))
return Unauthorized();
// 驗證OK:處理事件
// ... 冪等性檢查、業務處理 ...
return Ok();
}
常見實作失誤
| 失誤 | 影響 | 因應 |
|---|---|---|
| 以 JSON 解析後的字串計算簽章 | 空白、換行改變每次都失敗 | 務必使用原始位元組序列 |
以通常的 == 比較 |
計時攻擊可能被推測簽章 | 使用定時比較函式 |
| 不驗證時間戳 | 過去的有效請求被重送(重放攻擊) | 實作 ±5 分的容許範圍 |
| 將密鑰直接寫入程式碼 | 儲存庫外洩時外洩 | 改用環境變數或密鑰管理服務 |
驗證失敗時回傳 2xx |
讓攻擊者誤認「接收成功」 | 回傳 401 |
密鑰的安全管理
- 保管場所:環境變數、AWS Secrets Manager、Azure Key Vault、GCP Secret Manager 等密鑰管理服務
- 存取控制:正式密鑰讓正式環境的服務帳戶才能讀的狀態
- 輪替:至少每年 1 次,懷疑外洩時即時。詳情請參照「密鑰的輪替與安全的保管」
- 環境分離:正式/預備/開發使用不同密鑰
- 禁止日誌輸出:不將密鑰或簽章值輸出至錯誤日誌或稽核日誌
追加的網路對策(任意)
簽章驗證即足夠,但想更嚴格運用的情況也可以檢討以下。
- IP 許可清單:將 ReceiptRoller 的發送來源 IP 範圍加入許可清單(IP 範圍可能會無預告變更,因此不作為簽章驗證的替代,而作為追加策)
- WAF:在端點的前段放置 WAF,遮斷不審的請求
- 速率限制:限制來自單一 IP 的過剩請求
相關指南
發布日: 2026-04-27
更新日: 2026-07-06
標籤
API (22)
OAuth (15)
Android (10)
iOS (9)
Webhook (8)
api (7)
oauth (5)
POS串接 (4)
getting-started (4)
參考 (4)
相關文章
-
重送、順序、冪等性的設計解說 ReceiptRoller Webhook 的重送政策、投放順序不保證的理由、使用 event_id 的冪等性實作、無效信件的處理、常見反模式。
-
SNS Webhook 旁路(LINE 等外部 SNS 的 Webhook 轉發)解說 ReceiptRoller 將從 LINE 等 SNS 平台接收到的 Webhook,在店鋪端同意下轉發至開發者應用程式的「SNS Webhook 旁路」功能的機制、設定方法、簽章的處理、注意事項。
-
監視與失敗時的對應解說 ReceiptRoller Webhook 的投放紀錄的檢視方式、應監視的指標與警報設計、無效信件的重新投放、常見故障模式與復原步驟。
-
Webhook 的概要解說 ReceiptRoller 的 Webhook 所投放的主要事件種類、應該使用 Webhook 而非輪詢的理由、投放形式(HTTPS POST + JSON)、投放保證的思考方式。
-
開發者向幫助目次ReceiptRoller 開發者向幫助目次。彙整了開發者申請、應用程式登錄、OAuth 驗證與權限範圍、實作指南(錢包應用程式、店鋪向 Webhook、Survey API)、依資料領域別指南、運用與安全性、社群、疑難排解。