mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-10 17:43:15 +08:00
🔧 fix(db/kingbase_impl): 修复标识符无条件加双引号导致SQL语法报错
- quoteKingbaseIdent 改为条件引用,仅对大写字母、保留字、特殊字符的标识符添加双引号 - 新增 kingbaseIdentNeedsQuote 判断标识符是否需要引用 - 新增 isKingbaseReservedWord 检测常见SQL保留字 - 补充 TestQuoteKingbaseIdent、TestKingbaseIdentNeedsQuote 单测覆盖各场景 - refs #176
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -805,12 +806,56 @@ func normalizeKingbaseIdentifier(raw string) string {
|
||||
return value
|
||||
}
|
||||
|
||||
// kingbaseIdentNeedsQuote 判断标识符是否需要双引号包裹。
|
||||
// 与前端 sql.ts 中 needsQuote 逻辑保持一致。
|
||||
func kingbaseIdentNeedsQuote(ident string) bool {
|
||||
if ident == "" {
|
||||
return false
|
||||
}
|
||||
// 不是合法裸标识符格式(必须以字母或下划线开头,仅含字母、数字、下划线)
|
||||
if matched, _ := regexp.MatchString(`^[a-zA-Z_][a-zA-Z0-9_]*$`, ident); !matched {
|
||||
return true
|
||||
}
|
||||
// 包含大写字母时需要引号保护(KingbaseES/PostgreSQL 默认将未加引号的标识符折叠为小写)
|
||||
for _, r := range ident {
|
||||
if r >= 'A' && r <= 'Z' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// 是 SQL 保留字
|
||||
return isKingbaseReservedWord(ident)
|
||||
}
|
||||
|
||||
// isKingbaseReservedWord 检查是否为常见 SQL 保留字(简化版,与前端保持一致)。
|
||||
func isKingbaseReservedWord(ident string) bool {
|
||||
switch strings.ToLower(ident) {
|
||||
case "select", "from", "where", "table", "index", "user", "order", "group", "by",
|
||||
"limit", "offset", "and", "or", "not", "null", "true", "false", "key",
|
||||
"primary", "foreign", "references", "default", "constraint",
|
||||
"create", "drop", "alter", "insert", "update", "delete", "set", "values", "into",
|
||||
"join", "left", "right", "inner", "outer", "on", "as", "is", "in", "like",
|
||||
"between", "case", "when", "then", "else", "end", "having", "distinct",
|
||||
"all", "any", "exists", "union", "except", "intersect",
|
||||
"column", "check", "unique", "with", "grant", "revoke", "trigger",
|
||||
"begin", "commit", "rollback", "schema", "database", "view", "function",
|
||||
"procedure", "sequence", "type", "domain", "role", "session", "current",
|
||||
"authorization", "cross", "full", "natural", "some", "cast", "fetch",
|
||||
"for", "to", "do", "if", "return", "returns", "declare", "cursor":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func quoteKingbaseIdent(name string) string {
|
||||
n := normalizeKingbaseIdentifier(name)
|
||||
n = strings.ReplaceAll(n, `"`, `""`)
|
||||
if n == "" {
|
||||
return "\"\""
|
||||
}
|
||||
// 仅在需要时才加双引号,避免 KingbaseES 兼容性问题
|
||||
if !kingbaseIdentNeedsQuote(n) {
|
||||
return n
|
||||
}
|
||||
n = strings.ReplaceAll(n, `"`, `""`)
|
||||
return `"` + n + `"`
|
||||
}
|
||||
|
||||
|
||||
@@ -34,10 +34,25 @@ func TestQuoteKingbaseIdent(t *testing.T) {
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{name: "plain", in: "ldf_server", want: `"ldf_server"`},
|
||||
{name: "double quoted", in: `""ldf_server""`, want: `"ldf_server"`},
|
||||
{name: "escaped quoted", in: `\"ldf_server\"`, want: `"ldf_server"`},
|
||||
// 纯小写+下划线:不加引号
|
||||
{name: "plain lowercase", in: "ldf_server", want: "ldf_server"},
|
||||
{name: "plain lowercase 2", in: "bcs_barcode", want: "bcs_barcode"},
|
||||
{name: "double quoted input", in: `""ldf_server""`, want: "ldf_server"},
|
||||
{name: "escaped quoted input", in: `\"ldf_server\"`, want: "ldf_server"},
|
||||
// 含大写字母:加引号
|
||||
{name: "uppercase", in: "LDF_Server", want: `"LDF_Server"`},
|
||||
{name: "mixed case", in: "myTable", want: `"myTable"`},
|
||||
// SQL 保留字:加引号
|
||||
{name: "reserved word order", in: "order", want: `"order"`},
|
||||
{name: "reserved word user", in: "user", want: `"user"`},
|
||||
{name: "reserved word table", in: "table", want: `"table"`},
|
||||
{name: "reserved word select", in: "select", want: `"select"`},
|
||||
// 含特殊字符:加引号
|
||||
{name: "with hyphen", in: "my-table", want: `"my-table"`},
|
||||
{name: "with space", in: "my table", want: `"my table"`},
|
||||
{name: "with embedded quote", in: `ab"cd`, want: `"ab""cd"`},
|
||||
// 空值
|
||||
{name: "empty", in: "", want: `""`},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -49,6 +64,31 @@ func TestQuoteKingbaseIdent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestKingbaseIdentNeedsQuote(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
want bool
|
||||
}{
|
||||
{name: "plain lowercase", in: "ldf_server", want: false},
|
||||
{name: "starts with underscore", in: "_col", want: false},
|
||||
{name: "with digits", in: "col123", want: false},
|
||||
{name: "uppercase", in: "MyTable", want: true},
|
||||
{name: "reserved word", in: "order", want: true},
|
||||
{name: "with hyphen", in: "my-col", want: true},
|
||||
{name: "starts with digit", in: "123col", want: true},
|
||||
{name: "empty", in: "", want: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := kingbaseIdentNeedsQuote(tt.in); got != tt.want {
|
||||
t.Fatalf("kingbaseIdentNeedsQuote(%q) = %v, want %v", tt.in, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitKingbaseQualifiedTable(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -174,8 +175,31 @@ func (r *RedisClientImpl) toDisplayKey(key string) string {
|
||||
return strings.TrimPrefix(key, prefix)
|
||||
}
|
||||
|
||||
// sanitizeRedisPassword 对 Redis 密码进行防御性 URL 解码。
|
||||
// 当密码中包含 URL 编码序列(如 %40)时,尝试解码还原原始字符。
|
||||
// 这可以防止前端 URI 构建中 encodeURIComponent 编码后的密码被误传入。
|
||||
func sanitizeRedisPassword(password string) string {
|
||||
if password == "" {
|
||||
return password
|
||||
}
|
||||
// 仅当密码中包含 '%' 且后跟两位十六进制数字时,才尝试 URL 解码
|
||||
if !strings.Contains(password, "%") {
|
||||
return password
|
||||
}
|
||||
decoded, err := url.QueryUnescape(password)
|
||||
if err != nil {
|
||||
// 解码失败,使用原始密码
|
||||
return password
|
||||
}
|
||||
if decoded != password {
|
||||
logger.Warnf("Redis 密码检测到 URL 编码,已自动解码(原长度=%d 解码后长度=%d)", len(password), len(decoded))
|
||||
}
|
||||
return decoded
|
||||
}
|
||||
|
||||
// Connect establishes a connection to Redis
|
||||
func (r *RedisClientImpl) Connect(config connection.ConnectionConfig) error {
|
||||
config.Password = sanitizeRedisPassword(config.Password)
|
||||
r.config = config
|
||||
if r.config.RedisDB < 0 || r.config.RedisDB > 15 {
|
||||
r.config.RedisDB = 0
|
||||
|
||||
81
internal/redis/redis_impl_test.go
Normal file
81
internal/redis/redis_impl_test.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package redis
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSanitizeRedisPassword(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "empty password",
|
||||
input: "",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "plain password without special chars",
|
||||
input: "mypassword123",
|
||||
expected: "mypassword123",
|
||||
},
|
||||
{
|
||||
name: "password with @ not encoded",
|
||||
input: "p@ssword",
|
||||
expected: "p@ssword",
|
||||
},
|
||||
{
|
||||
name: "password with @ URL-encoded as %40",
|
||||
input: "p%40ssword",
|
||||
expected: "p@ssword",
|
||||
},
|
||||
{
|
||||
name: "password with multiple encoded chars",
|
||||
input: "p%40ss%23word",
|
||||
expected: "p@ss#word",
|
||||
},
|
||||
{
|
||||
name: "password with + encoded as %2B",
|
||||
input: "p%2Bss",
|
||||
expected: "p+ss",
|
||||
},
|
||||
{
|
||||
name: "password that is purely encoded",
|
||||
input: "%40%23%24",
|
||||
expected: "@#$",
|
||||
},
|
||||
{
|
||||
name: "password with invalid percent encoding",
|
||||
input: "p%ZZssword",
|
||||
expected: "p%ZZssword",
|
||||
},
|
||||
{
|
||||
name: "password with trailing percent",
|
||||
input: "password%",
|
||||
expected: "password%",
|
||||
},
|
||||
{
|
||||
name: "password with literal percent not encoding anything",
|
||||
input: "100%safe",
|
||||
expected: "100%safe",
|
||||
},
|
||||
{
|
||||
name: "password with space encoded as %20",
|
||||
input: "my%20pass",
|
||||
expected: "my pass",
|
||||
},
|
||||
{
|
||||
name: "complex password with mixed content",
|
||||
input: "P%40ss%23w0rd!",
|
||||
expected: "P@ss#w0rd!",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := sanitizeRedisPassword(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("sanitizeRedisPassword(%q) = %q, want %q", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user