fix(sec): 安全评审加固——goroutine panic 恢复 + 下单 body 上限 + 错误脱敏
- 后台 goroutine (notifyBiz/SyncPending) 加 defer recover,防 panic 拖垮进程 - 下单请求体 64KB 上限 (MaxBytesReader),防超大 body 内存 DoS - 下单错误不再回传原始 err.Error(),内部详情只进日志 - (另: pay nginx 加 per-IP 限流 limit_req,服务器侧配置) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UQmqWmV67sXGLrb3U1XXn
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -17,6 +18,9 @@ import (
|
||||
"github.com/wangjia/pay/internal/util"
|
||||
)
|
||||
|
||||
// maxOrderBodyBytes 下单请求体上限(防超大 body 内存 DoS)。
|
||||
const maxOrderBodyBytes = 64 << 10 // 64KB
|
||||
|
||||
// isMobileUA 粗略判断是否手机浏览器(决定支付宝走 wap.pay 拉起 App 还是 page.pay 扫码)。
|
||||
func isMobileUA(ua string) bool {
|
||||
ua = strings.ToLower(ua)
|
||||
@@ -75,14 +79,14 @@ func verifyBizSign(c *gin.Context, system string, rawBody []byte) error {
|
||||
// 独立收款(浏览器 /paytest):只传 product_id,无需签名。
|
||||
// 业务对接(jiu 后端):带 biz_system/biz_ref + HMAC 签名头,服务端校验后受理。
|
||||
func (h *OrderHandler) Create(c *gin.Context) {
|
||||
raw, err := io.ReadAll(c.Request.Body)
|
||||
raw, err := io.ReadAll(http.MaxBytesReader(c.Writer, c.Request.Body, maxOrderBodyBytes))
|
||||
if err != nil {
|
||||
util.RespondError(c, http.StatusBadRequest, "bad_request", "读取请求失败")
|
||||
util.RespondError(c, http.StatusBadRequest, "bad_request", "请求体过大或读取失败")
|
||||
return
|
||||
}
|
||||
var req createOrderRequest
|
||||
if err := json.Unmarshal(raw, &req); err != nil {
|
||||
util.RespondError(c, http.StatusBadRequest, "bad_request", "参数错误:"+err.Error())
|
||||
util.RespondError(c, http.StatusBadRequest, "bad_request", "参数格式错误")
|
||||
return
|
||||
}
|
||||
if req.ProductID == 0 {
|
||||
@@ -103,7 +107,9 @@ func (h *OrderHandler) Create(c *gin.Context) {
|
||||
util.RespondError(c, http.StatusNotFound, "product_not_found", err.Error())
|
||||
return
|
||||
}
|
||||
util.RespondError(c, http.StatusInternalServerError, "create_failed", err.Error())
|
||||
// 内部错误详情只进日志,不回给客户端(防信息泄露)。
|
||||
log.Printf("[order] 下单失败 product_id=%d biz=%q: %v", req.ProductID, req.BizSystem, err)
|
||||
util.RespondError(c, http.StatusInternalServerError, "create_failed", "下单失败,请稍后重试")
|
||||
return
|
||||
}
|
||||
util.RespondSuccess(c, gin.H{
|
||||
@@ -127,7 +133,8 @@ func (h *OrderHandler) CreateQR(c *gin.Context) {
|
||||
util.RespondError(c, http.StatusNotFound, "product_not_found", err.Error())
|
||||
return
|
||||
}
|
||||
util.RespondError(c, http.StatusInternalServerError, "create_failed", err.Error())
|
||||
log.Printf("[order] 扫码下单失败 product_id=%d: %v", req.ProductID, err)
|
||||
util.RespondError(c, http.StatusInternalServerError, "create_failed", "下单失败,请稍后重试")
|
||||
return
|
||||
}
|
||||
util.RespondSuccess(c, gin.H{
|
||||
|
||||
Reference in New Issue
Block a user