mirror of
https://gitee.com/samwaf/SamWaf.git
synced 2026-08-31 01:41:39 +08:00
fix(security): structured whitelist for SQL query API (remove raw SQL)
- Replace raw SQL execution with a structured query (table/mode/columns/filters/top); backend no longer accepts any SQL string - Fail-closed table & column whitelist: block sensitive tables (auth/key/cert/config/plugin) and columns (password/secret/token/private/key) - Block EAV/key-value tables (value/params columns) to prevent plaintext secret leak, e.g. system_configs.value - Parameterize all filter values via GORM clause expressions; operator whitelist; cap filters/IN length - Add queryable-schema endpoint + query audit logging; harden GetTableInfo to skip sensitive tables/columns
This commit is contained in:
+38
-8
@@ -1,8 +1,10 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"SamWaf/common/zlog"
|
||||
"SamWaf/model/common/response"
|
||||
"SamWaf/model/request"
|
||||
"fmt"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -13,16 +15,29 @@ type WafSqlQueryApi struct {
|
||||
func (w *WafSqlQueryApi) ExecuteQueryApi(c *gin.Context) {
|
||||
var req request.WafSqlQueryReq
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err == nil {
|
||||
result, err := wafSqlQueryService.ExecuteQuery(req)
|
||||
if err != nil {
|
||||
response.FailWithMessage("查询失败: "+err.Error(), c)
|
||||
} else {
|
||||
response.OkWithDetailed(result, "查询成功", c)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
response.FailWithMessage("解析失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
// 审计上下文:操作者账号 / IP(由 auth 中间件写入)
|
||||
account := c.GetString("loginAccount")
|
||||
ip := c.GetString("loginIP")
|
||||
|
||||
result, err := wafSqlQueryService.ExecuteQuery(req)
|
||||
if err != nil {
|
||||
// 被拒的查询(含命中敏感表/列、非法运算符等)记 Warn,便于发现探测行为
|
||||
zlog.Warn("SqlQueryAudit", fmt.Sprintf(
|
||||
"rejected account=%s ip=%s db=%s table=%s mode=%s err=%s",
|
||||
account, ip, req.DbType, req.Table, req.Mode, err.Error()))
|
||||
response.FailWithMessage("查询失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
zlog.Info("SqlQueryAudit", fmt.Sprintf(
|
||||
"ok account=%s ip=%s db=%s table=%s mode=%s columns=%v top=%d rows=%d",
|
||||
account, ip, req.DbType, req.Table, result.Mode, req.Columns, req.Top, result.Total))
|
||||
response.OkWithDetailed(result, "查询成功", c)
|
||||
}
|
||||
|
||||
func (w *WafSqlQueryApi) GetTableInfoApi(c *gin.Context) {
|
||||
@@ -38,3 +53,18 @@ func (w *WafSqlQueryApi) GetTableInfoApi(c *gin.Context) {
|
||||
response.OkWithDetailed(result, "获取成功", c)
|
||||
}
|
||||
}
|
||||
|
||||
// GetQueryableSchemaApi 返回可查表及其可见列,供前端向导下拉使用(不含敏感表/敏感列)。
|
||||
func (w *WafSqlQueryApi) GetQueryableSchemaApi(c *gin.Context) {
|
||||
var req request.WafDbTableInfoReq
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
response.FailWithMessage("参数解析失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
result, err := wafSqlQueryService.GetQueryableSchema(req)
|
||||
if err != nil {
|
||||
response.FailWithMessage("获取可查询结构失败: "+err.Error(), c)
|
||||
} else {
|
||||
response.OkWithDetailed(result, "获取成功", c)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,24 @@
|
||||
package request
|
||||
|
||||
type WafSqlQueryReq struct {
|
||||
DbType string `json:"db_type" form:"db_type"` // 数据库类型:local, log, stats
|
||||
Sql string `json:"sql" form:"sql"` // SQL 查询语句
|
||||
Limit int `json:"limit" form:"limit"` // 返回记录数限制,默认 1000
|
||||
// WafSqlFilter 结构化查询条件(单条),多条之间以 AND 连接。
|
||||
// 安全约束(三者缺一不可):
|
||||
// - Column 必须精确命中目标表的「可见列白名单」(真实列 − 敏感列);
|
||||
// - Op 必须取自固定运算符白名单(见 service 层 allowedSqlOps);
|
||||
// - Value 一律通过 GORM 参数(?)绑定,绝不拼接进 SQL 文本。
|
||||
type WafSqlFilter struct {
|
||||
Column string `json:"column"` // 条件列(白名单校验)
|
||||
Op string `json:"op"` // 运算符:= != > >= < <= like in
|
||||
Value interface{} `json:"value"` // 绑定值(in 运算符时为数组)
|
||||
}
|
||||
|
||||
// WafSqlQueryReq 结构化数据查询请求。
|
||||
// 后端不再接收任何裸 SQL:表 / 列 / 运算符全部走服务端白名单校验后由 GORM 链式构造,
|
||||
// 值全部参数化绑定;前端也不提供 SQL 输入框。
|
||||
type WafSqlQueryReq struct {
|
||||
DbType string `json:"db_type" form:"db_type"` // 数据库:local, log, stats
|
||||
Table string `json:"table" form:"table"` // 目标表(单选,白名单校验)
|
||||
Mode string `json:"mode" form:"mode"` // list=查行 / count=计数,默认 list
|
||||
Columns []string `json:"columns" form:"columns"` // 查询列(可空;空=全部可见列,仅 list 生效)
|
||||
Filters []WafSqlFilter `json:"filters" form:"filters"` // 结构化条件(可空;为空即无 WHERE)
|
||||
Top int `json:"top" form:"top"` // 返回行数上限(仅 list,默认 1000、封顶 1000)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
package response
|
||||
|
||||
type WafSqlQueryResp struct {
|
||||
Columns []string `json:"columns"` // 列名列表
|
||||
Data []map[string]interface{} `json:"data"` // 数据行列表
|
||||
Total int `json:"total"` // 总记录数
|
||||
Mode string `json:"mode"` // 回显模式:list / count
|
||||
Columns []string `json:"columns"` // 列名列表(list 模式)
|
||||
Data []map[string]interface{} `json:"data"` // 数据行列表(list 模式)
|
||||
Total int64 `json:"total"` // list=返回行数;count=符合条件的总记录数
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package response
|
||||
|
||||
// QueryableColumn 可查询列(已剔除敏感列)。
|
||||
type QueryableColumn struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// QueryableTable 可查询表及其可见列。
|
||||
type QueryableTable struct {
|
||||
TableName string `json:"table_name"`
|
||||
Columns []QueryableColumn `json:"columns"`
|
||||
}
|
||||
|
||||
// WafSqlQueryableResp 「取可查表/列」接口响应,仅供前端向导下拉使用:
|
||||
// 不含敏感表、不含敏感列,也不返回行数/索引等额外结构信息。
|
||||
type WafSqlQueryableResp struct {
|
||||
DbType string `json:"db_type"`
|
||||
Tables []QueryableTable `json:"tables"`
|
||||
}
|
||||
@@ -13,4 +13,5 @@ func (receiver *SqlQueryRouter) InitSqlQueryRouter(group *gin.RouterGroup) {
|
||||
router := group.Group("")
|
||||
router.POST("/api/v1/sql_query/execute", api.ExecuteQueryApi)
|
||||
router.GET("/api/v1/sql_query/table_info", api.GetTableInfoApi)
|
||||
router.GET("/api/v1/sql_query/queryable", api.GetQueryableSchemaApi)
|
||||
}
|
||||
|
||||
@@ -7,31 +7,204 @@ import (
|
||||
"SamWaf/wafdb/dialect"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type WafSqlQueryService struct{}
|
||||
|
||||
var WafSqlQueryServiceApp = new(WafSqlQueryService)
|
||||
|
||||
// GetTableInfo 获取指定数据库的所有表结构信息(列、索引、行数、数据大小)
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// 白名单:结构化查询的安全边界。后端不接收任何裸 SQL,一切按下列规则 fail-closed。
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// sensitiveColumnSubstrings 敏感列名子串(小写包含匹配)。命中即从「可见列」中剔除:
|
||||
// 既不能被 SELECT、也不能出现在 filters 条件里(因此无法用布尔盲注逐字节套取)。
|
||||
// 说明:
|
||||
// - "key" 用于兜住 SslConfig.key_content、SslOrder.apply_key、oplatform_keys.api_key
|
||||
// 这类列名不含 private/secret 却是私钥/密钥的字段——宁可多藏(普通列被藏仅影响易用性),
|
||||
// 绝不能漏藏机密(漏一个就是私钥/口令外泄)。
|
||||
// - "private" 兜住 PrivateInfo.private_value(值不含 key)。
|
||||
//
|
||||
// 新增敏感字段命名时,如落在这些子串之外,需在此同步补充。
|
||||
var sensitiveColumnSubstrings = []string{
|
||||
"password", "passwd", "pwd", "secret", "token",
|
||||
"private", "salt", "key", "credential", "cipher",
|
||||
}
|
||||
|
||||
// sensitiveTableSubstrings 敏感表名子串(小写包含匹配)。命中即整表不可查、不可枚举、
|
||||
// 连行数/结构都不暴露——作为列级过滤之外的域级兜底(认证 / 密钥 / 证书相关表)。
|
||||
// 用子串而非精确表名,避免因 GORM 复数命名 / 前缀差异导致漏配。
|
||||
var sensitiveTableSubstrings = []string{
|
||||
"account", // accounts, account_pwd_histories(口令/历史口令指纹/访问码)
|
||||
"otp", // otps(2FA 密钥)
|
||||
"ssl", // sslconfigs, sslorders, sslexpires(证书私钥 key_content/apply_key/result_private_key)
|
||||
"private", // private_infos(私密信息)
|
||||
"oplatform", // oplatform_keys(API Key)
|
||||
"notify_channel", // notify_channels(渠道 secret / access_token)
|
||||
"notifychannel",
|
||||
"http_auth", // http_auth_base_configs(访问密码)
|
||||
"httpauth",
|
||||
"token", // 任何令牌表
|
||||
"secret", // 任何以 secret 命名的表
|
||||
"config", // system_configs / *config* 键值配置表(可能明文存密钥)
|
||||
"plugin", // waf_plugin_* 插件配置/日志(参数/值/IO 可能含凭证)
|
||||
}
|
||||
|
||||
// allowedSqlOps 结构化条件允许的运算符白名单。
|
||||
var allowedSqlOps = map[string]bool{
|
||||
"=": true, "!=": true, ">": true, ">=": true,
|
||||
"<": true, "<=": true, "like": true, "in": true,
|
||||
}
|
||||
|
||||
// 资源上限:防止超大结构化请求造成的内存/DB 资源消耗。
|
||||
const (
|
||||
maxSqlFilters = 50 // filters 条数上限
|
||||
maxSqlInValues = 500 // in 运算符数组元素上限
|
||||
)
|
||||
|
||||
func isSensitiveColumn(name string) bool {
|
||||
l := strings.ToLower(name)
|
||||
for _, s := range sensitiveColumnSubstrings {
|
||||
if strings.Contains(l, s) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isSensitiveTable(name string) bool {
|
||||
l := strings.ToLower(name)
|
||||
for _, s := range sensitiveTableSubstrings {
|
||||
if strings.Contains(l, s) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// hasOpaqueValueColumn 判断表是否为键值/EAV 形态(含名为 value/params 的通用值列)。
|
||||
// 这类表把任意(可能是机密的)数据塞进一个通用列,列名子串分类器无法甄别其内容,
|
||||
// 故整表按敏感处理。典型:system_configs.value 明文存 gpt_token / zerossl_eab_hmac_key /
|
||||
// debug_pwd 等密钥;waf_plugin_system_configs.value、waf_plugin_configs.params 同理。
|
||||
func hasOpaqueValueColumn(cols []dialect.ColumnMeta) bool {
|
||||
for _, c := range cols {
|
||||
switch strings.ToLower(c.Name) {
|
||||
case "value", "params":
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// pickDB 按类型返回目标库实例。
|
||||
func (receiver *WafSqlQueryService) pickDB(dbType string) (*gorm.DB, error) {
|
||||
switch dbType {
|
||||
case "local":
|
||||
return global.GWAF_LOCAL_DB, nil
|
||||
case "log":
|
||||
return global.GWAF_LOCAL_LOG_DB, nil
|
||||
case "stats":
|
||||
return global.GWAF_LOCAL_STATS_DB, nil
|
||||
default:
|
||||
return nil, errors.New("无效的数据库类型")
|
||||
}
|
||||
}
|
||||
|
||||
// resolveQueryableColumns 校验表可查并返回其「可见列」(真实列 − 敏感列)。
|
||||
// fail-closed:命中敏感表、表不存在(未精确命中真实表清单)、或无可见列时返回错误。
|
||||
// 先用 ListTables 精确匹配再取列信息,确保传入下游 schema 查询的一定是真实表名。
|
||||
func (receiver *WafSqlQueryService) resolveQueryableColumns(db *gorm.DB, table string) (string, []string, error) {
|
||||
table = strings.TrimSpace(table)
|
||||
if table == "" {
|
||||
return "", nil, errors.New("必须指定查询表")
|
||||
}
|
||||
if isSensitiveTable(table) {
|
||||
return "", nil, fmt.Errorf("表不可查询: %s", table)
|
||||
}
|
||||
tables, err := dialect.Get().ListTables(db)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("获取表清单失败: %w", err)
|
||||
}
|
||||
matched := ""
|
||||
for _, t := range tables {
|
||||
if t == table {
|
||||
matched = t
|
||||
break
|
||||
}
|
||||
}
|
||||
if matched == "" {
|
||||
return "", nil, fmt.Errorf("表不存在或不可查询: %s", table)
|
||||
}
|
||||
cols, err := dialect.Get().ColumnInfo(db, matched)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("获取列信息失败: %w", err)
|
||||
}
|
||||
// EAV/键值表整表 fail-closed(value/params 通用值列可能承载明文机密)
|
||||
if hasOpaqueValueColumn(cols) {
|
||||
return "", nil, fmt.Errorf("表不可查询(键值型表可能含机密): %s", matched)
|
||||
}
|
||||
var visible []string
|
||||
for _, c := range cols {
|
||||
if isSensitiveColumn(c.Name) {
|
||||
continue
|
||||
}
|
||||
visible = append(visible, c.Name)
|
||||
}
|
||||
if len(visible) == 0 {
|
||||
return "", nil, fmt.Errorf("表无可查询列: %s", matched)
|
||||
}
|
||||
return matched, visible, nil
|
||||
}
|
||||
|
||||
// buildSqlWhere 依据白名单运算符构造参数化 WHERE 表达式。
|
||||
// col 已由调用方校验为可见列;op 取自 allowedSqlOps;value 一律经 GORM ? 绑定,绝不拼接。
|
||||
func buildSqlWhere(col, op string, value interface{}) (clause.Expression, error) {
|
||||
column := clause.Column{Name: col}
|
||||
switch op {
|
||||
case "=":
|
||||
return clause.Eq{Column: column, Value: value}, nil
|
||||
case "!=":
|
||||
return clause.Neq{Column: column, Value: value}, nil
|
||||
case ">":
|
||||
return clause.Gt{Column: column, Value: value}, nil
|
||||
case ">=":
|
||||
return clause.Gte{Column: column, Value: value}, nil
|
||||
case "<":
|
||||
return clause.Lt{Column: column, Value: value}, nil
|
||||
case "<=":
|
||||
return clause.Lte{Column: column, Value: value}, nil
|
||||
case "like":
|
||||
return clause.Like{Column: column, Value: value}, nil
|
||||
case "in":
|
||||
vals, ok := value.([]interface{})
|
||||
if !ok {
|
||||
return nil, errors.New("in 条件的值必须是数组")
|
||||
}
|
||||
if len(vals) == 0 {
|
||||
return nil, errors.New("in 条件的值不能为空数组")
|
||||
}
|
||||
if len(vals) > maxSqlInValues {
|
||||
return nil, fmt.Errorf("in 条件元素过多(上限 %d)", maxSqlInValues)
|
||||
}
|
||||
return clause.IN{Column: column, Values: vals}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("不允许的运算符: %s", op)
|
||||
}
|
||||
}
|
||||
|
||||
// GetTableInfo 获取指定数据库的表结构信息(列、索引、行数)。
|
||||
// 已收敛到白名单:跳过敏感表、剔除敏感列,避免结构页泄露认证/密钥表的结构。
|
||||
func (receiver *WafSqlQueryService) GetTableInfo(req request.WafDbTableInfoReq) (response.WafDbTableInfoResp, error) {
|
||||
var result response.WafDbTableInfoResp
|
||||
result.DbType = req.DbType
|
||||
|
||||
var db *gorm.DB
|
||||
switch req.DbType {
|
||||
case "local":
|
||||
db = global.GWAF_LOCAL_DB
|
||||
case "log":
|
||||
db = global.GWAF_LOCAL_LOG_DB
|
||||
case "stats":
|
||||
db = global.GWAF_LOCAL_STATS_DB
|
||||
default:
|
||||
return result, errors.New("无效的数据库类型")
|
||||
db, err := receiver.pickDB(req.DbType)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
// 获取所有用户表
|
||||
@@ -41,14 +214,25 @@ func (receiver *WafSqlQueryService) GetTableInfo(req request.WafDbTableInfoReq)
|
||||
}
|
||||
|
||||
for _, tableName := range tableNames {
|
||||
// 敏感表(认证/密钥/证书域)不在结构页暴露
|
||||
if isSensitiveTable(tableName) {
|
||||
continue
|
||||
}
|
||||
tableInfo := response.TableInfo{
|
||||
TableName: tableName,
|
||||
}
|
||||
|
||||
// 获取字段信息
|
||||
// 获取字段信息(剔除敏感列,与可查询范围保持一致)
|
||||
cols, colErr := dialect.Get().ColumnInfo(db, tableName)
|
||||
// EAV/键值表整表跳过,不在结构页暴露
|
||||
if colErr == nil && hasOpaqueValueColumn(cols) {
|
||||
continue
|
||||
}
|
||||
if colErr == nil {
|
||||
for _, c := range cols {
|
||||
if isSensitiveColumn(c.Name) {
|
||||
continue
|
||||
}
|
||||
tableInfo.Columns = append(tableInfo.Columns, response.TableColumnInfo{
|
||||
Cid: c.Cid,
|
||||
Name: c.Name,
|
||||
@@ -95,95 +279,145 @@ func (receiver *WafSqlQueryService) GetTableInfo(req request.WafDbTableInfoReq)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (receiver *WafSqlQueryService) ExecuteQuery(req request.WafSqlQueryReq) (response.WafSqlQueryResp, error) {
|
||||
var result response.WafSqlQueryResp
|
||||
var db *gorm.DB
|
||||
// GetQueryableSchema 返回可查表及其可见列,供前端向导下拉使用(不含敏感表/敏感列)。
|
||||
func (receiver *WafSqlQueryService) GetQueryableSchema(req request.WafDbTableInfoReq) (response.WafSqlQueryableResp, error) {
|
||||
var result response.WafSqlQueryableResp
|
||||
result.DbType = req.DbType
|
||||
result.Tables = []response.QueryableTable{}
|
||||
|
||||
// 验证并选择数据库
|
||||
switch req.DbType {
|
||||
case "local":
|
||||
db = global.GWAF_LOCAL_DB
|
||||
case "log":
|
||||
db = global.GWAF_LOCAL_LOG_DB
|
||||
case "stats":
|
||||
db = global.GWAF_LOCAL_STATS_DB
|
||||
default:
|
||||
return result, errors.New("无效的数据库类型")
|
||||
}
|
||||
|
||||
// 验证 SQL 语句,只允许 SELECT 查询
|
||||
sqlLower := strings.ToLower(strings.TrimSpace(req.Sql))
|
||||
if !strings.HasPrefix(sqlLower, "select") {
|
||||
return result, errors.New("仅允许执行 SELECT 查询")
|
||||
}
|
||||
|
||||
// 检查是否包含危险操作(使用单词边界匹配,避免误判字段名如 create_time、update_time)
|
||||
dangerousKeywords := []string{
|
||||
`\bdrop\b`, `\bdelete\b`, `\bupdate\b`, `\binsert\b`,
|
||||
`\btruncate\b`, `\balter\b`, `\bcreate\b`, `\bexec\b`,
|
||||
`\bexecute\b`, `\bpragma\b`, `\battach\b`,
|
||||
}
|
||||
for _, pattern := range dangerousKeywords {
|
||||
matched, err := regexp.MatchString(pattern, sqlLower)
|
||||
if err == nil && matched {
|
||||
return result, errors.New("查询包含不允许的操作: " + pattern)
|
||||
}
|
||||
}
|
||||
|
||||
// 设置默认限制
|
||||
if req.Limit <= 0 || req.Limit > 1000 {
|
||||
req.Limit = 1000
|
||||
}
|
||||
|
||||
// 添加 LIMIT 限制
|
||||
sql := req.Sql
|
||||
if !strings.Contains(sqlLower, "limit") {
|
||||
sql = sql + fmt.Sprintf(" LIMIT %d", req.Limit)
|
||||
}
|
||||
|
||||
// 执行查询
|
||||
rows, err := db.Raw(sql).Rows()
|
||||
db, err := receiver.pickDB(req.DbType)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
// 获取列名
|
||||
columns, err := rows.Columns()
|
||||
tables, err := dialect.Get().ListTables(db)
|
||||
if err != nil {
|
||||
return result, err
|
||||
return result, fmt.Errorf("获取表清单失败: %w", err)
|
||||
}
|
||||
result.Columns = columns
|
||||
|
||||
// 读取数据
|
||||
result.Data = make([]map[string]interface{}, 0)
|
||||
for rows.Next() {
|
||||
// 创建一个切片来存储当前行的值
|
||||
values := make([]interface{}, len(columns))
|
||||
valuePtrs := make([]interface{}, len(columns))
|
||||
for i := range columns {
|
||||
valuePtrs[i] = &values[i]
|
||||
for _, t := range tables {
|
||||
if isSensitiveTable(t) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 扫描当前行
|
||||
if err := rows.Scan(valuePtrs...); err != nil {
|
||||
return result, err
|
||||
cols, colErr := dialect.Get().ColumnInfo(db, t)
|
||||
if colErr != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 构建 map
|
||||
rowMap := make(map[string]interface{})
|
||||
for i, col := range columns {
|
||||
val := values[i]
|
||||
// 处理 []byte 类型,转换为 string
|
||||
if b, ok := val.([]byte); ok {
|
||||
rowMap[col] = string(b)
|
||||
} else {
|
||||
rowMap[col] = val
|
||||
// EAV/键值表不暴露给向导下拉
|
||||
if hasOpaqueValueColumn(cols) {
|
||||
continue
|
||||
}
|
||||
qt := response.QueryableTable{TableName: t, Columns: []response.QueryableColumn{}}
|
||||
for _, c := range cols {
|
||||
if isSensitiveColumn(c.Name) {
|
||||
continue
|
||||
}
|
||||
qt.Columns = append(qt.Columns, response.QueryableColumn{Name: c.Name, Type: c.Type})
|
||||
}
|
||||
result.Data = append(result.Data, rowMap)
|
||||
if len(qt.Columns) == 0 {
|
||||
continue // 全部列敏感的表不暴露
|
||||
}
|
||||
result.Tables = append(result.Tables, qt)
|
||||
}
|
||||
|
||||
result.Total = len(result.Data)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ExecuteQuery 结构化数据查询:表/列/运算符全部白名单校验,值全部参数化,由 GORM 链式构造。
|
||||
// 支持 list(查行)与 count(计数)两种模式,天然兼容 SQLite/MySQL/SQLServer。
|
||||
func (receiver *WafSqlQueryService) ExecuteQuery(req request.WafSqlQueryReq) (response.WafSqlQueryResp, error) {
|
||||
var result response.WafSqlQueryResp
|
||||
|
||||
db, err := receiver.pickDB(req.DbType)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
// 模式校验
|
||||
mode := strings.ToLower(strings.TrimSpace(req.Mode))
|
||||
if mode == "" {
|
||||
mode = "list"
|
||||
}
|
||||
if mode != "list" && mode != "count" {
|
||||
return result, errors.New("无效的查询模式,仅支持 list 或 count")
|
||||
}
|
||||
result.Mode = mode
|
||||
result.Columns = []string{}
|
||||
result.Data = []map[string]interface{}{}
|
||||
|
||||
// 表 + 可见列白名单(fail-closed)
|
||||
table, visibleCols, err := receiver.resolveQueryableColumns(db, req.Table)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
colSet := make(map[string]bool, len(visibleCols))
|
||||
for _, c := range visibleCols {
|
||||
colSet[c] = true
|
||||
}
|
||||
|
||||
// 列白名单(仅 list 生效;为空取全部可见列,绝不 SELECT *)
|
||||
var selectCols []string
|
||||
if mode == "list" {
|
||||
if len(req.Columns) == 0 {
|
||||
selectCols = visibleCols
|
||||
} else {
|
||||
for _, c := range req.Columns {
|
||||
if !colSet[c] {
|
||||
return result, fmt.Errorf("列不可查询或不存在: %s", c)
|
||||
}
|
||||
selectCols = append(selectCols, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 结构化条件:列白名单 + op 白名单 + 值参数化
|
||||
if len(req.Filters) > maxSqlFilters {
|
||||
return result, fmt.Errorf("查询条件过多(上限 %d)", maxSqlFilters)
|
||||
}
|
||||
q := db.Table(table)
|
||||
for _, f := range req.Filters {
|
||||
if !colSet[f.Column] {
|
||||
return result, fmt.Errorf("条件列不可查询或不存在: %s", f.Column)
|
||||
}
|
||||
op := strings.ToLower(strings.TrimSpace(f.Op))
|
||||
if !allowedSqlOps[op] {
|
||||
return result, fmt.Errorf("不允许的运算符: %s", f.Op)
|
||||
}
|
||||
expr, buildErr := buildSqlWhere(f.Column, op, f.Value)
|
||||
if buildErr != nil {
|
||||
return result, buildErr
|
||||
}
|
||||
q = q.Where(expr)
|
||||
}
|
||||
|
||||
// count 模式
|
||||
if mode == "count" {
|
||||
var total int64
|
||||
if err := q.Count(&total).Error; err != nil {
|
||||
return result, err
|
||||
}
|
||||
result.Total = total
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// list 模式
|
||||
top := req.Top
|
||||
if top <= 0 || top > 1000 {
|
||||
top = 1000
|
||||
}
|
||||
data := make([]map[string]interface{}, 0)
|
||||
if err := q.Select(selectCols).Limit(top).Find(&data).Error; err != nil {
|
||||
return result, err
|
||||
}
|
||||
// []byte → string,便于前端展示(与旧行为一致)
|
||||
for _, row := range data {
|
||||
for k, v := range row {
|
||||
if b, ok := v.([]byte); ok {
|
||||
row[k] = string(b)
|
||||
}
|
||||
}
|
||||
}
|
||||
result.Columns = selectCols
|
||||
result.Data = data
|
||||
result.Total = int64(len(data))
|
||||
return result, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user