diff --git a/.auto/log.jsonl b/.auto/log.jsonl index 27b7d23e..224fb429 100644 --- a/.auto/log.jsonl +++ b/.auto/log.jsonl @@ -44,3 +44,4 @@ {"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 变体,否则产生假缺口"}} +{"run":46,"commit":"2cb3392","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":106,"tsc_errors":0,"vitest_failed":0,"vitest_total":126},"status":"keep","description":"LIKE 过滤器转义修复:日志搜索含 %/_ 的输入不再被当通配符;pkg/util 新增 EscapeLike 共享助手 + 单测","timestamp":1787712116152,"segment":0,"confidence":null,"asi":{"hypothesis":"日志搜索 LIKE 过滤器不转义 %/_/\\,含下划线的路径/主机名搜索结果错误","next_action_hint":"同类遗留站点(upload/user/task_execution GORM 搜索)已记 ideas.md,可作后续轮次","result":"修复 4 个站点:analytics 两处 CH 过滤器 + logstore postgres_store 两处(PG/SQLite 加 ESCAPE '\\')。新增 pkg/util/like.go EscapeLike + 单测。metric 持平 8,全部测试通过","scope_decision":"GORM 实体搜索站(upload keyword、user username/email)同 bug 类但低风险且可能依赖现有通配语义,本轮不动"}} diff --git a/internal/repository/task_execution.go b/internal/repository/task_execution.go index 81284f2c..8c193677 100644 --- a/internal/repository/task_execution.go +++ b/internal/repository/task_execution.go @@ -16,6 +16,7 @@ import ( db "github.com/Rain-kl/Wavelet/internal/infra/persistence" "github.com/Rain-kl/Wavelet/internal/infra/persistence/idgen" "github.com/Rain-kl/Wavelet/internal/model" + "github.com/Rain-kl/Wavelet/pkg/util" ) const ( @@ -152,7 +153,7 @@ func ListTaskExecutions(ctx context.Context, req model.ListTaskExecutionsRequest } else if types := parseTaskTypesFilter(req.TaskTypes); len(types) > 0 { query = query.Where("task_type IN ?", types) } else if req.TaskTypePrefix != "" { - query = query.Where("task_type LIKE ?", req.TaskTypePrefix+"%") + query = query.Where("task_type LIKE ? ESCAPE '\\'", util.EscapeLike(req.TaskTypePrefix)+"%") } var total int64 diff --git a/internal/repository/upload.go b/internal/repository/upload.go index 3bad47e7..f366df7f 100644 --- a/internal/repository/upload.go +++ b/internal/repository/upload.go @@ -13,6 +13,7 @@ import ( db "github.com/Rain-kl/Wavelet/internal/infra/persistence" "github.com/Rain-kl/Wavelet/internal/model" + "github.com/Rain-kl/Wavelet/pkg/util" ) // UploadListFilter filters paginated upload queries. @@ -50,7 +51,7 @@ func ListUploads(ctx context.Context, filter UploadListFilter) (int64, []model.U query = query.Where("user_id = ?", filter.UserID) } if filter.Keyword != "" { - query = query.Where("LOWER(file_name) LIKE ?", "%"+strings.ToLower(filter.Keyword)+"%") + query = query.Where("LOWER(file_name) LIKE ? ESCAPE '\\'", "%"+util.EscapeLike(strings.ToLower(filter.Keyword))+"%") } if filter.Type != "" { query = query.Where("type = ?", filter.Type) diff --git a/internal/repository/user.go b/internal/repository/user.go index 1e54051b..6569acab 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -11,6 +11,7 @@ import ( db "github.com/Rain-kl/Wavelet/internal/infra/persistence" "github.com/Rain-kl/Wavelet/internal/infra/persistence/idgen" "github.com/Rain-kl/Wavelet/internal/model" + "github.com/Rain-kl/Wavelet/pkg/util" "gorm.io/gorm" ) @@ -70,10 +71,10 @@ func ListAdminUsers(ctx context.Context, filter AdminUserListFilter) (int64, []m query = query.Where("id = ?", *filter.UserID) } if filter.Username != "" { - query = query.Where("username LIKE ?", filter.Username+"%") + query = query.Where("username LIKE ? ESCAPE '\\'", util.EscapeLike(filter.Username)+"%") } if filter.Email != "" { - query = query.Where("email LIKE ?", filter.Email+"%") + query = query.Where("email LIKE ? ESCAPE '\\'", util.EscapeLike(filter.Email)+"%") } var total int64 @@ -185,7 +186,7 @@ func ListUserIDsByUsernameContains(ctx context.Context, username string) ([]uint } var userIDs []uint64 if err := db.DB(ctx).Model(&model.User{}). - Where("username LIKE ?", "%"+username+"%"). + Where("username LIKE ? ESCAPE '\\'", "%"+util.EscapeLike(username)+"%"). Pluck("id", &userIDs).Error; err != nil { return nil, err } @@ -225,7 +226,7 @@ func CreateUserFromOAuth(ctx context.Context, userOut *model.User, oauthInfo *mo func ListUsernamesMatchingBase(ctx context.Context, base string) ([]string, error) { var names []string if err := db.DB(ctx).Model(&model.User{}). - Where("username = ? OR username LIKE ?", base, base+"-%"). + Where("username = ? OR username LIKE ? ESCAPE '\\'", base, util.EscapeLike(base)+"-%"). Pluck("username", &names).Error; err != nil { return nil, err }