diff --git a/wafenginecore/accessgate/config.go b/wafenginecore/accessgate/config.go index 0956776..c481000 100644 --- a/wafenginecore/accessgate/config.go +++ b/wafenginecore/accessgate/config.go @@ -146,6 +146,8 @@ func IsAccessEnabled(mode int, globalEnable bool) bool { // // 匹配的是「路径段边界」而不是裸字符串前缀:写 /api/webhook 会放行 // /api/webhook 与 /api/webhook/github,但不会放行 /api/webhook_admin。 +// 白名单条目写不写尾斜杠等价(/api 与 /api/ 一个意思),因为用户按目录习惯填的 +// /api/ 若按字面比,反而会漏掉访问目录本身的那个请求。 // 裸前缀匹配在这里是危险的——白名单是给健康检查、webhook 这类调用方开的口子, // 用户想放行的是一棵子树,而不是"所有以这串字符开头的路径", // 后者会让一个 /admin 白名单顺手把 /adminconsole 也放出去。 @@ -159,15 +161,16 @@ func MatchPathPrefix(p string, list []string) bool { if item == "" { continue } - if p == item { + // 用户习惯把目录写成 /api/,但 p 已被 path.Clean 去掉尾斜杠, + // 直接按字符串比会让 /api/ 这条白名单漏掉「访问目录本身」的请求 + // (/api/ 与 /api 都命中不了,只有 /api/xxx 能过),所以两边都归一化到无尾斜杠。 + item = strings.TrimRight(item, "/") + if item == "" { + // 用户显式写了单个 "/",语义就是整站免认证 return true } - // item 自带尾斜杠时直接按前缀比;否则补一个斜杠,确保只在段边界上匹配 - if strings.HasSuffix(item, "/") { - if strings.HasPrefix(p, item) { - return true - } - } else if strings.HasPrefix(p, item+"/") { + // 补一个斜杠再比前缀,确保只在段边界上匹配 + if p == item || strings.HasPrefix(p, item+"/") { return true } } diff --git a/wafenginecore/accessgate/config_test.go b/wafenginecore/accessgate/config_test.go index 3a9ac2f..a92e960 100644 --- a/wafenginecore/accessgate/config_test.go +++ b/wafenginecore/accessgate/config_test.go @@ -126,14 +126,23 @@ func TestMatchPathPrefix(t *testing.T) { t.Fatalf("MatchPathPrefix(%q) = %v, 期望 %v", c.p, got, c.want) } } - // 用户自己写了尾斜杠时按前缀比,只放行子树 + // 用户按目录习惯写的尾斜杠:子树要放行,同前缀的兄弟路径不放行, + // 而且「访问目录本身」也必须放行 —— 请求路径进来前已被 path.Clean 去掉尾斜杠, + // 按字面比会让 /api/ 这条白名单恰好漏掉 /api/ 这个最常见的访问。 slash := []string{"/static/"} if !MatchPathPrefix("/static/a.js", slash) { t.Fatal("/static/ 应放行其子路径") } + if !MatchPathPrefix("/static", slash) { + t.Fatal("/static/ 应放行目录本身(path.Clean 后就是 /static)") + } if MatchPathPrefix("/staticx", slash) { t.Fatal("/static/ 不该放行 /staticx") } + // 单个 "/" 是用户显式表达的「整站免认证」,行为保持不变 + if !MatchPathPrefix("/whatever", []string{"/"}) { + t.Fatal(`"/" 应放行所有路径`) + } // nil 列表必须安全 if MatchPathPrefix("/anything", nil) { t.Fatal("空白名单不该匹配任何路径")