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:
1 parent
5e59347c40
commit
2975452c21
17 files changed
+271
-68
No files matched your search
+63
-43
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user