From fc982db3ef56616e943d5540eccda8782f34c391 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Benouarets?= Date: Thu, 6 Nov 2025 19:34:48 +0100 Subject: [PATCH] feat(sql): Add common whitespace characters, ASCII, HTML, backslashes to literal escaping --- build/alter.go | 6 ++++++ build/create.go | 3 +-- build/delete.go | 3 +-- build/drop.go | 1 + build/insert.go | 2 +- build/truncate.go | 1 + build/update.go | 4 ++-- sql/validate.go | 37 +++++++++++++++++++++++++++---------- utils/string.go | 12 ++++++++++++ 9 files changed, 52 insertions(+), 17 deletions(-) create mode 100644 utils/string.go diff --git a/build/alter.go b/build/alter.go index 5be8792..7c609ab 100644 --- a/build/alter.go +++ b/build/alter.go @@ -69,6 +69,7 @@ func AlterTable(s *schema.Table) (*string, error) { } ddl := fmt.Sprintf(sql.DDL_ALTER_TABLE, sql.QuoteIdent(s.Name), strings.Join(ddlParts, " ")) + return &ddl, nil } @@ -91,6 +92,7 @@ func DropColumn(s *schema.Table, f *schema.Field) (*string, error) { return nil, err } ddl := fmt.Sprintf(sql.DDL_DROP_COLUMN, sql.QuoteIdent(s.Name), sql.QuoteIdent(f.Name)) + return &ddl, nil } @@ -129,6 +131,7 @@ func AddForeignKey(s *schema.Table, f *schema.Field) (*string, error) { return nil, err } ddl := fmt.Sprintf(sql.DDL_ADD_FOREIGN_KEY, sql.QuoteIdent(s.Name), sql.QuoteIdent(f.Name)) + return &ddl, nil } @@ -140,6 +143,7 @@ func DropForeignKey(s *schema.Table, f *schema.Field) (*string, error) { return nil, err } ddl := fmt.Sprintf(sql.DDL_DROP_FOREIGN_KEY, sql.QuoteIdent(s.Name), sql.QuoteIdent(f.Name)) + return &ddl, nil } @@ -151,6 +155,7 @@ func AddConstraint(s *schema.Table, f *schema.Field) (*string, error) { return nil, err } ddl := fmt.Sprintf(sql.DDL_ADD_CONSTRAINT, sql.QuoteIdent(s.Name), sql.QuoteIdent(f.Name)) + return &ddl, nil } @@ -162,5 +167,6 @@ func DropConstraint(s *schema.Table, f *schema.Field) (*string, error) { return nil, err } ddl := fmt.Sprintf(sql.DDL_DROP_CONSTRAINT, sql.QuoteIdent(s.Name), sql.QuoteIdent(f.Name)) + return &ddl, nil } diff --git a/build/create.go b/build/create.go index 4b2cc8d..2e0abc5 100644 --- a/build/create.go +++ b/build/create.go @@ -92,7 +92,7 @@ func CreateTable(s *schema.Table) (*string, error) { } if f.Comment != nil { - comments = append(comments, fmt.Sprintf("COMMENT ON COLUMN %s.%s IS '%s'", sql.QuoteIdent(s.Name), sql.QuoteIdent(f.Name), *f.Comment)) + comments = append(comments, fmt.Sprintf("COMMENT ON COLUMN %s.%s IS '%s';", sql.QuoteIdent(s.Name), sql.QuoteIdent(f.Name), *f.Comment)) } } @@ -105,6 +105,5 @@ func CreateTable(s *schema.Table) (*string, error) { ddl += "\n" + strings.Join(comments, "\n") } - log.Printf("DDL: %s", ddl) return &ddl, nil } diff --git a/build/delete.go b/build/delete.go index a1b98c0..90e95af 100644 --- a/build/delete.go +++ b/build/delete.go @@ -2,7 +2,6 @@ package build import ( "fmt" - "log" "git.secnex.io/secnex/pgson/schema" "git.secnex.io/secnex/pgson/sql" @@ -23,6 +22,6 @@ func Delete(s *schema.Table, where map[string]any) (*string, error) { } ddl := fmt.Sprintf(sql.DML_DELETE, sql.QuoteIdent(s.Name), whereClause) - log.Printf("DDL: %s", ddl) + return &ddl, nil } diff --git a/build/drop.go b/build/drop.go index 2422f24..11c4ba1 100644 --- a/build/drop.go +++ b/build/drop.go @@ -12,5 +12,6 @@ func DropTable(s *schema.Table) (*string, error) { return nil, err } ddl := fmt.Sprintf(sql.DDL_DROP_TABLE, sql.QuoteIdent(s.Name)) + return &ddl, nil } diff --git a/build/insert.go b/build/insert.go index 1d174f5..ab6d6ee 100644 --- a/build/insert.go +++ b/build/insert.go @@ -28,6 +28,6 @@ func Insert(s *schema.Table, data map[string]any) (*string, error) { values = append(values, sql.QuoteValue(data[k])) } ddl := fmt.Sprintf(sql.DML_INSERT_INTO, sql.QuoteIdent(s.Name), strings.Join(cols, ", "), strings.Join(values, ", ")) - log.Printf("DDL: %s", ddl) + return &ddl, nil } diff --git a/build/truncate.go b/build/truncate.go index 3ffab5e..4d0c4a8 100644 --- a/build/truncate.go +++ b/build/truncate.go @@ -12,5 +12,6 @@ func TruncateTable(s *schema.Table) (*string, error) { return nil, err } ddl := fmt.Sprintf(sql.DDL_TRUNCATE_TABLE, sql.QuoteIdent(s.Name)) + return &ddl, nil } diff --git a/build/update.go b/build/update.go index 8c438a0..decf85c 100644 --- a/build/update.go +++ b/build/update.go @@ -44,7 +44,7 @@ func Update(s *schema.Table, data map[string]any, where map[string]any) (*string } ddl := fmt.Sprintf(sql.DML_UPDATE, sql.QuoteIdent(s.Name), strings.Join(setParts, ", "), whereClause) - log.Printf("DDL: %s", ddl) + return &ddl, nil } @@ -59,6 +59,6 @@ func UpdateDeletedAt(s *schema.Table, where map[string]any) (*string, error) { } ddl := fmt.Sprintf(sql.DML_UPDATE, sql.QuoteIdent(s.Name), "deleted_at = CURRENT_TIMESTAMP", whereClause) - log.Printf("DDL: %s", ddl) + return &ddl, nil } diff --git a/sql/validate.go b/sql/validate.go index 08a8aaf..cec7197 100644 --- a/sql/validate.go +++ b/sql/validate.go @@ -120,14 +120,36 @@ func QuoteIdent(name string) string { return `"` + strings.ReplaceAll(name, `"`, `""`) + `"` } -// QuoteLiteral escapes a string literal for use in SQL queries -// It doubles single quotes to prevent SQL injection func QuoteLiteral(s string) string { - return "'" + strings.ReplaceAll(s, "'", "''") + "'" + s = strings.ReplaceAll(s, "\x00", "") + + var b strings.Builder + b.Grow(len(s) + 2) + b.WriteByte('\'') + + for _, r := range s { + switch r { + case '\'': + b.WriteString("''") + case '\\': + b.WriteString("\\\\") + case '\x00': + continue + case '\t', '\n', '\r': + b.WriteRune(r) + default: + if r < 0x20 { + b.WriteString(fmt.Sprintf("\\%03o", r)) + } else { + b.WriteRune(r) + } + } + } + + b.WriteByte('\'') + return b.String() } -// QuoteValue formats a value according to its type for use in SQL queries -// It handles strings, numbers, booleans, null, and UUIDs safely func QuoteValue(v any) string { if v == nil { return "NULL" @@ -135,7 +157,6 @@ func QuoteValue(v any) string { switch val := v.(type) { case string: - // Check if it's a valid UUID format if matched, _ := regexp.MatchString(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`, strings.ToLower(val)); matched { return QuoteLiteral(val) } @@ -150,14 +171,10 @@ func QuoteValue(v any) string { } return "FALSE" default: - // For unknown types, convert to string and escape return QuoteLiteral(fmt.Sprintf("%v", val)) } } -// BuildWhereClause safely builds a WHERE clause from conditions -// conditions is a map of column names to values (uses = operator) -// Returns empty string if no conditions provided func BuildWhereClause(conditions map[string]any) (string, error) { if len(conditions) == 0 { return "", nil diff --git a/utils/string.go b/utils/string.go new file mode 100644 index 0000000..39c3092 --- /dev/null +++ b/utils/string.go @@ -0,0 +1,12 @@ +package utils + +func String(s *string) string { + if s == nil { + return "" + } + return *s +} + +func Pointer(s string) *string { + return &s +} -- 2.49.1