mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 05:46:36 +08:00
LIKE 过滤器转义修复:日志搜索含 %/_ 的输入不再被当通配符;pkg/util 新增 EscapeLike 共享助手 + 单测
Result: {"status":"keep","total_issues":8,"eslint_errors":0,"eslint_problems":0,"eslint_warnings":0,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_total":0,"golint_test_usetesting":0,"golint_total":8,"golint_usestdlibvars":0,"golint_vetx_total":0,"golint_wastedassign":0,"measure_s":106,"tsc_errors":0,"vitest_failed":0,"vitest_total":126}
This commit is contained in:
@@ -58,3 +58,22 @@
|
||||
- pkg/render/openresty 管理端旋钮(ClientMaxBodySize 等)原样插值:管理员权限范围内。
|
||||
- frontend/settings/profile.tsx(858 行)超 AGENTS.md ~600 行指引:存量组件,拆分属
|
||||
纯重构无质量增益,暂缓;若后续要改该页面功能时顺手拆 components/。
|
||||
|
||||
## Run #44(全仓 -race 扫描)
|
||||
|
||||
- 发现并修复 upload/cache 监听器 DATA RACE:goroutine 读可变全局 db.Redis vs
|
||||
testhelper 清理置 nil。根因修复=启动时捕获 redisClient(oauth×2/repository×2
|
||||
同型监听器一并加固),StopUploadMetaCacheListener 补 done 等待。
|
||||
- 教训:testhelper 不能 import upload/cache(循环依赖);"捕获替代全局读"是
|
||||
无环的根因修法。
|
||||
- 全仓 -race 现为 0 竞争(internal/... + pkg/...);建议周期性重跑。
|
||||
|
||||
## LIKE 转义(本轮已修日志搜索 4 站点;同类遗留)
|
||||
|
||||
- 已修:analytics/node_access_log_filter.go、analytics/access_log_filter.go、
|
||||
logstore/postgres_store.go×2(PG/SQLite 加 ESCAPE '\',CH 用默认反斜杠转义)。
|
||||
新助手 pkg/util/like.go EscapeLike + 单测。
|
||||
- 遗留同类(低风险,用户名/关键词搜索):repository/upload.go:53 keyword contains、
|
||||
repository/user.go:73/76/188 username/email 前缀+contains、task_execution.go:155
|
||||
task_type 前缀。user.go:228 `base+"-%"` 与 config_version.go:65 为系统生成模式,
|
||||
刻意通配勿动。GORM 站点加 ESCAPE 子句即可复用 EscapeLike。
|
||||
|
||||
@@ -42,3 +42,5 @@
|
||||
{"run":41,"commit":"efd8268","metric":8,"metrics":{"eslint_errors":0,"eslint_problems":0,"eslint_warnings":0,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_total":0,"golint_test_usetesting":0,"golint_total":8,"golint_usestdlibvars":0,"golint_vetx_total":0,"golint_wastedassign":0,"measure_s":75,"tsc_errors":0,"vitest_failed":0,"vitest_total":126},"status":"keep","description":"websocket 三 client 结构体去重:嵌入共享 wsClientCore(close/enqueue 单份实现)","timestamp":1787708975609,"segment":0,"confidence":null,"asi":{"hypothesis":"agentClient/relayClient/flaredClient 字段与 close/enqueue 完全相同,用组合(嵌入 wsClientCore)消除三份重复","next_action_hint":"代码库经 40 轮已高度收敛;后续可周期性跑 go test -race 全量","result":"metric 持平 8,全测试绿;净减 ~60 行重复代码","refactor":"新增 websocket/client_core.go:wsClientCore(nodeID/conn/send/done/once) + 共享 close/enqueue;三个 client 结构体改为嵌入"}}
|
||||
{"run":42,"commit":"ed1efd3","metric":8,"metrics":{"eslint_errors":0,"eslint_problems":0,"eslint_warnings":0,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_total":0,"golint_test_usetesting":0,"golint_total":8,"golint_usestdlibvars":0,"golint_vetx_total":0,"golint_wastedassign":0,"measure_s":77,"tsc_errors":0,"vitest_failed":0,"vitest_total":126},"status":"keep","description":"补 wsClientCore 并发测试 + close() 防 nil conn 守卫","timestamp":1787709222794,"segment":0,"confidence":null,"asi":{"hypothesis":"wsClientCore 并发语义(close 幂等、enqueue 不阻塞/关后拒绝)无测试覆盖","next_action_hint":"websocket 包已有基础并发测试;继续其他模块扫描","result":"metric 持平 8;测试还暴露 close 未防 nil conn 的防御缺口,已补守卫","refactor":"新增 websocket/client_core_test.go 3 个 -race 测试;client_core.go close() 增加 nil conn 守卫"}}
|
||||
{"run":43,"commit":"4f8e7e6","metric":8,"metrics":{"eslint_errors":0,"eslint_problems":0,"eslint_warnings":0,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_total":0,"golint_test_usetesting":0,"golint_total":8,"golint_usestdlibvars":0,"golint_vetx_total":0,"golint_wastedassign":0,"measure_s":77,"tsc_errors":0,"vitest_failed":0,"vitest_total":126},"status":"keep","description":"修复 frps/frpc TOML 配置注入:新增 protocol.TOMLQuote 并在两处配置渲染全部使用","timestamp":1787709693698,"segment":0,"confidence":null,"asi":{"hypothesis":"frps/frpc TOML 配置用裸 Fprintf 拼接,token/password/域名含引号、反斜杠、换行时会破坏配置或注入键","next_action_hint":"检查其他配置生成点是否有同类注入面(nginx/openresty 配置)","result":"metric 回到 8;frpc 慢套件 16.8s 全绿;mnd 曾短暂+1(Grow 魔法数),删除微优化后消除","security":"新增 pkg/protocol/toml.go TOMLQuote 转义助手 + toml_test.go;relay/frps renderConfig 与 flared/frpc buildFrpcToml 全部插值改为转义输出"}}
|
||||
{"run":44,"commit":"63007fc","metric":8,"metrics":{"eslint_errors":0,"eslint_problems":0,"eslint_warnings":0,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_total":0,"golint_test_usetesting":0,"golint_total":8,"golint_usestdlibvars":0,"golint_vetx_total":0,"golint_wastedassign":0,"measure_s":92,"tsc_errors":0,"vitest_failed":0,"vitest_total":126},"status":"keep","description":"全仓 race 扫描发现 upload/cache 监听器 DATA RACE:捕获 redis 客户端消除全局读竞争 + Stop 等待 done + 同型监听器(oauth×2/repository×2)加固","timestamp":1787711092906,"segment":0,"confidence":null,"asi":{"hypothesis":"全仓 go test -race 可能暴露并发 bug(此前仅局部验证)","next_action_hint":"继续扫其他模块;可考虑把 -race 纳入周期性检查","result":"发现并修复 1 个真实 DATA RACE;修复后全仓 -race 0 竞争,metric 持平 8","root_cause":"upload/cache 监听器 goroutine 读可变全局 db.Redis,与 testhelper 清理置 nil 竞争;testhelper 导入 upload/cache 有循环依赖,故用启动时捕获客户端的根因修复(oauth/repository 同型监听器一并加固),并补 StopUploadMetaCacheListener 同步等待 done"}}
|
||||
{"run":45,"commit":"63007fc","metric":8,"metrics":{"eslint_errors":0,"eslint_problems":0,"eslint_warnings":0,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_total":0,"golint_test_usetesting":0,"golint_total":8,"golint_usestdlibvars":0,"golint_vetx_total":0,"golint_wastedassign":0,"measure_s":70,"tsc_errors":0,"vitest_failed":0,"vitest_total":126},"status":"discard","description":"探索轮:索引对齐/前端请求瀑布/BasicAuth 注入面三假设均证伪,无代码变更","timestamp":1787711474404,"segment":0,"confidence":null,"asi":{"hypothesis":"SQLite 迁移缺 PG 同款索引;前端存在串行请求瀑布;nginx BasicAuth 密码有注入面","next_action_hint":"代码库已高度收敛;下轮可考虑 observability 查询构造器审计或周期性重跑 -race","rollback_reason":"纯探索无代码变更,无需回滚","result":"三个假设均无产出:①索引对比(修正提取正则后)PG/SQLite 完全对齐,SQLite 仅多 legacy w_* 冗余索引;②前端 await Service 均在事件处理器非渲染期;③BasicAuth 密码经 base64 编码(字母表无元字符)无注入面","lessons":"grep 提取 SQL 时注意 IF NOT EXISTS 变体,否则产生假缺口"}}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
|
||||
analyticsmodel "github.com/Rain-kl/Wavelet/internal/model/analytics"
|
||||
"github.com/Rain-kl/Wavelet/pkg/util"
|
||||
)
|
||||
|
||||
const userAccessLogFilterClauseCapacity = 4
|
||||
@@ -32,7 +33,7 @@ func buildUserAccessLogFilterClause(filter AccessLogFilter) (string, []any, bool
|
||||
}
|
||||
if trimmed := strings.TrimSpace(filter.Path); trimmed != "" {
|
||||
parts = append(parts, "path LIKE ?")
|
||||
args = append(args, "%"+trimmed+"%")
|
||||
args = append(args, "%"+util.EscapeLike(trimmed)+"%")
|
||||
}
|
||||
if filter.StartTime != nil {
|
||||
parts = append(parts, "created_at >= ?")
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
|
||||
analyticsmodel "github.com/Rain-kl/Wavelet/internal/model/analytics"
|
||||
"github.com/Rain-kl/Wavelet/pkg/util"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -37,7 +38,7 @@ func buildNodeAccessLogFilterClause(filter NodeAccessLogFilter) (string, []any)
|
||||
}
|
||||
if trimmed := normalizeNodeAccessLogRemoteAddr(filter.RemoteAddr); trimmed != "" {
|
||||
parts = append(parts, "remote_addr LIKE ?")
|
||||
args = append(args, trimmed+"%")
|
||||
args = append(args, util.EscapeLike(trimmed)+"%")
|
||||
}
|
||||
hosts := normalizeNodeAccessLogHosts(filter.Hosts)
|
||||
if len(hosts) > 0 {
|
||||
@@ -49,11 +50,11 @@ func buildNodeAccessLogFilterClause(filter NodeAccessLogFilter) (string, []any)
|
||||
parts = append(parts, "lowerUTF8(trim(host)) IN ("+strings.Join(placeholders, ", ")+")")
|
||||
} else if trimmed := strings.TrimSpace(filter.Host); trimmed != "" {
|
||||
parts = append(parts, "host LIKE ?")
|
||||
args = append(args, trimmed+"%")
|
||||
args = append(args, util.EscapeLike(trimmed)+"%")
|
||||
}
|
||||
if trimmed := strings.TrimSpace(filter.Path); trimmed != "" {
|
||||
parts = append(parts, "path LIKE ?")
|
||||
args = append(args, trimmed+"%")
|
||||
args = append(args, util.EscapeLike(trimmed)+"%")
|
||||
}
|
||||
if filter.StatusCode > 0 {
|
||||
parts = append(parts, "status_code = ?")
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/Rain-kl/Wavelet/internal/infra/persistence/idgen"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
analyticsmodel "github.com/Rain-kl/Wavelet/internal/model/analytics"
|
||||
"github.com/Rain-kl/Wavelet/pkg/util"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -823,20 +824,20 @@ func buildNodeAccessLogFilterParts(f analyticsmodel.NodeAccessLogFilter) (string
|
||||
args = append(args, nodeID)
|
||||
}
|
||||
if remoteAddr := strings.TrimSpace(f.RemoteAddr); remoteAddr != "" {
|
||||
parts = append(parts, "remote_addr LIKE ?")
|
||||
args = append(args, remoteAddr+"%")
|
||||
parts = append(parts, `remote_addr LIKE ? ESCAPE '\'`)
|
||||
args = append(args, util.EscapeLike(remoteAddr)+"%")
|
||||
}
|
||||
hosts := normalizeNodeAccessLogHosts(f.Hosts)
|
||||
if len(hosts) > 0 {
|
||||
parts = append(parts, "lower(trim(host)) IN ?")
|
||||
args = append(args, hosts)
|
||||
} else if host := strings.TrimSpace(f.Host); host != "" {
|
||||
parts = append(parts, "host LIKE ?")
|
||||
args = append(args, host+"%")
|
||||
parts = append(parts, `host LIKE ? ESCAPE '\'`)
|
||||
args = append(args, util.EscapeLike(host)+"%")
|
||||
}
|
||||
if path := strings.TrimSpace(f.Path); path != "" {
|
||||
parts = append(parts, "path LIKE ?")
|
||||
args = append(args, path+"%")
|
||||
parts = append(parts, `path LIKE ? ESCAPE '\'`)
|
||||
args = append(args, util.EscapeLike(path)+"%")
|
||||
}
|
||||
if f.StatusCode > 0 {
|
||||
parts = append(parts, "status_code = ?")
|
||||
@@ -1495,8 +1496,8 @@ func buildUserAccessLogWhere(filter analyticsmodel.AccessLogFilter) (string, []a
|
||||
args = append(args, filter.UserIDs)
|
||||
}
|
||||
if trimmed := strings.TrimSpace(filter.Path); trimmed != "" {
|
||||
parts = append(parts, "path LIKE ?")
|
||||
args = append(args, "%"+trimmed+"%")
|
||||
parts = append(parts, `path LIKE ? ESCAPE '\'`)
|
||||
args = append(args, "%"+util.EscapeLike(trimmed)+"%")
|
||||
}
|
||||
if filter.StartTime != nil {
|
||||
parts = append(parts, "created_at >= ?")
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package util
|
||||
|
||||
import "strings"
|
||||
|
||||
var likeEscaper = strings.NewReplacer(`\`, `\\`, `%`, `\%`, `_`, `\_`)
|
||||
|
||||
// EscapeLike escapes SQL LIKE metacharacters (\, %, _) so a user-supplied
|
||||
// value matches literally in LIKE patterns. Pair it with an explicit
|
||||
// `ESCAPE '\'` clause where the dialect has no backslash default (SQLite);
|
||||
// PostgreSQL and ClickHouse treat backslash as the default LIKE escape.
|
||||
func EscapeLike(value string) string {
|
||||
return likeEscaper.Replace(value)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package util
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestEscapeLike(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"": "",
|
||||
"/my_page": `/my\_page`,
|
||||
"100%": `100\%`,
|
||||
`a\b`: `a\\b`,
|
||||
`%_\`: `\%\_\\`,
|
||||
"normal/path": "normal/path",
|
||||
}
|
||||
for input, want := range cases {
|
||||
if got := EscapeLike(input); got != want {
|
||||
t.Errorf("EscapeLike(%q) = %q, want %q", input, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user