fix(audit): inferAuditFields 下标越界导致进程级 panic(4 段路径全部 500)

=== 现象(线上实际日志)===
    http: panic serving 172.19.16.138:35256: runtime error: index out of range [4] with length 4
    lokistack/middleware.inferAuditFields(...)
    lokistack/middleware/audit.go:179

=== 根因 ===
    segments := strings.Split(strings.Trim(path, "/"), "/")
    if len(segments) >= 4 {
        resource = segments[3]
        resourceID = segments[4]   // ← 4 段时越界
    }
/api/v1/hosts/2 正好拆成 4 段 [api v1 hosts 2],取 segments[4] 直接 panic。
影响面比想象大:**所有 4 段路径的写请求**(PUT/DELETE /hosts/:id、
/storage/:id、/clusters/:id 等)都 500,且中间件 panic 后审计也写不进去
—— 等于审计功能在最常用的更新/删除接口上完全失效。

=== 修法 ===
每个下标单独判断长度:
    if len(segments) >= 3 { module = segments[2] }
    if len(segments) >= 4 { resource = segments[3] }
    if len(segments) >= 5 { resourceID = segments[4] }

=== 回归测试(middleware/audit_test.go)===
  TestInferAuditFieldsNoPanic        : 0/1/2/3/4/5 段的 12 种路径 × 3 种方法,
                                       全部不得 panic(原 bug 的 4 段路径在列)
  TestInferAuditFieldsFourSegments   : 4 段时 module/resource 正确、resourceID 为空
  TestInferAuditFieldsFiveSegments   : 5 段时能解出 resourceID
  TestInferAuditFieldsSemanticActions: deploy/stop/install/uninstall/reset-password/
                                       validate/test 语义动作不被位置解析覆盖
  TestShouldAudit                    : 只读接口跳过、部署安装类必须记

=== 顺带修:CLI 模式下的两处误报 ===
1) backup 的 manifest.recordCount 恒为 0
   原因:CLI(CronJob 里跑 lokistack backup)没 init DB,GetDB() 返回 nil。
   改为 countRecords() 直接从备份出的字节数集合。
2) self-check 报「数据库: 未初始化」
   同上原因,误报会让巡检脚本以为库坏了。
   改为 CLI 模式直接解析数据文件统计(fileDBStats)。

=== 实测(修复后)===
  修复前:POST /api/v1/hosts/2/validate → panic,无审计记录
  修复后:POST /api/v1/hosts/2/validate → 200
          audit-logs total=5,含
            [hosts/validate] admin POST /api/v1/hosts/2/validate -> 200
            [auth/login]                  POST /api/v1/auth/login     -> 200
          panic serving 计数 = 0
  用户数据完好(initial-admin-password.txt 不存在,说明不是首次部署)

镜像:lghost/lokistack:latest + v0.1 = 2493a288758d
This commit is contained in:
opsx committed 2026-10-28 21:40:24 +00:00
1 parent c44ffad61f
commit b2c82da377
4 files changed
+179 -10

No files matched your search

+21 -9
View File
@@ -61,15 +61,11 @@ func CreateBackup(w io.Writer) (*BackupManifest, error) {
}
sum := sha256.Sum256(dbBytes)
db := repository.GetDB()
recordCount := 0
if db != nil {
db.RLock()
recordCount = len(db.Users) + len(db.Clusters) + len(db.Hosts) + len(db.Storages) +
len(db.Deployments) + len(db.Collectors) + len(db.LogSources) +
len(db.AlertRules) + len(db.NotifyChannels) + len(db.LogPolicyGroups) + len(db.AuditLogs)
db.RUnlock()
}
// 记录数从**备份出来的字节**里数,不用 repository.GetDB():
// CLI 模式(lokistack backup,跑在 CronJob 里)根本没初始化 DB,
// 用 GetDB() 会得到 nil,manifest 里的 recordCount 恒为 0,
// 恢复时就看不出"这份备份里有没有东西"。
recordCount := countRecords(dbBytes)
manifest := &BackupManifest{
Version: "2.0.0",
@@ -249,6 +245,22 @@ func ValidateAndStageBackup(r io.Reader) (*RestoreResult, error) {
return res, nil
}
// countRecords 从数据库 JSON 字节里统计各集合的记录总数。
func countRecords(dbBytes []byte) int {
var raw map[string]json.RawMessage
if err := json.Unmarshal(dbBytes, &raw); err != nil {
return 0
}
total := 0
for _, v := range raw {
var arr []json.RawMessage
if err := json.Unmarshal(v, &arr); err == nil {
total += len(arr)
}
}
return total
}
// encryptionKeyFingerprint 返回当前 ENCRYPTION_KEY 的指纹(sha256 前 16 位),不泄露密钥。
func encryptionKeyFingerprint() string {
key := os.Getenv("ENCRYPTION_KEY")