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:
LLMGuardX Dev
2026-08-13 15:22:19 +08:00
parent 8000bccde3
commit 58535fda7b
21 changed files with 287 additions and 124 deletions
+2 -2
View File
@@ -50,8 +50,8 @@ func (s *AgentPolicyService) Set(ctx context.Context, portalUserID string, polic
if s == nil || s.pool == nil {
return errors.New("安全策略服务不可用")
}
if policy.RateLimitMultiplier < 1 || policy.RateLimitMultiplier > 100 {
return errors.New("限流倍数必须在 1-100 之间")
if policy.RateLimitMultiplier < 1 || policy.RateLimitMultiplier > 10 {
return errors.New("限流倍数必须在 1-10 之间")
}
_, err := s.pool.Exec(ctx, `INSERT INTO gateway.portal_agent_policies(portal_user_id,auto_approve_tools,rate_limit_multiplier) VALUES($1,$2,$3)
ON CONFLICT(portal_user_id) DO UPDATE SET auto_approve_tools=$2,rate_limit_multiplier=$3,updated_at=clock_timestamp()`,
+30 -16
View File
@@ -11,6 +11,7 @@ import (
"net/http"
"net/url"
"strings"
"syscall"
"time"
"aigateway.local/core/internal/platform/cryptox"
@@ -184,6 +185,14 @@ func (s *ToolService) Delete(ctx context.Context, id, actorID string) error {
if used {
return ErrConflict
}
// 有调用历史的工具禁止删除:tool_runs 级联删除会永久丢失报表/审计数据。
var hasRuns bool
if err = tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM gateway.tool_runs WHERE tool_id=$1)`, id).Scan(&hasRuns); err != nil {
return err
}
if hasRuns {
return errors.New("工具存在调用历史,不能删除;请停用以保留报表数据")
}
tag, err := tx.Exec(ctx, `DELETE FROM gateway.tool_definitions WHERE id=$1`, id)
if err != nil {
return err
@@ -234,7 +243,12 @@ func (s *ToolService) enforceGovernance(ctx context.Context, tool Tool, apiKeyID
if err := s.assets.pool.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM gateway.tool_approval_requests WHERE tool_id=$1 AND status='approved')`, tool.ID).Scan(&approved); err != nil {
return false, err
}
if !approved {
// rejected 后 24 小时冷却:避免每次调用都重新发起申请、通知轰炸管理员。
var recentlyRejected bool
if err := s.assets.pool.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM gateway.tool_approval_requests WHERE tool_id=$1 AND status='rejected' AND decided_at>clock_timestamp()-interval '24 hours')`, tool.ID).Scan(&recentlyRejected); err != nil {
return false, err
}
if !approved && !recentlyRejected {
// 自动发起待审申请(唯一部分索引防重复),通知管理员。
requestID, err := newUUID()
if err != nil {
@@ -283,6 +297,9 @@ func (s *ToolService) enforceGovernance(ctx context.Context, tool Tool, apiKeyID
return false, err
}
if count > int64(limit) {
// 被拒调用补偿递减:该次计数不消耗窗口额度,避免故障重试风暴
// 打满窗口波及合法调用者。
_, _ = s.assets.pool.Exec(ctx, `UPDATE gateway.tool_rate_usage SET call_count=greatest(call_count-1,0) WHERE tool_id=$1 AND portal_user_id=$2 AND window_start=date_trunc('minute',clock_timestamp())`, tool.ID, userKey)
return false, ErrToolRateLimited
}
}
@@ -510,24 +527,21 @@ func safeToolDial(allowPrivate bool) func(context.Context, string, string) (net.
if allowPrivate {
return dialer.DialContext
}
return func(ctx context.Context, network, address string) (net.Conn, error) {
host, port, err := net.SplitHostPort(address)
// 在系统 connect 阶段用 Dialer.Control 校验最终目标 IP:内核完成
// 解析后、TCP 握手前回调,校验与连接之间不存在 DNS rebinding 窗口。
dialer.Control = func(_, address string, _ syscall.RawConn) error {
host, _, err := net.SplitHostPort(address)
if err != nil {
return nil, err
return err
}
addresses, err := net.DefaultResolver.LookupIPAddr(ctx, host)
if err != nil {
return nil, err
ip := net.ParseIP(host)
if ip == nil {
return errors.New("工具目标不是 IP 地址")
}
if len(addresses) == 0 {
return nil, errors.New("工具主机没有解析结果")
if ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsMulticast() || ip.IsUnspecified() {
return fmt.Errorf("工具目标为受限地址 %s", ip)
}
for _, candidate := range addresses {
ip := candidate.IP
if ip == nil || ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsMulticast() || ip.IsUnspecified() {
return nil, fmt.Errorf("工具主机解析到受限地址 %s", ip)
}
}
return dialer.DialContext(ctx, network, net.JoinHostPort(addresses[0].IP.String(), port))
return nil
}
return dialer.DialContext
}