diff --git a/manager/backend/services/msg-etl/domain/repository/metricsrepo/repo.go b/manager/backend/services/msg-etl/domain/repository/metricsrepo/repo.go index 814d367bc13059f9c250f49f3c0c91cb4de38c22..460dbd2997532bd983aa40f83ec0afb99a25fccb 100644 --- a/manager/backend/services/msg-etl/domain/repository/metricsrepo/repo.go +++ b/manager/backend/services/msg-etl/domain/repository/metricsrepo/repo.go @@ -2,6 +2,7 @@ package metricsrepo import ( "fmt" + "regexp" "strings" "time" @@ -13,6 +14,44 @@ import ( "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/msg-etl/infrastructure/config" ) +// tagsExprDangerousPatterns matches SQL injection vectors that must never +// appear in a TagsExpr WHERE fragment. Compiled once at package init. +// +// IK49NZ: TagsExpr was previously concatenated directly into the WHERE +// clause without validation. Even though the upstream service layer +// runs exprConvert (Go AST parse), the repository must independently +// reject injection attempts as defense-in-depth. +var tagsExprDangerousPatterns = regexp.MustCompile( + `(?i)(\b(union|select|insert|delete|update|drop|alter|create|truncate|exec|execute|grant|revoke|merge|replace)\b|--|/\*|\*/|;|\bxp_|\bsp_)`, +) + +// tagsExprSafeChars enforces a strict character whitelist. After +// exprConvert, a valid tag expression only needs: +// - letters/digits/underscore (tag keys, values, and/or) +// - [ ] ' for tags['key'] access +// - ( ) for grouping +// - = ! < > for comparison operators +// - space and comma for separators +// - . - : / for common tag value content (e.g. versions, paths) +var tagsExprSafeChars = regexp.MustCompile(`^[a-zA-Z0-9_\s\[\]'"\(\)=!<>.,:/-]+$`) + +// validateTagsExpr sanitizes a TagsExpr before it is used as a raw WHERE +// fragment. It rejects SQL injection vectors and disallowed characters. +// Returns the trimmed expression if safe, or an error otherwise. +func validateTagsExpr(expr string) (string, error) { + expr = strings.TrimSpace(expr) + if expr == "" { + return "", fmt.Errorf("tags expr is empty") + } + if tagsExprDangerousPatterns.MatchString(expr) { + return "", fmt.Errorf("tags expr contains forbidden SQL keyword or injection pattern") + } + if !tagsExprSafeChars.MatchString(expr) { + return "", fmt.Errorf("tags expr contains disallowed characters") + } + return expr, nil +} + // MetricsRepository *metricsRepo 别名 type MetricsRepository = *metricsRepo @@ -154,7 +193,14 @@ func (repo *metricsRepo) ListMetricsDataPage(req *request.ListMetricsDataRequest tx = tx.Where("uuid = ?", req.Uuid) } if req.TagsExpr != "" { - tx = tx.Where(req.TagsExpr) + // IK49NZ: TagsExpr 来自用户输入,原先直接拼入 WHERE 子句存在 SQL 注入风险。 + // 上游 service 层的 exprConvert 已用 Go AST 解析做了一层防护,但仓储层 + // 仍需独立校验,防止绕过 service 直接调用仓储时注入任意 SQL。 + safeExpr, err := validateTagsExpr(req.TagsExpr) + if err != nil { + return nil, pkgerr.WrapStackError(err) + } + tx = tx.Where(safeExpr) } if req.StrValue != "" { tx = tx.Where("data like ?", fmt.Sprintf("%%%s%%", req.StrValue)) diff --git a/manager/backend/services/msg-etl/domain/repository/metricsrepo/repo_tagsexpr_test.go b/manager/backend/services/msg-etl/domain/repository/metricsrepo/repo_tagsexpr_test.go new file mode 100644 index 0000000000000000000000000000000000000000..8732ac8dbfefd4521a04fccd24940a19d575673e --- /dev/null +++ b/manager/backend/services/msg-etl/domain/repository/metricsrepo/repo_tagsexpr_test.go @@ -0,0 +1,58 @@ +package metricsrepo + +import ( + "testing" +) + +func TestValidateTagsExpr_SafeExpressions(t *testing.T) { + cases := []string{ + `tags['arch'] = 'x86_64'`, + `tags['arch'] = 'x86_64' and tags['os'] = 'linux'`, + `tags['arch'] != 'arm64' or tags['os'] = 'linux'`, + `(tags['arch'] = 'x86_64' or tags['arch'] = 'arm64') and tags['env'] = 'prod'`, + `tags['version'] = '1.2.3'`, + `tags['path'] = '/usr/local/bin'`, + } + for _, expr := range cases { + t.Run(expr, func(t *testing.T) { + got, err := validateTagsExpr(expr) + if err != nil { + t.Fatalf("expected safe, got error: %v", err) + } + if got == "" { + t.Error("expected non-empty result") + } + }) + } +} + +func TestValidateTagsExpr_RejectsInjection(t *testing.T) { + cases := []string{ + `1=1 UNION SELECT id,password FROM login_users --`, + `tags['k'] = 'v'; DROP TABLE metrics`, + `tags['k'] = 'v' OR 1=1 --`, + `/* comment */ tags['k'] = 'v'`, + `tags['k'] = 'v' UNION SELECT 1`, + `tags['k'] = 'v'; SELECT * FROM login_users`, + `exec tags['k'] = 'v'`, + } + for _, expr := range cases { + t.Run(expr, func(t *testing.T) { + _, err := validateTagsExpr(expr) + if err == nil { + t.Fatal("expected error for injection attempt, got nil") + } + }) + } +} + +func TestValidateTagsExpr_EmptyInput(t *testing.T) { + _, err := validateTagsExpr("") + if err == nil { + t.Fatal("expected error for empty input") + } + _, err = validateTagsExpr(" ") + if err == nil { + t.Fatal("expected error for whitespace-only input") + } +}