Files
sundynix-agentix/sundynix-gateway/internal/middleware/guardrail.go
T
Blizzard 2f78fc565e feat(harness): 输入护栏升级 —— 归一化反绕过(Tier1) + LLM 越狱分类器(Tier2)
原输入护栏纯正则,空格/编码/同形字一改写即漏,且 bannedTerms 空置。升级为两层:

Tier1(网关同步、无 LLM):先归一化再匹配,干掉绕过——
- 小写 + 去零宽字符 + 去变音符 + 同形字折叠(西里尔/希腊→拉丁) +
  拆字间隔还原(i g n o r e / i.g.n.o.r.e → ignore) + base64 解码回扫
- 多视图(原文/归一化/紧凑/解码)匹配高精度注入正则,无需穷举变体
- bannedTerms 经 GUARDRAIL_BANNED_TERMS env 落地
- 软信号(jailbreak/developer mode/无限制…)→ 灰区,放行但打 safety_check 标志

Tier2(dispatcher harness LLM 分类器,escalation):
- 仅对灰区任务执行前调 LLM 裁决 jailbreak+severity,≥0.7 → rejected
- 明确干净/恶意的不付 LLM 成本;模型抖动/解析失败 fail-open 不误锁正常用户

契约新增 MetaSafetyCheck 透传灰区标志;orchestrator 加执行前护栏门控 + SetGuardian。
网关 6 单测 + dispatcher 4 单测,三模块全绿。live:拆字/base64/西里尔同形字均 422 拦,
恶意灰区被 LLM 拒(severity 1)、良性灰区(海盗 roleplay)放行完成。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 09:38:34 +08:00

57 lines
2.1 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))
res := guardrail.Inspect(body)
if res.Blocked {
log.Printf("[guardrail] 拦截 %s %s%s", c.Request.Method, c.Request.URL.Path, res.Reason)
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{"error": "输入护栏拦截:" + res.Reason})
return
}
if res.Suspect { // 灰区:放行但打标,交 Dispatcher 的 LLM 分类器(Tier2)裁决
log.Printf("[guardrail] 灰区放行 %s %s:软信号 %v", c.Request.Method, c.Request.URL.Path, res.Signals)
c.Set("guardrail_suspect", true)
}
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()
}
}