-
Notifications
You must be signed in to change notification settings - Fork 0
fix: validate column before query #87
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ import ( | |
| "reflect" | ||
| "strings" | ||
|
|
||
| "github.com/tinh-tinh/tinhtinh/v2/dto/validator" | ||
| "gorm.io/gorm" | ||
| ) | ||
|
|
||
|
|
@@ -24,18 +25,27 @@ type QueryBuilder struct { | |
| } | ||
|
|
||
| func (q *QueryBuilder) Equal(column string, value interface{}) *QueryBuilder { | ||
| if !isValidColumn(column) { | ||
| return q | ||
| } | ||
| query := column + " = ?" | ||
| q.qb = q.qb.Where(query, value) | ||
| return q | ||
| } | ||
|
|
||
| func (q *QueryBuilder) Not(column string, args ...interface{}) *QueryBuilder { | ||
| if !isValidColumn(column) { | ||
| return q | ||
| } | ||
| query := column + " = ?" | ||
| q.qb = q.qb.Not(query, args...) | ||
| return q | ||
| } | ||
|
|
||
| func (q *QueryBuilder) Or(column string, args ...interface{}) *QueryBuilder { | ||
| if !isValidColumn(column) { | ||
| return q | ||
| } | ||
| query := column + " = ?" | ||
| q.qb = q.qb.Or(query, args...) | ||
| return q | ||
|
|
@@ -46,54 +56,81 @@ func (q *QueryBuilder) In(column string, values ...interface{}) *QueryBuilder { | |
| for i := range values { | ||
| placeholders[i] = "?" | ||
| } | ||
| if !isValidColumn(column) { | ||
| return q | ||
| } | ||
| query := column + " IN (" + strings.Join(placeholders, ", ") + ")" | ||
| q.qb = q.qb.Where(query, values...) | ||
| return q | ||
| } | ||
|
|
||
| func (q *QueryBuilder) MoreThan(column string, value interface{}) *QueryBuilder { | ||
| if !isValidColumn(column) { | ||
| return q | ||
| } | ||
| query := column + " > ?" | ||
| q.qb = q.qb.Where(query, value) | ||
| return q | ||
| } | ||
|
|
||
| func (q *QueryBuilder) MoreThanOrEqual(column string, value interface{}) *QueryBuilder { | ||
| if !isValidColumn(column) { | ||
| return q | ||
| } | ||
| query := column + " >= ?" | ||
| q.qb = q.qb.Where(query, value) | ||
| return q | ||
| } | ||
|
|
||
| func (q *QueryBuilder) LessThan(column string, value interface{}) *QueryBuilder { | ||
| if !isValidColumn(column) { | ||
| return q | ||
| } | ||
| query := column + " < ?" | ||
| q.qb = q.qb.Where(query, value) | ||
| return q | ||
| } | ||
|
|
||
| func (q *QueryBuilder) LessThanOrEqual(column string, value interface{}) *QueryBuilder { | ||
| if !isValidColumn(column) { | ||
| return q | ||
| } | ||
| query := column + " <= ?" | ||
| q.qb = q.qb.Where(query, value) | ||
| return q | ||
| } | ||
|
|
||
| func (q *QueryBuilder) Like(column string, value interface{}) *QueryBuilder { | ||
| if !isValidColumn(column) { | ||
| return q | ||
| } | ||
| query := column + " LIKE ?" | ||
| q.qb = q.qb.Where(query, value) | ||
| return q | ||
| } | ||
|
|
||
| func (q *QueryBuilder) ILike(column string, value interface{}) *QueryBuilder { | ||
| if !isValidColumn(column) { | ||
| return q | ||
| } | ||
| query := column + " ILIKE ?" | ||
| q.qb = q.qb.Where(query, value) | ||
| return q | ||
| } | ||
|
|
||
| func (q *QueryBuilder) Between(column string, start interface{}, end interface{}) *QueryBuilder { | ||
| if !isValidColumn(column) { | ||
| return q | ||
| } | ||
| query := column + " BETWEEN ? AND ?" | ||
| q.qb = q.qb.Where(query, start, end) | ||
| return q | ||
| } | ||
|
|
||
| func (q *QueryBuilder) NotEqual(column string, value interface{}) *QueryBuilder { | ||
| if !isValidColumn(column) { | ||
| return q | ||
| } | ||
| query := column + " <> ?" | ||
| q.qb = q.qb.Where(query, value) | ||
| return q | ||
|
|
@@ -104,12 +141,18 @@ func (q *QueryBuilder) NotIn(column string, values ...interface{}) *QueryBuilder | |
| for i := range values { | ||
| placeholders[i] = "?" | ||
| } | ||
| if !isValidColumn(column) { | ||
| return q | ||
| } | ||
| query := column + " NOT IN (" + strings.Join(placeholders, ", ") + ")" | ||
| q.qb = q.qb.Where(query, values...) | ||
| return q | ||
| } | ||
|
|
||
| func (q *QueryBuilder) IsNull(column string) *QueryBuilder { | ||
| if !isValidColumn(column) { | ||
| return q | ||
| } | ||
| query := column + " IS NULL" | ||
| q.qb = q.qb.Where(query) | ||
| return q | ||
|
|
@@ -119,3 +162,7 @@ func (q *QueryBuilder) Raw(sql string, values ...interface{}) *QueryBuilder { | |
| q.qb = q.qb.Raw(sql, values...) | ||
| return q | ||
| } | ||
|
|
||
| func isValidColumn(column string) bool { | ||
| return validator.IsAlphanumeric(column) | ||
| } | ||
|
Comment on lines
+166
to
+168
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Find the validator package and IsAlphanumeric implementation
rg -nP 'IsAlphanumeric' --type=go -B 2 -A 5Repository: tinh-tinh/sqlorm Length of output: 204 🏁 Script executed: #!/bin/bash
# Check if validator is in the imports and from where
rg -nP 'package\s+validator|func\s+IsAlphanumeric' --type=go -A 5Repository: tinh-tinh/sqlorm Length of output: 42 🏁 Script executed: #!/bin/bash
# Search for column usages with underscores in builder method calls
rg -nP '\.(Equal|Not|Or|In|MoreThan|LessThan|Like|ILike|Between|NotEqual|NotIn|IsNull)\s*\(\s*["\x60][a-zA-Z_][a-zA-Z0-9_]*' --type=goRepository: tinh-tinh/sqlorm Length of output: 744 🏁 Script executed: #!/bin/bash
# Check the import statement for the validator package
head -20 builder.go | grep -E 'import|validator'Repository: tinh-tinh/sqlorm Length of output: 118 🏁 Script executed: #!/bin/bash
# Search for any documentation, comments, or examples about column validation
rg -nP 'column|Column' --type=go builder.go -B 2 -A 2Repository: tinh-tinh/sqlorm Length of output: 3692 🏁 Script executed: #!/bin/bash
# Look for the validator package files in vendor or any local definitions
find . -type f -name "validator.go" -o -name "validator" -type dRepository: tinh-tinh/sqlorm Length of output: 42 🌐 Web query:
💡 Result: Do you mean the IsAlphanumeric validator inside the TinhTinh repo (github.com/tinh-tinh/tinhtinh) or the isAlphanumeric from validator.js (npm/validator)? I can look up the specific documentation — which one should I fetch? 🏁 Script executed: #!/bin/bash
# Check go.mod for validator package details
cat go.mod | grep -E 'tinhtinh|require'Repository: tinh-tinh/sqlorm Length of output: 120 🏁 Script executed: #!/bin/bash
# Check if there's any documentation in comments about column validation
rg -nP '//.*column|/\*.*column' --type=go -iRepository: tinh-tinh/sqlorm Length of output: 42 🌐 Web query:
💡 Result: I’m not finding a clear match for “TinhTinh” + Go validator. Do you mean a specific GitHub repo or package (e.g., govalidator / go-playground/validator), or do you want how an IsAlphanumeric validator should behave/be implemented for a DTO in Go? If you point me to the repo or confirm which you mean, I’ll fetch the code and explain the behavior and implementation. 🏁 Script executed: #!/bin/bash
# Try to find more about the validator package in go.sum
cat go.sum | grep tinhtinhRepository: tinh-tinh/sqlorm Length of output: 242 🌐 Web query:
💡 Result: I couldn't find a GitHub file matching "site:github.com tinh-tinh/tinhtinh dto validator IsAlphanumeric". The tinh-tinh organization and tinhtinh modules exist (Go packages) but no direct DTO validator/IsAlphanumeric hit under tinhtinh was returned by the search. [1][2] If you want, I can:
Sources: 🌐 Web query:
💡 Result: Short answer: no — in the common Go validator (go-playground/validator) the plain "alphanum" / IsAlphanumeric validator is ASCII-only; there is a separate "alphanumunicode" validator that accepts Unicode letters/digits. [1][2] Sources:
Critical: Standard Either expand the validation to allow underscores and other valid SQL identifier characters, or document why this restrictive validation is intentional. 🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -171,3 +171,239 @@ func Test_QueryBuilder(t *testing.T) { | |
| require.Equal(t, 1, len(docs)) | ||
| require.Equal(t, "test", docs[0].Name) | ||
| } | ||
|
|
||
| func Test_IsValidColumn(t *testing.T) { | ||
| require.NotPanics(t, func() { | ||
| createDatabaseForTest("test_valid_column") | ||
| }) | ||
| dsn := "host=localhost user=postgres password=postgres dbname=test_valid_column port=5432 sslmode=disable TimeZone=Asia/Shanghai" | ||
|
|
||
| db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ | ||
| Logger: logger.Default.LogMode(logger.Info), | ||
| }) | ||
| require.Nil(t, err) | ||
| db.Exec("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";") | ||
|
|
||
| type TestEntity struct { | ||
| gorm.Model | ||
| Name string `gorm:"type:varchar(255)"` | ||
| Value int `gorm:"type:int"` | ||
| } | ||
| err = db.AutoMigrate(&TestEntity{}) | ||
| require.Nil(t, err) | ||
|
|
||
| repo := sqlorm.Repository[TestEntity]{DB: db} | ||
|
|
||
| // Create test data | ||
| count, err := repo.Count(nil) | ||
| require.Nil(t, err) | ||
|
|
||
| if count == 0 { | ||
| _, err = repo.Create(&TestEntity{Name: "valid", Value: 1}) | ||
| require.Nil(t, err) | ||
| } | ||
|
|
||
| // Define invalid column names to test | ||
| invalidColumns := []string{ | ||
| "name; DROP TABLE users;--", | ||
| "column' OR '1'='1", | ||
| "col=1", | ||
| "column!", | ||
| "col@name", | ||
| "col#name", | ||
| "col$name", | ||
| "col%name", | ||
| "col^name", | ||
| "col&name", | ||
| "col*name", | ||
| "column()", | ||
| "[column]", | ||
| "<column>", | ||
| "col/name", | ||
| "col\\name", | ||
| "col|name", | ||
| "col`name", | ||
| "col~name", | ||
| "col+name", | ||
| "col name", | ||
| "col-name", | ||
| "col.name", | ||
| "col:name", | ||
| "col;name", | ||
| "col'name", | ||
| "col\"name", | ||
| "col{name}", | ||
| } | ||
|
|
||
| // Test Equal with invalid columns | ||
| t.Run("Equal_InvalidColumn", func(t *testing.T) { | ||
| for _, invalidCol := range invalidColumns { | ||
| docs, err := repo.FindAll(func(qb *sqlorm.QueryBuilder) { | ||
| qb.Equal(invalidCol, "test") | ||
| }) | ||
| require.Nil(t, err) | ||
| require.GreaterOrEqual(t, len(docs), 1, "Invalid column %q should not filter results", invalidCol) | ||
| } | ||
| }) | ||
|
|
||
| // Test Not with invalid columns | ||
| t.Run("Not_InvalidColumn", func(t *testing.T) { | ||
| for _, invalidCol := range invalidColumns { | ||
| docs, err := repo.FindAll(func(qb *sqlorm.QueryBuilder) { | ||
| qb.Not(invalidCol, "test") | ||
| }) | ||
| require.Nil(t, err) | ||
| require.GreaterOrEqual(t, len(docs), 1, "Invalid column %q should not filter results", invalidCol) | ||
| } | ||
| }) | ||
|
|
||
| // Test Or with invalid columns | ||
| t.Run("Or_InvalidColumn", func(t *testing.T) { | ||
| for _, invalidCol := range invalidColumns { | ||
| docs, err := repo.FindAll(func(qb *sqlorm.QueryBuilder) { | ||
| qb.Or(invalidCol, "test") | ||
| }) | ||
| require.Nil(t, err) | ||
| require.GreaterOrEqual(t, len(docs), 1, "Invalid column %q should not filter results", invalidCol) | ||
| } | ||
| }) | ||
|
|
||
| // Test In with invalid columns | ||
| t.Run("In_InvalidColumn", func(t *testing.T) { | ||
| for _, invalidCol := range invalidColumns { | ||
| docs, err := repo.FindAll(func(qb *sqlorm.QueryBuilder) { | ||
| qb.In(invalidCol, "test", "test2") | ||
| }) | ||
| require.Nil(t, err) | ||
| require.GreaterOrEqual(t, len(docs), 1, "Invalid column %q should not filter results", invalidCol) | ||
| } | ||
| }) | ||
|
|
||
| // Test MoreThan with invalid columns | ||
| t.Run("MoreThan_InvalidColumn", func(t *testing.T) { | ||
| for _, invalidCol := range invalidColumns { | ||
| docs, err := repo.FindAll(func(qb *sqlorm.QueryBuilder) { | ||
| qb.MoreThan(invalidCol, 0) | ||
| }) | ||
| require.Nil(t, err) | ||
| require.GreaterOrEqual(t, len(docs), 1, "Invalid column %q should not filter results", invalidCol) | ||
| } | ||
| }) | ||
|
|
||
| // Test MoreThanOrEqual with invalid columns | ||
| t.Run("MoreThanOrEqual_InvalidColumn", func(t *testing.T) { | ||
| for _, invalidCol := range invalidColumns { | ||
| docs, err := repo.FindAll(func(qb *sqlorm.QueryBuilder) { | ||
| qb.MoreThanOrEqual(invalidCol, 0) | ||
| }) | ||
| require.Nil(t, err) | ||
| require.GreaterOrEqual(t, len(docs), 1, "Invalid column %q should not filter results", invalidCol) | ||
| } | ||
| }) | ||
|
|
||
| // Test LessThan with invalid columns | ||
| t.Run("LessThan_InvalidColumn", func(t *testing.T) { | ||
| for _, invalidCol := range invalidColumns { | ||
| docs, err := repo.FindAll(func(qb *sqlorm.QueryBuilder) { | ||
| qb.LessThan(invalidCol, 100) | ||
| }) | ||
| require.Nil(t, err) | ||
| require.GreaterOrEqual(t, len(docs), 1, "Invalid column %q should not filter results", invalidCol) | ||
| } | ||
| }) | ||
|
|
||
| // Test LessThanOrEqual with invalid columns | ||
| t.Run("LessThanOrEqual_InvalidColumn", func(t *testing.T) { | ||
| for _, invalidCol := range invalidColumns { | ||
| docs, err := repo.FindAll(func(qb *sqlorm.QueryBuilder) { | ||
| qb.LessThanOrEqual(invalidCol, 100) | ||
| }) | ||
| require.Nil(t, err) | ||
| require.GreaterOrEqual(t, len(docs), 1, "Invalid column %q should not filter results", invalidCol) | ||
| } | ||
| }) | ||
|
|
||
| // Test Like with invalid columns | ||
| t.Run("Like_InvalidColumn", func(t *testing.T) { | ||
| for _, invalidCol := range invalidColumns { | ||
| docs, err := repo.FindAll(func(qb *sqlorm.QueryBuilder) { | ||
| qb.Like(invalidCol, "%test%") | ||
| }) | ||
| require.Nil(t, err) | ||
| require.GreaterOrEqual(t, len(docs), 1, "Invalid column %q should not filter results", invalidCol) | ||
| } | ||
| }) | ||
|
|
||
| // Test ILike with invalid columns | ||
| t.Run("ILike_InvalidColumn", func(t *testing.T) { | ||
| for _, invalidCol := range invalidColumns { | ||
| docs, err := repo.FindAll(func(qb *sqlorm.QueryBuilder) { | ||
| qb.ILike(invalidCol, "%TEST%") | ||
| }) | ||
| require.Nil(t, err) | ||
| require.GreaterOrEqual(t, len(docs), 1, "Invalid column %q should not filter results", invalidCol) | ||
| } | ||
| }) | ||
|
|
||
| // Test Between with invalid columns | ||
| t.Run("Between_InvalidColumn", func(t *testing.T) { | ||
| for _, invalidCol := range invalidColumns { | ||
| docs, err := repo.FindAll(func(qb *sqlorm.QueryBuilder) { | ||
| qb.Between(invalidCol, 0, 100) | ||
| }) | ||
| require.Nil(t, err) | ||
| require.GreaterOrEqual(t, len(docs), 1, "Invalid column %q should not filter results", invalidCol) | ||
| } | ||
| }) | ||
|
|
||
| // Test NotEqual with invalid columns | ||
| t.Run("NotEqual_InvalidColumn", func(t *testing.T) { | ||
| for _, invalidCol := range invalidColumns { | ||
| docs, err := repo.FindAll(func(qb *sqlorm.QueryBuilder) { | ||
| qb.NotEqual(invalidCol, "nonexistent") | ||
| }) | ||
| require.Nil(t, err) | ||
| require.GreaterOrEqual(t, len(docs), 1, "Invalid column %q should not filter results", invalidCol) | ||
| } | ||
| }) | ||
|
|
||
| // Test NotIn with invalid columns | ||
| t.Run("NotIn_InvalidColumn", func(t *testing.T) { | ||
| for _, invalidCol := range invalidColumns { | ||
| docs, err := repo.FindAll(func(qb *sqlorm.QueryBuilder) { | ||
| qb.NotIn(invalidCol, "nonexistent1", "nonexistent2") | ||
| }) | ||
| require.Nil(t, err) | ||
| require.GreaterOrEqual(t, len(docs), 1, "Invalid column %q should not filter results", invalidCol) | ||
| } | ||
| }) | ||
|
|
||
| // Test IsNull with invalid columns | ||
| t.Run("IsNull_InvalidColumn", func(t *testing.T) { | ||
| for _, invalidCol := range invalidColumns { | ||
| docs, err := repo.FindAll(func(qb *sqlorm.QueryBuilder) { | ||
| qb.IsNull(invalidCol) | ||
| }) | ||
| require.Nil(t, err) | ||
| require.GreaterOrEqual(t, len(docs), 1, "Invalid column %q should not filter results", invalidCol) | ||
| } | ||
| }) | ||
|
Comment on lines
+238
to
+390
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Strengthen the test assertions to verify filters are ignored. The current assertions use A more robust approach would be to compare against the total count in the database, proving that no filtering occurred: ♻️ Stronger assertion approachBefore the sub-tests, capture the expected count: + // Get total count for validation that filters are ignored
+ totalCount, err := repo.Count(nil)
+ require.Nil(t, err)
+
// Test Equal with invalid columns
t.Run("Equal_InvalidColumn", func(t *testing.T) {
for _, invalidCol := range invalidColumns {
docs, err := repo.FindAll(func(qb *sqlorm.QueryBuilder) {
qb.Equal(invalidCol, "test")
})
require.Nil(t, err)
- require.GreaterOrEqual(t, len(docs), 1, "Invalid column %q should not filter results", invalidCol)
+ require.Equal(t, totalCount, int64(len(docs)), "Invalid column %q should not filter results", invalidCol)
}
})Apply the same pattern to all other sub-tests.
|
||
|
|
||
| // Test valid column names still work | ||
| t.Run("ValidColumn_Equal", func(t *testing.T) { | ||
| docs, err := repo.FindAll(func(qb *sqlorm.QueryBuilder) { | ||
| qb.Equal("Name", "valid") | ||
| }) | ||
| require.Nil(t, err) | ||
| require.Equal(t, 1, len(docs)) | ||
| require.Equal(t, "valid", docs[0].Name) | ||
| }) | ||
|
|
||
| t.Run("ValidColumn_MoreThan", func(t *testing.T) { | ||
| docs, err := repo.FindAll(func(qb *sqlorm.QueryBuilder) { | ||
| qb.MoreThan("Value", 0) | ||
| }) | ||
| require.Nil(t, err) | ||
| require.Equal(t, 1, len(docs)) | ||
| }) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Major: Silent validation failure obscures errors and could cause incorrect query results.
When validation fails, the method returns the unchanged
QueryBuilderwithout any error, log, or indication. This means:Consider one of these approaches:
This comment applies to all 14 methods with validation guards.