feat:web page anti-tamper protection

This commit is contained in:
samwaf
2026-07-01 13:47:32 +08:00
parent 69dde96f17
commit d698f88e69
20 changed files with 1116 additions and 0 deletions
+2
View File
@@ -41,6 +41,7 @@ type APIGroup struct {
WafPrivateInfoApi
WafPrivateGroupApi
WafCacheRuleApi
WafTamperRuleApi
WafTunnelApi
WafVpConfigApi
WafFileApi
@@ -120,6 +121,7 @@ var (
wafPrivateInfoService = waf_service.WafPrivateInfoServiceApp
wafPrivateGroupService = waf_service.WafPrivateGroupServiceApp
wafCacheRuleService = waf_service.WafCacheRuleServiceApp
wafTamperRuleService = waf_service.WafTamperRuleServiceApp
wafTunnelService = waf_service.WafTunnelServiceApp
wafMonitorService = waf_service.WafSystemMonitorServiceApp
+195
View File
@@ -0,0 +1,195 @@
package api
import (
"SamWaf/enums"
"SamWaf/global"
"SamWaf/model"
"SamWaf/model/common/response"
"SamWaf/model/request"
"SamWaf/model/spec"
"encoding/base64"
"errors"
"strings"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type WafTamperRuleApi struct {
}
// AddApi 新增网页防篡改规则
func (w *WafTamperRuleApi) AddApi(c *gin.Context) {
var req request.WafTamperRuleAddReq
err := c.ShouldBindJSON(&req)
if err == nil {
cnt := wafTamperRuleService.CheckIsExistApi(req)
if cnt == 0 {
err = wafTamperRuleService.AddApi(req)
if err == nil {
w.NotifyWaf(req.HostCode)
response.OkWithMessage("添加成功", c)
} else {
response.FailWithMessage("添加失败:"+err.Error(), c)
}
return
} else {
response.FailWithMessage("同站点下该URL的防篡改规则已存在", c)
return
}
} else {
response.FailWithMessage("解析失败", c)
}
}
// GetDetailApi 获取网页防篡改规则详情(不含基线正文)
func (w *WafTamperRuleApi) GetDetailApi(c *gin.Context) {
var req request.WafTamperRuleDetailReq
err := c.ShouldBind(&req)
if err == nil {
bean := wafTamperRuleService.GetDetailApi(req)
response.OkWithDetailed(bean, "获取成功", c)
} else {
response.FailWithMessage("解析失败", c)
}
}
// GetListApi 获取网页防篡改规则列表(不含基线正文,保持轻量)
func (w *WafTamperRuleApi) GetListApi(c *gin.Context) {
var req request.WafTamperRuleSearchReq
err := c.ShouldBindJSON(&req)
if err == nil {
list, total, _ := wafTamperRuleService.GetListApi(req)
response.OkWithDetailed(response.PageResult{
List: list,
Total: total,
PageIndex: req.PageIndex,
PageSize: req.PageSize,
}, "获取成功", c)
} else {
response.FailWithMessage("解析失败", c)
}
}
// DelApi 删除网页防篡改规则
func (w *WafTamperRuleApi) DelApi(c *gin.Context) {
var req request.WafTamperRuleDelReq
err := c.ShouldBind(&req)
if err == nil {
bean := wafTamperRuleService.GetDetailByIdApi(req.Id)
if bean.Id == "" {
response.FailWithMessage("未找到信息", c)
return
}
err = wafTamperRuleService.DelApi(req)
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
response.FailWithMessage("请检测参数", c)
} else if err != nil {
response.FailWithMessage("发生错误", c)
} else {
w.NotifyWaf(bean.HostCode)
response.OkWithMessage("删除成功", c)
}
} else {
response.FailWithMessage("解析失败", c)
}
}
// ModifyApi 编辑网页防篡改规则
func (w *WafTamperRuleApi) ModifyApi(c *gin.Context) {
var req request.WafTamperRuleEditReq
err := c.ShouldBindJSON(&req)
if err == nil {
err = wafTamperRuleService.ModifyApi(req)
if err != nil {
response.FailWithMessage("编辑发生错误:"+err.Error(), c)
} else {
w.NotifyWaf(req.HostCode)
response.OkWithMessage("编辑成功", c)
}
} else {
response.FailWithMessage("解析失败", c)
}
}
// RelearnApi 重新学习基线
func (w *WafTamperRuleApi) RelearnApi(c *gin.Context) {
var req request.WafTamperRuleRelearnReq
err := c.ShouldBind(&req)
if err == nil {
bean := wafTamperRuleService.GetDetailByIdApi(req.Id)
if bean.Id == "" {
response.FailWithMessage("未找到信息", c)
return
}
err = wafTamperRuleService.RelearnApi(req)
if err != nil {
response.FailWithMessage("操作失败:"+err.Error(), c)
} else {
w.NotifyWaf(bean.HostCode)
response.OkWithMessage("已标记重新学习,下次访问该URL将重新捕获基线", c)
}
} else {
response.FailWithMessage("解析失败", c)
}
}
// GetBaselineApi 查看基线正文(按需拉取;文本回内容,二进制只回元数据)
func (w *WafTamperRuleApi) GetBaselineApi(c *gin.Context) {
var req request.WafTamperRuleBaselineReq
err := c.ShouldBind(&req)
if err != nil {
response.FailWithMessage("解析失败", c)
return
}
bean := wafTamperRuleService.GetBaselineApi(req.Id)
if bean.Id == "" {
response.FailWithMessage("未找到信息", c)
return
}
isText := isTextContentType(bean.ContentType)
result := gin.H{
"content_type": bean.ContentType,
"content_size": bean.ContentSize,
"baseline_hash": bean.BaselineHash,
"baseline_status": bean.BaselineStatus,
"last_learn_time": bean.LastLearnTime,
"is_text": isText,
"content": "", // 文本正文
"content_base64": "", // 非文本(图片等)以 base64 返回,供前端 data URL 渲染/下载
}
if len(bean.BaselineContent) > 0 {
if isText {
result["content"] = string(bean.BaselineContent)
} else {
result["content_base64"] = base64.StdEncoding.EncodeToString(bean.BaselineContent)
}
}
response.OkWithDetailed(result, "获取成功", c)
}
// isTextContentType 判断 Content-Type 是否可按文本展示
func isTextContentType(ct string) bool {
ct = strings.ToLower(ct)
if strings.HasPrefix(ct, "text/") {
return true
}
for _, kw := range []string{"javascript", "json", "xml", "html", "css", "ecmascript", "x-www-form-urlencoded"} {
if strings.Contains(ct, kw) {
return true
}
}
return false
}
// NotifyWaf 通知到 waf 引擎实时生效
func (w *WafTamperRuleApi) NotifyWaf(host_code string) {
var list []model.TamperRule
global.GWAF_LOCAL_DB.Where("host_code = ? ", host_code).Find(&list)
var chanInfo = spec.ChanCommonHost{
HostCode: host_code,
Type: enums.ChanTypeTamperRule,
Content: list,
}
global.GWAF_CHAN_MSG <- chanInfo
}
+5
View File
@@ -683,6 +683,11 @@ func (m *wafSystenService) run() {
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.UpdateHost(msg.HostCode, func(h *wafenginmodel.HostSafe) { h.CacheRule = cacheRule })
zlog.Debug("远程配置", zap.Any("配置缓存规则", cacheRule))
break
case enums.ChanTypeTamperRule:
tamperRules := msg.Content.([]model.TamperRule)
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.UpdateHost(msg.HostCode, func(h *wafenginmodel.HostSafe) { h.TamperRules = tamperRules })
zlog.Debug("远程配置", zap.Any("配置网页防篡改规则", tamperRules))
break
case enums.ChanTypeHostPathRule:
pathRules := msg.Content.([]model.HostPathRule)
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.UpdateHost(msg.HostCode, func(h *wafenginmodel.HostSafe) { h.PathRules = pathRules })
+1
View File
@@ -17,4 +17,5 @@ const (
ChanTypeBlockingPage
ChanTypeCacheRule
ChanTypeHostPathRule
ChanTypeTamperRule
)
+30
View File
@@ -51,6 +51,7 @@ type Hosts struct {
ResponseCompressJSON string `gorm:"type:text" json:"response_compress_json"` //响应压缩配置 jsonGzip/Brotli/Zstd
CookieSecurityJSON string `gorm:"type:text" json:"cookie_security_json"` //Cookie安全保护配置 jsonHttpOnly/Secure/SameSite
CsrfJSON string `gorm:"type:text" json:"csrf_json"` //CSRF防护配置 jsonOrigin/Referer 强校验)
TamperJSON string `gorm:"type:text" json:"tamper_json"` //网页防篡改配置 json(响应基线比对)
IPMode string `gorm:"size:20" json:"ip_mode"` //IP提取模式: "nic" 网卡模式 或 "proxy" 代理模式
}
@@ -244,6 +245,35 @@ func ParseCsrfConfig(jsonStr string) CsrfConfig {
return c
}
// TamperConfig 网页防篡改配置(反代响应基线比对)
type TamperConfig struct {
IsEnable int `json:"is_enable"` // 1 开启 0 关闭(默认0,老站点不受影响)
Action string `json:"action"` // "replace"(比对+命中回吐正确副本+告警,默认) / "alert"(仅告警,仍放行后端页,监控档)
MaxSizeKB int `json:"max_size_kb"` // 基线最大字节(KB),默认1024(1MB),超限不学习/不保护
}
// ParseTamperConfig 解析网页防篡改配置;空 JSON 给安全默认值(但默认关闭)
func ParseTamperConfig(jsonStr string) TamperConfig {
c := TamperConfig{
IsEnable: 0,
Action: "replace",
MaxSizeKB: 1024,
}
if jsonStr == "" {
return c
}
if err := json.Unmarshal([]byte(jsonStr), &c); err != nil {
return TamperConfig{IsEnable: 0, Action: "replace", MaxSizeKB: 1024}
}
if c.Action == "" {
c.Action = "replace"
}
if c.MaxSizeKB <= 0 {
c.MaxSizeKB = 1024
}
return c
}
// TransportConfig 传输配置
type TransportConfig struct {
MaxIdleConns int `json:"max_idle_conns"` // 最大空闲连接数
+2
View File
@@ -45,6 +45,7 @@ type WafHostAddReq struct {
ResponseCompressJSON string `json:"response_compress_json"` //响应压缩配置 json
CookieSecurityJSON string `json:"cookie_security_json"` //Cookie安全保护配置 json
CsrfJSON string `json:"csrf_json"` //CSRF防护配置 json
TamperJSON string `json:"tamper_json"` //网页防篡改配置 json
IPMode string `json:"ip_mode"` //IP提取模式: "nic" 网卡模式 或 "proxy" 代理模式
}
@@ -99,6 +100,7 @@ type WafHostEditReq struct {
ResponseCompressJSON string `json:"response_compress_json"` //响应压缩配置 json
CookieSecurityJSON string `json:"cookie_security_json"` //Cookie安全保护配置 json
CsrfJSON string `json:"csrf_json"` //CSRF防护配置 json
TamperJSON string `json:"tamper_json"` //网页防篡改配置 json
IPMode string `json:"ip_mode"` //IP提取模式: "nic" 网卡模式 或 "proxy" 代理模式
}
type WafHostGuardStatusReq struct {
+39
View File
@@ -0,0 +1,39 @@
package request
import "SamWaf/model/common/request"
type WafTamperRuleAddReq struct {
HostCode string `json:"host_code" form:"host_code"`
Url string `json:"url" form:"url"`
RuleName string `json:"rule_name" form:"rule_name"`
IsEnable int `json:"is_enable" form:"is_enable"`
IgnoreQuery int `json:"ignore_query" form:"ignore_query"`
Remarks string `json:"remarks" form:"remarks"`
}
type WafTamperRuleEditReq struct {
Id string `json:"id"`
HostCode string `json:"host_code" form:"host_code"`
Url string `json:"url" form:"url"`
RuleName string `json:"rule_name" form:"rule_name"`
IsEnable int `json:"is_enable" form:"is_enable"`
IgnoreQuery int `json:"ignore_query" form:"ignore_query"`
Remarks string `json:"remarks" form:"remarks"`
}
type WafTamperRuleDetailReq struct {
Id string `json:"id" form:"id"`
}
type WafTamperRuleDelReq struct {
Id string `json:"id" form:"id"`
}
type WafTamperRuleRelearnReq struct {
Id string `json:"id" form:"id"`
}
// WafTamperRuleBaselineReq 查看/下载基线正文
type WafTamperRuleBaselineReq struct {
Id string `json:"id" form:"id"`
}
type WafTamperRuleSearchReq struct {
HostCode string `json:"host_code" form:"host_code"`
request.PageInfo
}
+24
View File
@@ -0,0 +1,24 @@
package model
import "SamWaf/model/baseorm"
// TamperRule 网页防篡改规则(按站点、按精确 URL 存基线,反代响应基线比对)
type TamperRule struct {
baseorm.BaseOrm
HostCode string `json:"host_code" gorm:"column:host_code;size:64"` // 主机代码
Url string `json:"url" gorm:"column:url;size:1024"` // 精确URL路径(不含query),如 /index.html、/app.js
RuleName string `json:"rule_name" gorm:"column:rule_name;size:255"` // 规则名称
IsEnable int `json:"is_enable" gorm:"column:is_enable"` // 1启用 0停用
IgnoreQuery int `json:"ignore_query" gorm:"column:ignore_query"` // 1忽略query照常比对(静态资源带?v=时间戳默认) / 0带query则跳过放行
BaselineHash string `json:"baseline_hash" gorm:"column:baseline_hash;size:64"` // sha256(hex) of 解压后正文
BaselineContent []byte `json:"baseline_content" gorm:"column:baseline_content"` // 解压后正文字节(≤上限);DB存储→多节点可同步
ContentType string `json:"content_type" gorm:"column:content_type;size:255"` // 学习时记录的 Content-Type
StatusCode int `json:"status_code" gorm:"column:status_code"` // 学习时记录的状态码
ContentSize int `json:"content_size" gorm:"column:content_size"` // 基线正文字节数
BaselineStatus int `json:"baseline_status" gorm:"column:baseline_status"` // 0未学习 1已学习 2学习失败(超限)
BaselineMsg string `json:"baseline_msg" gorm:"column:baseline_msg;size:500"` // 学习失败原因
LastLearnTime string `json:"last_learn_time" gorm:"column:last_learn_time;size:32"` // 上次学习时间
TamperCount int `json:"tamper_count" gorm:"column:tamper_count"` // 累计命中篡改次数
LastTamperTime string `json:"last_tamper_time" gorm:"column:last_tamper_time;size:32"` // 上次命中篡改时间
Remarks string `json:"remarks" gorm:"column:remarks;size:500"` // 备注
}
+1
View File
@@ -33,6 +33,7 @@ type HostSafe struct {
HttpAuthBases []model.HttpAuthBase //HTTP AUTH校验
BlockingPage map[string]model.BlockingPage //自定义拦截界面
CacheRule []model.CacheRule //CacheRule
TamperRules []model.TamperRule //网页防篡改规则(含基线正文,供响应比对/回吐)
PathRules []model.HostPathRule //路径路由规则
StaticConfig model.StaticSiteConfig //解析后的静态站点安全配置,供路径规则静态服务共享
}
+1
View File
@@ -39,6 +39,7 @@ type ApiGroup struct {
WafPrivateInfoRouter
WafPrivateGroupRouter
WafCacheRuleRouter
WafTamperRuleRouter
WafTunnelRouter
WafVpConfigRouter
WafFileRouter
+21
View File
@@ -0,0 +1,21 @@
package router
import (
"SamWaf/api"
"github.com/gin-gonic/gin"
)
type WafTamperRuleRouter struct {
}
func (receiver *WafTamperRuleRouter) InitWafTamperRuleRouter(group *gin.RouterGroup) {
api := api.APIGroupAPP.WafTamperRuleApi
router := group.Group("")
router.POST("/api/v1/wafhost/tamperrule/add", api.AddApi)
router.POST("/api/v1/wafhost/tamperrule/list", api.GetListApi)
router.GET("/api/v1/wafhost/tamperrule/detail", api.GetDetailApi)
router.POST("/api/v1/wafhost/tamperrule/edit", api.ModifyApi)
router.GET("/api/v1/wafhost/tamperrule/del", api.DelApi)
router.GET("/api/v1/wafhost/tamperrule/relearn", api.RelearnApi)
router.GET("/api/v1/wafhost/tamperrule/baseline", api.GetBaselineApi)
}
+2
View File
@@ -103,6 +103,7 @@ func (receiver *WafHostService) AddApi(wafHostAddReq request.WafHostAddReq) (str
ResponseCompressJSON: wafHostAddReq.ResponseCompressJSON,
CookieSecurityJSON: wafHostAddReq.CookieSecurityJSON,
CsrfJSON: wafHostAddReq.CsrfJSON,
TamperJSON: wafHostAddReq.TamperJSON,
IPMode: wafHostAddReq.IPMode,
}
global.GWAF_LOCAL_DB.Create(wafHost)
@@ -170,6 +171,7 @@ func (receiver *WafHostService) ModifyApi(wafHostEditReq request.WafHostEditReq)
"ResponseCompressJSON": wafHostEditReq.ResponseCompressJSON,
"CookieSecurityJSON": wafHostEditReq.CookieSecurityJSON,
"CsrfJSON": wafHostEditReq.CsrfJSON,
"TamperJSON": wafHostEditReq.TamperJSON,
"IPMode": wafHostEditReq.IPMode,
}
err := global.GWAF_LOCAL_DB.Debug().Model(model.Hosts{}).Where("CODE=?", wafHostEditReq.CODE).Updates(hostMap).Error
@@ -0,0 +1,160 @@
package waf_service
import (
"SamWaf/common/uuid"
"SamWaf/customtype"
"SamWaf/global"
"SamWaf/model"
"SamWaf/model/baseorm"
"SamWaf/model/request"
"errors"
"strings"
"time"
)
type WafTamperRuleService struct{}
var WafTamperRuleServiceApp = new(WafTamperRuleService)
// validateUrl 受保护 URL 必须是精确路径:非空、以 / 开头、不含通配符与 query
func validateTamperUrl(url string) error {
url = strings.TrimSpace(url)
if url == "" {
return errors.New("受保护URL不能为空")
}
if !strings.HasPrefix(url, "/") {
return errors.New("受保护URL需以 / 开头的精确路径,例如 /index.html")
}
if strings.Contains(url, "*") {
return errors.New("受保护URL不支持通配符,请填写精确路径")
}
if strings.ContainsAny(url, "?#") {
return errors.New("受保护URL不能包含参数(?)或锚点(#)")
}
return nil
}
func (receiver *WafTamperRuleService) AddApi(req request.WafTamperRuleAddReq) error {
if err := validateTamperUrl(req.Url); err != nil {
return err
}
var bean = &model.TamperRule{
BaseOrm: baseorm.BaseOrm{
Id: uuid.GenUUID(),
USER_CODE: global.GWAF_USER_CODE,
Tenant_ID: global.GWAF_TENANT_ID,
CREATE_TIME: customtype.JsonTime(time.Now()),
UPDATE_TIME: customtype.JsonTime(time.Now()),
},
HostCode: req.HostCode,
Url: strings.TrimSpace(req.Url),
RuleName: req.RuleName,
IsEnable: req.IsEnable,
IgnoreQuery: req.IgnoreQuery,
BaselineStatus: 0, // 待学习
Remarks: req.Remarks,
}
global.GWAF_LOCAL_DB.Create(bean)
return nil
}
func (receiver *WafTamperRuleService) CheckIsExistApi(req request.WafTamperRuleAddReq) int {
var total int64 = 0
global.GWAF_LOCAL_DB.Model(&model.TamperRule{}).Where("host_code=? and url=?", req.HostCode, strings.TrimSpace(req.Url)).Count(&total)
return int(total)
}
func (receiver *WafTamperRuleService) ModifyApi(req request.WafTamperRuleEditReq) error {
if err := validateTamperUrl(req.Url); err != nil {
return err
}
// 同站点同 URL 唯一(排除自身)
var total int64 = 0
global.GWAF_LOCAL_DB.Model(&model.TamperRule{}).Where("host_code=? and url=? and id<>?", req.HostCode, strings.TrimSpace(req.Url), req.Id).Count(&total)
if total > 0 {
return errors.New("同站点下该URL的防篡改规则已存在")
}
// 若 URL 变化则基线作废,需重新学习
var old model.TamperRule
global.GWAF_LOCAL_DB.Where("id=?", req.Id).Find(&old)
beanMap := map[string]interface{}{
"HostCode": req.HostCode,
"Url": strings.TrimSpace(req.Url),
"RuleName": req.RuleName,
"IsEnable": req.IsEnable,
"IgnoreQuery": req.IgnoreQuery,
"Remarks": req.Remarks,
"UPDATE_TIME": customtype.JsonTime(time.Now()),
}
if old.Id != "" && old.Url != strings.TrimSpace(req.Url) {
beanMap["BaselineStatus"] = 0
beanMap["BaselineHash"] = ""
beanMap["BaselineContent"] = []byte{}
beanMap["BaselineMsg"] = "URL已变更,待重新学习"
}
err := global.GWAF_LOCAL_DB.Model(model.TamperRule{}).Where("id = ?", req.Id).Updates(beanMap).Error
return err
}
// RelearnApi 触发重新学习:清空基线状态,下次访问该 URL 时重新捕获
func (receiver *WafTamperRuleService) RelearnApi(req request.WafTamperRuleRelearnReq) error {
beanMap := map[string]interface{}{
"BaselineStatus": 0,
"BaselineHash": "",
"BaselineContent": []byte{},
"ContentSize": 0,
"BaselineMsg": "已标记重新学习",
"UPDATE_TIME": customtype.JsonTime(time.Now()),
}
return global.GWAF_LOCAL_DB.Model(model.TamperRule{}).Where("id = ?", req.Id).Updates(beanMap).Error
}
func (receiver *WafTamperRuleService) GetDetailApi(req request.WafTamperRuleDetailReq) model.TamperRule {
var bean model.TamperRule
// 列表/详情不带大 blob,避免加载慢
global.GWAF_LOCAL_DB.Omit("baseline_content").Where("id=?", req.Id).Find(&bean)
return bean
}
func (receiver *WafTamperRuleService) GetDetailByIdApi(id string) model.TamperRule {
var bean model.TamperRule
global.GWAF_LOCAL_DB.Omit("baseline_content").Where("id=?", id).Find(&bean)
return bean
}
// GetBaselineApi 按需取基线正文(含 blob),供“查看基线”弹窗
func (receiver *WafTamperRuleService) GetBaselineApi(id string) model.TamperRule {
var bean model.TamperRule
global.GWAF_LOCAL_DB.Where("id=?", id).Find(&bean)
return bean
}
func (receiver *WafTamperRuleService) GetListApi(req request.WafTamperRuleSearchReq) ([]model.TamperRule, int64, error) {
var list []model.TamperRule
var total int64 = 0
var whereField = ""
var whereValues []interface{}
if len(req.HostCode) > 0 {
whereField = " host_code=? "
whereValues = append(whereValues, req.HostCode)
}
// Omit baseline_content:列表绝不携带大 blob
global.GWAF_LOCAL_DB.Model(&model.TamperRule{}).Omit("baseline_content").Where(whereField, whereValues...).Limit(req.PageSize).Offset(req.PageSize * (req.PageIndex - 1)).Order("create_time desc").Find(&list)
global.GWAF_LOCAL_DB.Model(&model.TamperRule{}).Where(whereField, whereValues...).Count(&total)
return list, total, nil
}
func (receiver *WafTamperRuleService) DelApi(req request.WafTamperRuleDelReq) error {
var bean model.TamperRule
err := global.GWAF_LOCAL_DB.Where("id = ?", req.Id).First(&bean).Error
if err != nil {
return err
}
err = global.GWAF_LOCAL_DB.Where("id = ?", req.Id).Delete(model.TamperRule{}).Error
return err
}
+33
View File
@@ -1089,6 +1089,39 @@ func RunCoreDBMigrations(db *gorm.DB) error {
return nil
},
},
{
ID: "202607010001_add_tamper_protection",
Migrate: func(tx *gorm.DB) error {
zlog.Info("迁移 202607010001: 为 hosts 表添加 tamper_json 字段并新建 tamper_rule 表")
if !tx.Migrator().HasColumn(&model.Hosts{}, "tamper_json") {
if err := tx.Migrator().AddColumn(&model.Hosts{}, "tamper_json"); err != nil {
return fmt.Errorf("添加 tamper_json 字段失败: %w", err)
}
defaultJSON := `{"is_enable":0,"action":"replace","max_size_kb":1024}`
if err := tx.Exec("UPDATE hosts SET tamper_json = ? WHERE tamper_json IS NULL OR tamper_json = ''", defaultJSON).Error; err != nil {
zlog.Warn("设置 tamper_json 默认值失败", "error", err.Error())
}
} else {
zlog.Info("tamper_json 字段已存在,跳过添加")
}
// 创建/同步网页防篡改规则表(幂等)
if err := tx.AutoMigrate(&model.TamperRule{}); err != nil {
return fmt.Errorf("同步 tamper_rule 表失败: %w", err)
}
zlog.Info("网页防篡改结构迁移成功")
return nil
},
Rollback: func(tx *gorm.DB) error {
zlog.Info("回滚 202607010001: 删除 tamper_json 字段与 tamper_rule 表")
if tx.Migrator().HasColumn(&model.Hosts{}, "tamper_json") {
_ = tx.Migrator().DropColumn(&model.Hosts{}, "tamper_json")
}
if tx.Migrator().HasTable(&model.TamperRule{}) {
return tx.Migrator().DropTable(&model.TamperRule{})
}
return nil
},
},
{
ID: "202606290001_add_rbac_and_password_policy",
Migrate: func(tx *gorm.DB) error {
+84
View File
@@ -7,6 +7,7 @@ import (
"SamWaf/model"
"SamWaf/model/wafenginmodel"
"bytes"
"mime"
"net/http"
"os"
"path"
@@ -296,6 +297,11 @@ func (waf *WafEngine) serveStaticFile(w http.ResponseWriter, r *http.Request, co
return true
}
// 网页防篡改:受保护静态文件的基线比对/回吐(命中 replace 则短路,不再走正常伺服)
if waf.checkAndServeStaticTamper(w, r, absFullPath, config, weblog, hostsafe) {
return true
}
// 记录合法的静态文件访问到日志队列
waf.logStaticFileAccess(r.URL.Path, r.RemoteAddr, fileInfo.Size(), weblog, hostsafe)
@@ -522,6 +528,84 @@ func (waf *WafEngine) setSecurityHeaders(w http.ResponseWriter, config model.Sta
}
}
// checkAndServeStaticTamper 静态伺服链路的网页防篡改:命中受保护规则则读盘比对基线。
// 静态站点服务 / 路径路由静态文件走 http.ServeFile 直接落盘伺服、不过 modifyResponse
// 故在此单独挂钩。返回 true 表示已完整处理(已回吐正确副本并记日志),调用方应直接 return;
// 返回 false 表示未拦截(未开启/非GET/无匹配规则/带参跳过/未学习/一致/仅告警),走正常静态伺服。
func (waf *WafEngine) checkAndServeStaticTamper(w http.ResponseWriter, r *http.Request, absFullPath string, config model.StaticSiteConfig, weblog *innerbean.WebLog, hostsafe *wafenginmodel.HostSafe) bool {
cfg := model.ParseTamperConfig(hostsafe.Host.TamperJSON)
if cfg.IsEnable != 1 {
return false
}
// 只对 GET 比对(HEAD 无正文,交由正常伺服)
if !strings.EqualFold(r.Method, http.MethodGet) {
return false
}
rule := matchTamperRule(hostsafe.TamperRules, r.URL.Path)
if rule == nil {
return false
}
if rule.IgnoreQuery == 0 && r.URL.RawQuery != "" {
return false
}
// 静态文件即基线原文(磁盘未压缩),直接读盘做哈希基准
content, err := os.ReadFile(absFullPath)
if err != nil || len(content) == 0 {
return false
}
switch evaluateTamper(sha256Hex(content), *rule) {
case tamperCapture:
waf.captureTamperBaseline(rule, content, staticContentType(absFullPath, content), http.StatusOK, cfg)
return false
case tamperPass, tamperSkip:
return false
case tamperTampered:
replaced := cfg.Action != "alert"
// 统计 + 告警(异步,文案区分是否已替换)
waf.onTamperDetected(rule, weblog, replaced)
weblog.RULE = "网页防篡改"
weblog.RISK_LEVEL = 3
weblog.GUEST_IDENTIFICATION = "网页篡改"
if !replaced {
// 仅告警:不替换,放行原(被篡改)文件,日志由正常流程记录
return false
}
// replace:回吐基线正确副本并短路
waf.serveStaticTamperBaseline(w, r, rule, config)
weblog.ACTION = "阻止"
weblog.STATUS = "200 OK"
weblog.STATUS_CODE = http.StatusOK
weblog.RES_CONTENT_LENGTH = int64(len(rule.BaselineContent))
waf.enqueueTamperLog(weblog, hostsafe.Host.EXCLUDE_URL_LOG)
return true
}
return false
}
// serveStaticTamperBaseline 静态链路回吐基线正确副本(未压缩直出,带安全头)
func (waf *WafEngine) serveStaticTamperBaseline(w http.ResponseWriter, r *http.Request, rule *model.TamperRule, config model.StaticSiteConfig) {
waf.setSecurityHeaders(w, config)
ct := rule.ContentType
if ct == "" {
ct = "application/octet-stream"
}
w.Header().Set("Content-Type", ct)
w.Header().Set("Content-Length", strconv.Itoa(len(rule.BaselineContent)))
w.WriteHeader(http.StatusOK)
if !strings.EqualFold(r.Method, http.MethodHead) {
w.Write(rule.BaselineContent)
}
}
// staticContentType 依扩展名/内容推断静态文件 Content-Type(与 http.ServeFile 行为一致)
func staticContentType(fullPath string, content []byte) string {
if ct := mime.TypeByExtension(filepath.Ext(fullPath)); ct != "" {
return ct
}
return http.DetectContentType(content)
}
// logStaticFileAccess 记录成功的静态文件访问到传入的weblog
func (waf *WafEngine) logStaticFileAccess(path, remoteAddr string, fileSize int64, weblog *innerbean.WebLog, hostsafe *wafenginmodel.HostSafe) {
// 更新weblog信息
+293
View File
@@ -0,0 +1,293 @@
package wafenginecore
import (
"SamWaf/enums"
"SamWaf/global"
"SamWaf/innerbean"
"SamWaf/model"
"SamWaf/model/spec"
"SamWaf/utils"
"bytes"
"compress/flate"
"compress/gzip"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"sync"
"time"
"github.com/andybalholm/brotli"
"github.com/klauspost/compress/zstd"
"gorm.io/gorm"
)
// tamperLearning 防止同一规则并发重复学习(内存态,key=ruleId)
var tamperLearning sync.Map
// tamperDecision 防篡改判定结果
type tamperDecision int
const (
tamperPass tamperDecision = iota // 未篡改,放行
tamperCapture // 未学习,需捕获基线
tamperTampered // 已篡改
tamperSkip // 学习失败等,跳过(不保护)
)
// sha256Hex 计算字节内容的 sha256 十六进制串
func sha256Hex(b []byte) string {
h := sha256.Sum256(b)
return hex.EncodeToString(h[:])
}
// matchTamperRule 按路径精确匹配启用中的防篡改规则(r.URL.Path 天然不含 query
func matchTamperRule(rules []model.TamperRule, path string) *model.TamperRule {
for i := range rules {
if rules[i].IsEnable == 1 && rules[i].Url == path {
return &rules[i]
}
}
return nil
}
// isTamperCandidate 是否需要做防篡改比对:GET + 规则启用 + (忽略query 或 无query)
func isTamperCandidate(method, rawQuery string, rule model.TamperRule) bool {
if !strings.EqualFold(method, http.MethodGet) {
return false
}
if rule.IsEnable != 1 {
return false
}
if rule.IgnoreQuery == 0 && rawQuery != "" {
return false
}
return true
}
// overBaselineCap 基线正文是否超过大小上限
func overBaselineCap(size, maxKB int) bool {
if maxKB <= 0 {
maxKB = 1024
}
return size > maxKB*1024
}
// evaluateTamper 依据基线状态与实时哈希判定
func evaluateTamper(liveHash string, rule model.TamperRule) tamperDecision {
switch rule.BaselineStatus {
case 0:
return tamperCapture
case 1:
if liveHash == rule.BaselineHash {
return tamperPass
}
return tamperTampered
default:
return tamperSkip
}
}
// readDecompressedBody 读取响应正文并按 Content-Encoding 解压,返回解压后字节;
// 同时把 resp.Body 复位为原始字节,供后续常规处理再次读取。
// 以“解压后内容”为哈希基准,避免同一文件因 Accept-Encoding 不同(压/不压)导致哈希不一致。
func readDecompressedBody(resp *http.Response) ([]byte, error) {
raw, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
// 复位原始 body 供下游再次读取
resp.Body = io.NopCloser(bytes.NewReader(raw))
switch strings.ToLower(resp.Header.Get("Content-Encoding")) {
case "gzip":
zr, e := gzip.NewReader(bytes.NewReader(raw))
if e != nil {
return nil, e
}
defer zr.Close()
return io.ReadAll(zr)
case "deflate":
fr := flate.NewReader(bytes.NewReader(raw))
defer fr.Close()
return io.ReadAll(fr)
case "br":
return io.ReadAll(brotli.NewReader(bytes.NewReader(raw)))
case "zstd":
zr, e := zstd.NewReader(bytes.NewReader(raw))
if e != nil {
return nil, e
}
defer zr.Close()
return io.ReadAll(zr)
default:
return raw, nil
}
}
// checkAndHandleTamper 网页防篡改主流程。返回 true 表示已完整处理(回吐基线并记日志),
// 调用方应直接 return nil;返回 false 则继续正常应答处理。
func (waf *WafEngine) checkAndHandleTamper(resp *http.Response, r *http.Request, host string, weblog *innerbean.WebLog) bool {
hostTarget, ok := waf.rt().HostTarget[host]
if !ok {
return false
}
cfg := model.ParseTamperConfig(hostTarget.Host.TamperJSON)
if cfg.IsEnable != 1 {
return false
}
if !strings.EqualFold(r.Method, http.MethodGet) {
return false
}
// 仅对正常 200 响应学习/比对,避免把 404/500/302 学成基线
if resp.StatusCode != http.StatusOK {
return false
}
rule := matchTamperRule(hostTarget.TamperRules, r.URL.Path)
if rule == nil {
return false
}
if rule.IgnoreQuery == 0 && r.URL.RawQuery != "" {
return false
}
body, err := readDecompressedBody(resp)
if err != nil || len(body) == 0 {
return false
}
switch evaluateTamper(sha256Hex(body), *rule) {
case tamperCapture:
waf.captureTamperBaseline(rule, body, resp.Header.Get("Content-Type"), resp.StatusCode, cfg)
return false
case tamperPass, tamperSkip:
return false
case tamperTampered:
// 命中篡改:alert 仅告警不替换;replace 回吐正确副本并短路
replaced := cfg.Action != "alert"
// 统计 + 告警(异步,文案区分是否已替换)
waf.onTamperDetected(rule, weblog, replaced)
weblog.RULE = "网页防篡改"
weblog.RISK_LEVEL = 3
weblog.GUEST_IDENTIFICATION = "网页篡改"
if !replaced {
// 仅告警:不替换,放行后端页,日志由正常流程记录
return false
}
// replace:回吐基线正确副本并短路
waf.serveTamperBaseline(resp, rule)
// 与 WAF 其它拦截一致的处置态(访问日志「状态」列识别 放行/阻止/禁止)
weblog.ACTION = "阻止"
weblog.STATUS = resp.Status
weblog.STATUS_CODE = resp.StatusCode
weblog.RES_CONTENT_LENGTH = sanitizeContentLength(resp.ContentLength)
weblog.ResHeader = joinHeader(resp.Header)
waf.enqueueTamperLog(weblog, hostTarget.Host.EXCLUDE_URL_LOG)
return true
}
return false
}
// serveTamperBaseline 用基线正文替换响应体(基线为解压后原文,未压缩直出)
func (waf *WafEngine) serveTamperBaseline(resp *http.Response, rule *model.TamperRule) {
content := rule.BaselineContent
resp.Body = io.NopCloser(bytes.NewReader(content))
resp.Header.Del("Content-Encoding")
if rule.ContentType != "" {
resp.Header.Set("Content-Type", rule.ContentType)
}
resp.ContentLength = int64(len(content))
resp.Header.Set("Content-Length", strconv.Itoa(len(content)))
resp.StatusCode = http.StatusOK
resp.Status = "200 OK"
}
// captureTamperBaseline 被动捕获基线(异步、按规则去重),完成后触发热重载
func (waf *WafEngine) captureTamperBaseline(rule *model.TamperRule, body []byte, contentType string, statusCode int, cfg model.TamperConfig) {
if _, loaded := tamperLearning.LoadOrStore(rule.Id, struct{}{}); loaded {
return
}
ruleId := rule.Id
hostCode := rule.HostCode
contentCopy := make([]byte, len(body))
copy(contentCopy, body)
go func() {
defer tamperLearning.Delete(ruleId)
now := time.Now().Format("2006-01-02 15:04:05")
upd := map[string]interface{}{
"LastLearnTime": now,
"ContentSize": len(contentCopy),
}
if overBaselineCap(len(contentCopy), cfg.MaxSizeKB) {
upd["BaselineStatus"] = 2
upd["BaselineMsg"] = fmt.Sprintf("正文 %d 字节超过上限 %d KB,未学习", len(contentCopy), cfg.MaxSizeKB)
} else {
upd["BaselineHash"] = sha256Hex(contentCopy)
upd["BaselineContent"] = contentCopy
upd["ContentType"] = contentType
upd["StatusCode"] = statusCode
upd["BaselineStatus"] = 1
upd["BaselineMsg"] = ""
}
global.GWAF_LOCAL_DB.Model(&model.TamperRule{}).Where("id=?", ruleId).Updates(upd)
waf.notifyTamperReload(hostCode)
}()
}
// onTamperDetected 命中篡改后异步累计次数并发告警;replaced 区分是否已回吐正确副本
func (waf *WafEngine) onTamperDetected(rule *model.TamperRule, weblog *innerbean.WebLog, replaced bool) {
ruleId := rule.Id
domain := weblog.HOST
srcIp := weblog.SRC_IP
url := weblog.URL
go func() {
now := time.Now().Format("2006-01-02 15:04:05")
global.GWAF_LOCAL_DB.Model(&model.TamperRule{}).Where("id=?", ruleId).Updates(map[string]interface{}{
"TamperCount": gorm.Expr("tamper_count + 1"),
"LastTamperTime": now,
})
ruleInfo := "检测到网页被篡改(仅告警,未替换):" + url
if replaced {
ruleInfo = "检测到网页被篡改,已回吐正确副本:" + url
}
global.GQEQUE_MESSAGE_DB.Enqueue(innerbean.RuleMessageInfo{
BaseMessageInfo: innerbean.BaseMessageInfo{OperaType: "网页防篡改", Server: global.GWAF_CUSTOM_SERVER_NAME},
Domain: domain,
RuleInfo: ruleInfo,
Ip: fmt.Sprintf("%s (%s)", srcIp, utils.GetCountry(srcIp)),
})
}()
}
// notifyTamperReload 重新读取该站点防篡改规则并推送到引擎热更新
func (waf *WafEngine) notifyTamperReload(hostCode string) {
var list []model.TamperRule
global.GWAF_LOCAL_DB.Where("host_code=?", hostCode).Find(&list)
global.GWAF_CHAN_MSG <- spec.ChanCommonHost{
HostCode: hostCode,
Type: enums.ChanTypeTamperRule,
Content: list,
}
}
// enqueueTamperLog 防篡改替换短路时记录访问日志(复用 all/abnormal + EXCLUDE_URL_LOG 规则)。
// 反代/静态两条链路共用:调用方先填好 STATUS/STATUS_CODE/RES_CONTENT_LENGTH/ResHeader/ACTION,再传各自的 EXCLUDE_URL_LOG。
func (waf *WafEngine) enqueueTamperLog(weblog *innerbean.WebLog, excludeURLLog string) {
datetimeNow := time.Now()
weblog.TimeSpent = datetimeNow.UnixNano()/1e6 - weblog.UNIX_ADD_TIME
weblog.TASK_FLAG = 1
// 防篡改替换属于异常事件,all / abnormal 两种模式都记录
if excludeURLLog != "" {
for _, line := range strings.Split(excludeURLLog, "\n") {
if line != "" && strings.HasPrefix(weblog.URL, line) {
return
}
}
}
global.GQEQUE_LOG_DB.Enqueue(weblog)
}
+211
View File
@@ -0,0 +1,211 @@
package wafenginecore
import (
"SamWaf/model"
"bytes"
"compress/gzip"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestMatchTamperRule(t *testing.T) {
rules := []model.TamperRule{
{Url: "/index.html", IsEnable: 1},
{Url: "/app.js", IsEnable: 1},
{Url: "/disabled.html", IsEnable: 0},
}
cases := []struct {
path string
want bool // 是否匹配到
}{
{"/index.html", true},
{"/app.js", true},
{"/disabled.html", false}, // 停用
{"/notfound", false},
{"/INDEX.HTML", false}, // 大小写敏感
}
for _, c := range cases {
got := matchTamperRule(rules, c.path)
if (got != nil) != c.want {
t.Errorf("matchTamperRule(%q) matched=%v, 期望 %v", c.path, got != nil, c.want)
}
}
}
func TestIsTamperCandidate(t *testing.T) {
ruleIgnore := model.TamperRule{IsEnable: 1, IgnoreQuery: 1}
ruleStrict := model.TamperRule{IsEnable: 1, IgnoreQuery: 0}
ruleOff := model.TamperRule{IsEnable: 0, IgnoreQuery: 1}
cases := []struct {
name string
method string
rawQuery string
rule model.TamperRule
want bool
}{
{"GET无参忽略query", "GET", "", ruleIgnore, true},
{"GET带参忽略query照常", "GET", "v=123", ruleIgnore, true},
{"GET带参严格跳过", "GET", "v=123", ruleStrict, false},
{"GET无参严格", "GET", "", ruleStrict, true},
{"POST不比对", "POST", "", ruleIgnore, false},
{"HEAD不比对", "HEAD", "", ruleIgnore, false},
{"规则停用", "GET", "", ruleOff, false},
}
for _, c := range cases {
if got := isTamperCandidate(c.method, c.rawQuery, c.rule); got != c.want {
t.Errorf("%s: isTamperCandidate=%v, 期望 %v", c.name, got, c.want)
}
}
}
func TestOverBaselineCap(t *testing.T) {
cases := []struct {
size int
maxKB int
want bool
}{
{1024, 1, false}, // 恰好 1KB,不超
{1025, 1, true}, // 超 1KB
{1024 * 1024, 1024, false}, // 恰好 1MB
{1024*1024 + 1, 1024, true},
{500, 0, false}, // maxKB<=0 用默认 1024
}
for _, c := range cases {
if got := overBaselineCap(c.size, c.maxKB); got != c.want {
t.Errorf("overBaselineCap(%d,%d)=%v, 期望 %v", c.size, c.maxKB, got, c.want)
}
}
}
func TestEvaluateTamper(t *testing.T) {
learned := model.TamperRule{BaselineStatus: 1, BaselineHash: "abc"}
unlearned := model.TamperRule{BaselineStatus: 0}
failed := model.TamperRule{BaselineStatus: 2}
cases := []struct {
name string
liveHash string
rule model.TamperRule
want tamperDecision
}{
{"未学习→捕获", "abc", unlearned, tamperCapture},
{"已学习哈希一致→放行", "abc", learned, tamperPass},
{"已学习哈希不一致→篡改", "xyz", learned, tamperTampered},
{"学习失败→跳过", "abc", failed, tamperSkip},
}
for _, c := range cases {
if got := evaluateTamper(c.liveHash, c.rule); got != c.want {
t.Errorf("%s: evaluateTamper=%v, 期望 %v", c.name, got, c.want)
}
}
}
func TestSha256HexStable(t *testing.T) {
a := sha256Hex([]byte("hello world"))
b := sha256Hex([]byte("hello world"))
c := sha256Hex([]byte("hello world!"))
if a != b {
t.Errorf("相同内容哈希应一致: %s vs %s", a, b)
}
if a == c {
t.Errorf("不同内容哈希应不同")
}
if len(a) != 64 {
t.Errorf("sha256 十六进制应为64字符,实际 %d", len(a))
}
}
func TestReadDecompressedBody(t *testing.T) {
plain := []byte("<html>hello 防篡改</html>")
// 1) 无压缩
respPlain := &http.Response{
Header: http.Header{},
Body: io.NopCloser(bytes.NewReader(plain)),
}
got, err := readDecompressedBody(respPlain)
if err != nil || !bytes.Equal(got, plain) {
t.Fatalf("无压缩读取失败: err=%v got=%q", err, got)
}
// 复位后 body 应可再次读取到原始字节
again, _ := io.ReadAll(respPlain.Body)
if !bytes.Equal(again, plain) {
t.Errorf("resp.Body 未正确复位,got=%q", again)
}
// 2) gzip 压缩:解压后应与原文一致(哈希基准稳定)
var buf bytes.Buffer
gw := gzip.NewWriter(&buf)
gw.Write(plain)
gw.Close()
respGzip := &http.Response{
Header: http.Header{"Content-Encoding": []string{"gzip"}},
Body: io.NopCloser(bytes.NewReader(buf.Bytes())),
}
gotGzip, err := readDecompressedBody(respGzip)
if err != nil || !bytes.Equal(gotGzip, plain) {
t.Fatalf("gzip 解压失败: err=%v got=%q", err, gotGzip)
}
// 压缩与不压缩,解压后哈希必须一致
if sha256Hex(got) != sha256Hex(gotGzip) {
t.Errorf("同一内容压/不压后哈希应一致")
}
}
func TestStaticContentType(t *testing.T) {
// 有扩展名:按扩展名推断(不同机器 mime 表可能有别,只断言非空)
if ct := staticContentType("/site/index.html", []byte("<html></html>")); ct == "" {
t.Errorf(".html 应能推断出 Content-Type,实际为空")
}
// 无扩展名 + HTML 内容:回退 http.DetectContentType
if ct := staticContentType("/site/noext", []byte("<html>hi</html>")); !strings.Contains(ct, "text/html") {
t.Errorf("无扩展名 HTML 内容应回退推断为 text/html,实际 %q", ct)
}
// 无扩展名 + 未知二进制:回退为 application/octet-stream
bin := []byte{0x00, 0x01, 0x02, 0x03, 0x04}
if ct := staticContentType("/site/blob", bin); !strings.Contains(ct, "application/octet-stream") {
t.Errorf("无扩展名未知内容应回退为 octet-stream,实际 %q", ct)
}
}
func TestServeStaticTamperBaseline(t *testing.T) {
waf := &WafEngine{}
baseline := []byte("<html>correct baseline</html>")
rule := &model.TamperRule{ContentType: "text/html; charset=utf-8", BaselineContent: baseline}
// GET:回吐基线正文 + 头部
getRec := httptest.NewRecorder()
waf.serveStaticTamperBaseline(getRec, httptest.NewRequest("GET", "/index.html", nil), rule, model.StaticSiteConfig{})
if getRec.Code != http.StatusOK {
t.Errorf("状态码应为 200,实际 %d", getRec.Code)
}
if !bytes.Equal(getRec.Body.Bytes(), baseline) {
t.Errorf("GET 应回吐基线正文,实际 %q", getRec.Body.String())
}
if ct := getRec.Header().Get("Content-Type"); ct != rule.ContentType {
t.Errorf("Content-Type 应为 %q,实际 %q", rule.ContentType, ct)
}
if cl := getRec.Header().Get("Content-Length"); cl != "29" {
t.Errorf("Content-Length 应为 29,实际 %q", cl)
}
// HEAD:只出头部不出正文
headRec := httptest.NewRecorder()
waf.serveStaticTamperBaseline(headRec, httptest.NewRequest("HEAD", "/index.html", nil), rule, model.StaticSiteConfig{})
if headRec.Body.Len() != 0 {
t.Errorf("HEAD 不应写正文,实际 %d 字节", headRec.Body.Len())
}
if cl := headRec.Header().Get("Content-Length"); cl != "29" {
t.Errorf("HEAD 仍应带 Content-Length 29,实际 %q", cl)
}
// ContentType 为空 → 回退 octet-stream
emptyRec := httptest.NewRecorder()
emptyRule := &model.TamperRule{BaselineContent: []byte("x")}
waf.serveStaticTamperBaseline(emptyRec, httptest.NewRequest("GET", "/x", nil), emptyRule, model.StaticSiteConfig{})
if ct := emptyRec.Header().Get("Content-Type"); ct != "application/octet-stream" {
t.Errorf("空 ContentType 应回退 octet-stream,实际 %q", ct)
}
}
+6
View File
@@ -1109,6 +1109,12 @@ func (waf *WafEngine) modifyResponse() func(*http.Response) error {
// 上游 chunked 传输时 resp.ContentLength 为 -1,先按 0 计;非静态资源后续会用真实落盘字节数回填
weblogfrist.RES_CONTENT_LENGTH = sanitizeContentLength(resp.ContentLength)
// 网页防篡改:反代响应基线比对(命中且 replace 动作则回吐正确副本并短路)
if waf.checkAndHandleTamper(resp, r, host, weblogfrist) {
weblogfrist.BackendCheckCost = time.Now().UnixNano()/1e6 - backendCheckStart
return nil
}
//返回内容的类型
respContentType := strings.ToLower(resp.Header.Get("Content-Type"))
respContentType = strings.Replace(respContentType, "; charset=utf-8", "", -1)
+5
View File
@@ -229,6 +229,10 @@ func (waf *WafEngine) LoadHost(inHost model.Hosts) []innerbean.ServerRunTime {
var cacheRuleList []model.CacheRule
global.GWAF_LOCAL_DB.Where("host_code=? ", inHost.Code).Find(&cacheRuleList)
//查询网页防篡改规则(含基线正文,供响应比对/回吐)
var tamperRuleList []model.TamperRule
global.GWAF_LOCAL_DB.Where("host_code=? ", inHost.Code).Find(&tamperRuleList)
//查询路径路由规则
var pathRuleList []model.HostPathRule
global.GWAF_LOCAL_DB.Where("host_code=? ", inHost.Code).Order("priority asc, create_time asc").Find(&pathRuleList)
@@ -263,6 +267,7 @@ func (waf *WafEngine) LoadHost(inHost model.Hosts) []innerbean.ServerRunTime {
HttpAuthBases: httpAuthList,
BlockingPage: blockingPageMap,
CacheRule: cacheRuleList,
TamperRules: tamperRuleList,
PathRules: pathRuleList,
StaticConfig: staticCfg,
}
+1
View File
@@ -153,6 +153,7 @@ func (web *WafWebManager) initRouter(r *gin.Engine) {
router.ApiGroupApp.InitWafPrivateInfoRouter(RouterGroup)
router.ApiGroupApp.InitWafPrivateGroupRouter(RouterGroup)
router.ApiGroupApp.InitWafCacheRuleRouter(RouterGroup)
router.ApiGroupApp.InitWafTamperRuleRouter(RouterGroup)
router.ApiGroupApp.InitWafTunnelRouter(RouterGroup)
router.ApiGroupApp.InitWafVpConfigRouter(RouterGroup)
router.ApiGroupApp.InitWafFileRouter(RouterGroup)