0.11.7: 代码审查查缺补漏(安全/并发/前端三轮审查修复)
安全: - 渠道 webhook 入站强制令牌鉴权(恒定时间比较+统一文案),企微签名官方算法; - 报表/概览/systemInfo 端点按 usage:read/audit:read/system:manage 授权; - sso_error 固定错误码;个人渠道令牌仅请求头;工具出站 Dialer.Control 消除 DNS rebinding TOCTOU;新增 channel:read/manage 权限;限流倍数上限 10。 并发/一致性: - 任务上报单条条件 UPDATE 防重放双提交;认领回收过期 claimed 任务; - 审批改先开通后落记录(幂等,无嵌套事务);聊天消息单事务落库; - 会话列表校验 AuthVersion;吊销先 Del 后 SRem;删工具保护调用历史; - rejected 冷却 24h;限流被拒补偿;maintenance 清理限流窗口。 前端/菜单: - 修复 gatewayChildren late-append 导致 reports/tenants/channels 菜单不可见; - 聊天改名 PUT 对齐;渠道编辑清空凭据防串写+启用开关; - 聊天响应防串扰;报表本地时区日期。
This commit is contained in:
+26
-13
@@ -2,7 +2,9 @@ package channel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -46,7 +48,7 @@ func (h *HTTPHandler) require(w http.ResponseWriter, r *http.Request, permission
|
||||
}
|
||||
|
||||
func (h *HTTPHandler) list(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := h.require(w, r, identity.PermissionNotificationRead); !ok {
|
||||
if _, ok := h.require(w, r, identity.PermissionChannelRead); !ok {
|
||||
return
|
||||
}
|
||||
items, err := h.service.List(r.Context())
|
||||
@@ -69,7 +71,7 @@ type channelInput struct {
|
||||
}
|
||||
|
||||
func (h *HTTPHandler) save(w http.ResponseWriter, r *http.Request) {
|
||||
actor, ok := h.require(w, r, identity.PermissionNotificationManage)
|
||||
actor, ok := h.require(w, r, identity.PermissionChannelManage)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -100,7 +102,7 @@ func (h *HTTPHandler) save(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (h *HTTPHandler) delete(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := h.require(w, r, identity.PermissionNotificationManage); !ok {
|
||||
if _, ok := h.require(w, r, identity.PermissionChannelManage); !ok {
|
||||
return
|
||||
}
|
||||
if err := h.service.Delete(r.Context(), r.PathValue("id")); err != nil {
|
||||
@@ -112,7 +114,7 @@ func (h *HTTPHandler) delete(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// test 用渠道绑定模型发送一条测试消息并尝试平台回复。
|
||||
func (h *HTTPHandler) test(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := h.require(w, r, identity.PermissionNotificationManage); !ok {
|
||||
if _, ok := h.require(w, r, identity.PermissionChannelManage); !ok {
|
||||
return
|
||||
}
|
||||
items, err := h.service.List(r.Context())
|
||||
@@ -160,7 +162,8 @@ func (h *InboundHTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *InboundHTTPHandler) inbound(w http.ResponseWriter, r *http.Request) {
|
||||
c, err := h.service.GetByCode(r.Context(), strings.ToLower(r.PathValue("code")))
|
||||
if err != nil {
|
||||
apiresponse.Error(w, http.StatusNotFound, "渠道不存在或未启用")
|
||||
// 与令牌无效返回同一错误,不泄露渠道存在性。
|
||||
apiresponse.Error(w, http.StatusUnauthorized, "渠道令牌无效或渠道不存在")
|
||||
return
|
||||
}
|
||||
cfg, err := h.service.DecryptConfig(c)
|
||||
@@ -180,8 +183,19 @@ func (h *InboundHTTPHandler) inbound(w http.ResponseWriter, r *http.Request) {
|
||||
apiresponse.Error(w, http.StatusUnauthorized, "签名校验失败")
|
||||
return
|
||||
}
|
||||
case "dingtalk":
|
||||
// 钉钉机器人验签由平台侧 access_token 控制;此处信任令牌。
|
||||
case "dingtalk", "feishu":
|
||||
// 钉钉/飞书机器人验签由平台侧 access_token/回调令牌控制;此处信任平台。
|
||||
case "webhook":
|
||||
// 通用 webhook 必须携带入站令牌:未配置令牌的渠道拒绝入站,防止
|
||||
// 任意调用者消耗绑定模型的配额与费用。
|
||||
presented := strings.TrimSpace(r.Header.Get("X-Inbound-Token"))
|
||||
if presented == "" {
|
||||
presented = strings.TrimSpace(r.URL.Query().Get("token"))
|
||||
}
|
||||
if cfg.InboundToken == "" || subtle.ConstantTimeCompare([]byte(presented), []byte(cfg.InboundToken)) != 1 {
|
||||
apiresponse.Error(w, http.StatusUnauthorized, "渠道令牌无效或渠道不存在")
|
||||
return
|
||||
}
|
||||
}
|
||||
var payload struct {
|
||||
Text struct {
|
||||
@@ -189,9 +203,8 @@ func (h *InboundHTTPHandler) inbound(w http.ResponseWriter, r *http.Request) {
|
||||
} `json:"text"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
raw := make([]byte, 1<<20)
|
||||
n, _ := r.Body.Read(raw)
|
||||
_ = json.Unmarshal(raw[:n], &payload)
|
||||
raw, _ := io.ReadAll(io.LimitReader(r.Body, 1<<20))
|
||||
_ = json.Unmarshal(raw, &payload)
|
||||
text := payload.Text.Content
|
||||
if text == "" {
|
||||
text = payload.Content
|
||||
@@ -219,7 +232,7 @@ func (h *InboundHTTPHandler) inbound(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// listGrants 渠道用户授权列表。
|
||||
func (h *HTTPHandler) listGrants(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := h.require(w, r, identity.PermissionNotificationRead); !ok {
|
||||
if _, ok := h.require(w, r, identity.PermissionChannelRead); !ok {
|
||||
return
|
||||
}
|
||||
items, err := h.service.ListGrants(r.Context(), r.PathValue("id"))
|
||||
@@ -232,7 +245,7 @@ func (h *HTTPHandler) listGrants(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// grant 直接授予用户渠道使用权限(管理员显式授权,无需走申请流)。
|
||||
func (h *HTTPHandler) grant(w http.ResponseWriter, r *http.Request) {
|
||||
actor, ok := h.require(w, r, identity.PermissionNotificationManage)
|
||||
actor, ok := h.require(w, r, identity.PermissionChannelManage)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -254,7 +267,7 @@ func (h *HTTPHandler) grant(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// revokeGrant 撤销用户的渠道使用权限。
|
||||
func (h *HTTPHandler) revokeGrant(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := h.require(w, r, identity.PermissionNotificationManage); !ok {
|
||||
if _, ok := h.require(w, r, identity.PermissionChannelManage); !ok {
|
||||
return
|
||||
}
|
||||
if err := h.service.RevokeGrant(r.Context(), r.PathValue("id"), r.PathValue("user_id")); err != nil {
|
||||
|
||||
@@ -183,6 +183,9 @@ func (s *Service) Save(ctx context.Context, id, code, name, kind string, cfg Con
|
||||
if modelBinding == nil {
|
||||
modelBinding = json.RawMessage(`{}`)
|
||||
}
|
||||
if departmentIDs == nil {
|
||||
departmentIDs = []string{}
|
||||
}
|
||||
_, err = s.pool.Exec(ctx, `INSERT INTO gateway.channels(id,code,name,kind,encrypted_config,config_kek_version,encrypted_api_key,api_key_kek_version,model_binding,department_ids,enabled,created_by)
|
||||
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)
|
||||
ON CONFLICT(code) DO UPDATE SET name=$3,kind=$4,encrypted_config=$5,config_kek_version=$6,
|
||||
@@ -415,9 +418,12 @@ func (s *Service) postJSON(ctx context.Context, endpoint string, payload []byte,
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyWeComSignature 校验企业微信回调签名(URL 参数签名)。
|
||||
// VerifyWeComSignature 校验企业微信回调签名。
|
||||
// 官方算法:token、timestamp、nonce、echostr(或加密消息)四个参数按字典序
|
||||
// 排序后拼接,取 SHA1 十六进制与 msg_signature 比较。values 用于带密文消息
|
||||
// 体校验时补充参与签名计算的参数。
|
||||
func VerifyWeComSignature(token, timestamp, nonce, echostr string, values map[string]string) (string, bool) {
|
||||
parts := []string{token, timestamp, nonce}
|
||||
parts := []string{token, timestamp, nonce, echostr}
|
||||
if values != nil {
|
||||
keys := make([]string, 0, len(values))
|
||||
for key := range values {
|
||||
|
||||
Reference in New Issue
Block a user