Files
sundynix-agentix/sundynix-gateway/internal/middleware/guardrail.go
T
Blizzard e63632adf5 feat(gateway): 输入护栏拦提示词注入/超大体(弃用空桩)+ 单测
Guardrail 中间件此前是空桩(直接 c.Next)。落地输入护栏:

- 新增纯逻辑包 internal/guardrail:Inspect(body) 检测提示词注入(忽略既定指令/
  角色越权/诱导泄露提示词,中英文模式)+ 超大体(>256KB),与 HTTP 解耦便于单测;
  敏感词黑名单留空可扩展。
- 中间件:仅对带 JSON 体的 POST/PUT 检查(文件上传 multipart 与 GET/SSE 跳过);
  限读 + 命中拦截返回 422;未命中则还原请求体(io.NopCloser)供 handler 读取。
- 输出护栏不在网关做:Token 流是 SSE 实时流,网关缓冲会破坏流式 —— 标到路线图,
  应在 dispatcher token 发射层做。

验证:
- 单测:正常输入不误拦、中英文注入均拦、超大体拦、边界恰好放行。
- 实跑(nats+gateway):注入(中/英) → 422 带原因;干净输入 → 202 且 body 正确还原、
  handler 正常发布到 NATS。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 15:19:14 +08:00

52 lines
1.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package middleware 提供 Guardrail 与限流等接入层中间件。
package middleware
import (
"bytes"
"io"
"log"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/sundynix/sundynix-gateway/internal/guardrail"
"github.com/sundynix/sundynix-gateway/internal/store"
)
// Guardrail 实现 Harness 输入护栏:拦截提示词注入 / 超大请求体。
// 只检查带 JSON 体的写请求(POST/PUT);文件上传(multipart)与 GET/SSE 不经此。
// 输出护栏不在此做 —— Token 流为 SSE 实时流,网关缓冲会破坏流式,输出过滤应在
// dispatcher 的 token 发射层(见 PROGRESS 路线图)。
func Guardrail() gin.HandlerFunc {
return func(c *gin.Context) {
if m := c.Request.Method; (m == http.MethodPost || m == http.MethodPut) &&
strings.HasPrefix(c.GetHeader("Content-Type"), "application/json") {
// 限读上限 + 1 字节以判定"过大";命中拦截则后续 handler 不执行。
body, _ := io.ReadAll(io.LimitReader(c.Request.Body, guardrail.MaxJSONBytes+1))
if reason, blocked := guardrail.Inspect(body); blocked {
log.Printf("[guardrail] 拦截 %s %s%s", c.Request.Method, c.Request.URL.Path, reason)
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{"error": "输入护栏拦截:" + reason})
return
}
c.Request.Body = io.NopCloser(bytes.NewReader(body)) // 还原请求体供后续 handler 读取
}
c.Next()
}
}
// RateLimit 基于 Redis 的会话级限流(按客户端 IP,每分钟上限)。
// Redis 降级时 Allow 始终放行,不阻断业务。
func RateLimit(cache *store.Redis) gin.HandlerFunc {
const perMinute = 120
return func(c *gin.Context) {
ok, _ := cache.Allow(c.Request.Context(), c.ClientIP(), perMinute, time.Minute)
if !ok {
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"error": "rate limit exceeded"})
return
}
c.Next()
}
}