fix(security,data): P0 — 命令注入/停止毁数据/采集路径绕过/DB 清库/前端竞态

后端(实机 10.10.10.161 验证 host-binary 全链路 + 注入双路径 400):
- Loki 版本号严格 ^\d+.\d+.\d+$ 校验并接入 Update 路径:version 拼进 SSH shell 命令,
  旧校验放行 3.0.0'; id; echo ' 等注入载荷(root RCE)
- Stop/Restart 清理清单移除 PVC:停止部署曾连带删除 loki-data PVC,filesystem 存储日志被永久清空(测试锁死红线)
- 采集敏感路径防护覆盖默认 LogPath 分支与 collector Create(/etc/shadow 不再可绕过入库)
- 数据文件损坏时原文件改名 .corrupt-* 留档再空库启动,不再被静默覆盖(loadFromDisk 抽出可测)

前端:
- 审计页用户名过滤 300ms 防抖 + AbortController/版本号(逐键竞态数据错乱)
- 向导自动部署改用创建返回实例(或 id 最大且 pending),不再盲取 list[0] 对错误实例发起真实部署
- 路由级 ErrorBoundary:单页崩溃/chunk 404 只影响当前页
This commit is contained in:
opsx committed 2026-10-29 13:02:11 +08:00
1 parent 5e59347c40
commit 2975452c21
17 files changed
+271 -68

No files matched your search

+63 -43
View File
@@ -539,50 +539,25 @@ func (d *JsonDB) migrateSchema() bool {
return true
}
func InitDB() error {
cfg := config.GetConfig()
_ = os.MkdirAll(cfg.DataDir, 0700)
d := &JsonDB{
dataFile: cfg.DatabasePath,
Users: []models.User{},
Clusters: []models.Cluster{},
Hosts: []models.Host{},
Storages: []models.StorageConfig{},
Deployments: []models.LokiDeployment{},
Collectors: []models.Collector{},
Logs: []models.DeploymentLog{},
Settings: []models.SystemSetting{},
LogSources: []models.LogSource{},
AlertRules: []models.AlertRule{},
AlertDeliveries: []models.AlertDelivery{},
NotifyChannels: []models.NotifyChannel{},
LogPolicyGroups: []models.LogPolicyGroup{},
AuditLogs: []models.AuditLog{},
DailyVolumeSnapshots: []models.DailyVolumeSnapshot{},
SchemaVersion: CurrentSchemaVersion,
sequences: map[string]uint{
"users": 0,
"clusters": 0,
"hosts": 0,
"storages": 0,
"deployments": 0,
"collectors": 0,
"logs": 0,
"logPolicyGroups": 0,
"logSources": 0,
"notifyChannels": 0,
"alertRules": 0,
"alertDeliveries": 0,
"auditLogs": 0,
"dailyVolumeSnapshots": 0,
},
saveChan: make(chan saveRequest, 10),
stopChan: make(chan struct{}),
}
// loadFromDisk 读取并解析数据文件,重建 ID 序列;文件损坏时备份留档绝不覆盖。
func (d *JsonDB) loadFromDisk() {
if data, err := os.ReadFile(d.dataFile); err == nil {
if err := json.Unmarshal(data, d); err == nil {
if err := json.Unmarshal(data, d); err != nil {
// 损坏的数据文件可能已被部分填充,绝不能让随后的空库落盘覆盖原文件:
// 先把原文件改名留档,再清回空库继续启动(seed 会重建 admin)。
// 改名失败(权限/只读)时宁可退出,也不销毁可能可恢复的数据。
backup := fmt.Sprintf("%s.corrupt-%s", d.dataFile, time.Now().Format("20060102-150405"))
if renameErr := os.Rename(d.dataFile, backup); renameErr != nil {
log.Fatalf("[db] 数据文件 %s 解析失败(%v)且无法备份(%v),为避免覆盖可恢复数据进程退出,请人工处理", d.dataFile, err, renameErr)
}
log.Printf("[ERROR] [db] 数据文件 %s 解析失败(%v),原文件已备份为 %s,以空库继续启动", d.dataFile, err, backup)
d.Users, d.Clusters, d.Hosts, d.Storages = nil, nil, nil, nil
d.Deployments, d.Collectors, d.Logs, d.Settings = nil, nil, nil, nil
d.LogSources, d.AlertRules, d.AlertDeliveries = nil, nil, nil
d.NotifyChannels, d.LogPolicyGroups, d.AuditLogs = nil, nil, nil
d.DailyVolumeSnapshots = nil
d.SchemaVersion = 0
} else {
d.sequences = map[string]uint{}
for _, u := range d.Users {
if u.ID > d.sequences["users"] {
@@ -661,6 +636,51 @@ func InitDB() error {
}
}
}
func InitDB() error {
cfg := config.GetConfig()
_ = os.MkdirAll(cfg.DataDir, 0700)
d := &JsonDB{
dataFile: cfg.DatabasePath,
Users: []models.User{},
Clusters: []models.Cluster{},
Hosts: []models.Host{},
Storages: []models.StorageConfig{},
Deployments: []models.LokiDeployment{},
Collectors: []models.Collector{},
Logs: []models.DeploymentLog{},
Settings: []models.SystemSetting{},
LogSources: []models.LogSource{},
AlertRules: []models.AlertRule{},
AlertDeliveries: []models.AlertDelivery{},
NotifyChannels: []models.NotifyChannel{},
LogPolicyGroups: []models.LogPolicyGroup{},
AuditLogs: []models.AuditLog{},
DailyVolumeSnapshots: []models.DailyVolumeSnapshot{},
SchemaVersion: CurrentSchemaVersion,
sequences: map[string]uint{
"users": 0,
"clusters": 0,
"hosts": 0,
"storages": 0,
"deployments": 0,
"collectors": 0,
"logs": 0,
"logPolicyGroups": 0,
"logSources": 0,
"notifyChannels": 0,
"alertRules": 0,
"alertDeliveries": 0,
"auditLogs": 0,
"dailyVolumeSnapshots": 0,
},
saveChan: make(chan saveRequest, 10),
stopChan: make(chan struct{}),
}
d.loadFromDisk()
migratedLogAccess := migrateLogAccessToGroups(d)
// 结构版本迁移(审计表、采集项等新字段的兼容处理)
migratedSchema := d.migrateSchema()
+42
View File
@@ -0,0 +1,42 @@
package repository
import (
"os"
"path/filepath"
"strings"
"testing"
)
// TestInitDBCorruptedFileIsNotOverwritten 数据文件损坏时,原内容必须以 .corrupt-* 备份留档,
// 不允许被随后的空库落盘覆盖(可恢复数据销毁 = P0)。
func TestInitDBCorruptedFileIsNotOverwritten(t *testing.T) {
dir := t.TempDir()
dataFile := filepath.Join(dir, "lokistack.json")
corrupt := "{not valid json"
if err := os.WriteFile(dataFile, []byte(corrupt), 0o600); err != nil {
t.Fatal(err)
}
d := &JsonDB{dataFile: dataFile}
d.loadFromDisk()
// 模拟启动后任意一次落盘:损坏原文若未被移走,这里就会覆盖它
if err := d.doSaveInner(); err != nil {
t.Fatalf("空库落盘失败: %v", err)
}
entries, err := os.ReadDir(dir)
if err != nil {
t.Fatal(err)
}
backedUp := false
for _, e := range entries {
if strings.HasPrefix(e.Name(), "lokistack.json.corrupt-") {
content, readErr := os.ReadFile(filepath.Join(dir, e.Name()))
if readErr == nil && string(content) == corrupt {
backedUp = true
}
}
}
if !backedUp {
t.Fatalf("损坏原文未被完整备份留档: %v", entries)
}
}