Files
opsxdb/server/internal/db/danger.go
T

108 lines
3.2 KiB
Go

package db
import (
"regexp"
"strings"
)
// 危险语句规则:词边界匹配,作用于 StripComments 之后的纯代码文本。
// 词表:DROP/TRUNCATE/ALTER/GRANT/REVOKE/CREATE USER|ROLE/RENAME TABLE/
// REPLACE INTO/LOAD DATA/INTO OUTFILE|DUMPFILE/xp_cmdshell/COPY..PROGRAM/EXEC/CALL。
var dangerPatterns = []struct {
re *regexp.Regexp
desc string
}{
{regexp.MustCompile(`\bDROP\b`), "DROP"},
{regexp.MustCompile(`\bTRUNCATE\b`), "TRUNCATE"},
{regexp.MustCompile(`\bALTER\b`), "ALTER"},
{regexp.MustCompile(`\bGRANT\b`), "GRANT"},
{regexp.MustCompile(`\bREVOKE\b`), "REVOKE"},
{regexp.MustCompile(`\bCREATE\s+(USER|ROLE)\b`), "CREATE USER/ROLE"},
{regexp.MustCompile(`\bRENAME\s+TABLE\b`), "RENAME TABLE"},
{regexp.MustCompile(`\bREPLACE\s+INTO\b`), "REPLACE INTO"},
{regexp.MustCompile(`\bLOAD\s+DATA\b`), "LOAD DATA"},
{regexp.MustCompile(`\bINTO\s+(OUTFILE|DUMPFILE)\b`), "INTO OUTFILE/DUMPFILE"},
{regexp.MustCompile(`(?i)\bxp_cmdshell\b`), "xp_cmdshell"},
{regexp.MustCompile(`\bCOPY\b[^;]*\b(FROM|TO)\s+PROGRAM\b`), "COPY FROM/TO PROGRAM"},
}
var (
reWriteNoWhere = regexp.MustCompile(`\b(DELETE|UPDATE)\b`)
reWhere = regexp.MustCompile(`\bWHERE\b`)
reExecCallHead = regexp.MustCompile(`^(EXEC|EXECUTE|CALL)\b`)
)
// CheckDangerousSQL 服务端权威的危险语句判定(前端预检仅为 UX)。
// 逐语句检查;注释/字符串内的关键字不计;MySQL /*! */ 可执行注释内容参与检查;
// WITH 包裹的写操作因全文扫描 CTE 体同样命中。
// 返回命中的危险原因清单(去重)。
func CheckDangerousSQL(driver, sqlText string) []string {
_ = driver // 词表当前各驱动通用,保留参数便于后续按方言细化
seen := map[string]struct{}{}
var hits []string
add := func(desc, stmt string) {
if _, ok := seen[desc]; ok {
return
}
seen[desc] = struct{}{}
hits = append(hits, desc+": "+truncateSQL(stmt, 80))
}
for _, stmt := range SplitStatements(sqlText) {
code := strings.ToUpper(StripComments(stmt))
code = strings.TrimSpace(code)
if code == "" {
continue
}
for _, p := range dangerPatterns {
if p.re.MatchString(code) {
add(p.desc, stmt)
}
}
// DELETE/UPDATE 无 WHERE(含 WITH CTE 体内的写操作)
if reWriteNoWhere.MatchString(code) && !reWhere.MatchString(code) {
kw := reWriteNoWhere.FindString(code)
add(kw+" 无 WHERE", stmt)
}
// 存储过程/动态执行仅匹配语句开头,降低误报
if m := reExecCallHead.FindString(code); m != "" {
add(m, stmt)
}
}
return hits
}
// IsDangerousSQL 兼容旧调用:命中任意危险规则即 true。
func IsDangerousSQL(sql string) bool {
return len(CheckDangerousSQL("", sql)) > 0
}
func truncateSQL(s string, n int) string {
s = strings.Join(strings.Fields(s), " ")
if len(s) <= n {
return s
}
return s[:n] + "…"
}
func stripSQLLead(sql string) string {
s := strings.TrimSpace(sql)
for {
if strings.HasPrefix(s, "--") {
if i := strings.IndexByte(s, '\n'); i >= 0 {
s = strings.TrimSpace(s[i+1:])
continue
}
return ""
}
if strings.HasPrefix(s, "/*") {
if i := strings.Index(s, "*/"); i >= 0 {
s = strings.TrimSpace(s[i+2:])
continue
}
return ""
}
break
}
return s
}