mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-23 02:53:31 +00:00
fix: SQL injection in field filters, document search ranking, item search ordering
- Sanitize field-filter keys from query params before interpolating into JSON path expressions — prevents SQL injection via crafted query parameter names (affects both SQLite and PostgreSQL) - Fix document search ORDER BY rank on PostgreSQL — the PG query path doesn't expose a `rank` column; use ts_rank() with DESC - Fix item search rank ordering in ListItems and SearchItems for PostgreSQL — ts_rank() needs DESC (higher = more relevant)
This commit is contained in:
@@ -97,7 +97,15 @@ func (s *Store) ListDocuments(workspaceID string, params models.DocumentListPara
|
||||
}
|
||||
|
||||
if params.Query != "" {
|
||||
query += fmt.Sprintf(" ORDER BY rank, d.%s %s", sortCol, order)
|
||||
if s.dialect.Driver() == DriverPostgres {
|
||||
// PostgreSQL ts_rank(): higher = more relevant → DESC
|
||||
ftsRank := s.dialect.FTSRank("d", "search_vector")
|
||||
query += fmt.Sprintf(" ORDER BY %s DESC, d.%s %s", ftsRank, sortCol, order)
|
||||
args = append(args, params.Query) // extra placeholder for ts_rank
|
||||
} else {
|
||||
// SQLite FTS5: rank is a hidden column on the FTS JOIN (ascending = better)
|
||||
query += fmt.Sprintf(" ORDER BY rank, d.%s %s", sortCol, order)
|
||||
}
|
||||
} else {
|
||||
query += fmt.Sprintf(" ORDER BY pinned DESC, %s %s", sortCol, order)
|
||||
}
|
||||
|
||||
+17
-2
@@ -421,6 +421,11 @@ func (s *Store) ListItems(workspaceID string, params models.ItemListParams) ([]m
|
||||
|
||||
// Field filters — supports comma-separated values as OR
|
||||
for key, value := range params.Fields {
|
||||
// Sanitize the key to prevent SQL injection — field names must be
|
||||
// alphanumeric/underscore only (user-controlled from query params).
|
||||
if !isValidFieldKey(key) {
|
||||
continue
|
||||
}
|
||||
jsonExpr := s.dialect.JSONExtractText("i.fields", key)
|
||||
if strings.Contains(value, ",") {
|
||||
values := strings.Split(value, ",")
|
||||
@@ -513,7 +518,13 @@ func (s *Store) listItemsFTS(workspaceID string, params models.ItemListParams) (
|
||||
args = append(args, params.CollectionSlug)
|
||||
}
|
||||
|
||||
query += " ORDER BY " + ftsRank
|
||||
// SQLite bm25(): more negative = more relevant → ASC (default).
|
||||
// PostgreSQL ts_rank(): higher = more relevant → DESC.
|
||||
if s.dialect.Driver() == DriverPostgres {
|
||||
query += " ORDER BY " + ftsRank + " DESC"
|
||||
} else {
|
||||
query += " ORDER BY " + ftsRank
|
||||
}
|
||||
|
||||
if params.Limit > 0 {
|
||||
query += " LIMIT ?"
|
||||
@@ -761,7 +772,11 @@ func (s *Store) SearchItems(workspaceID, query string) ([]ItemSearchResult, erro
|
||||
args = append(args, workspaceID)
|
||||
}
|
||||
|
||||
sqlQuery += " ORDER BY rank_score LIMIT 50"
|
||||
if s.dialect.Driver() == DriverPostgres {
|
||||
sqlQuery += " ORDER BY rank_score DESC LIMIT 50"
|
||||
} else {
|
||||
sqlQuery += " ORDER BY rank_score LIMIT 50"
|
||||
}
|
||||
|
||||
rows, err := s.db.Query(s.q(sqlQuery), args...)
|
||||
if err != nil {
|
||||
|
||||
@@ -332,6 +332,21 @@ func isAlpha(b byte) bool {
|
||||
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == '_'
|
||||
}
|
||||
|
||||
// isValidFieldKey checks that a field name contains only safe characters
|
||||
// (alphanumeric, underscore, hyphen). This prevents SQL injection when
|
||||
// field keys from user input are interpolated into JSON path expressions.
|
||||
func isValidFieldKey(key string) bool {
|
||||
if key == "" {
|
||||
return false
|
||||
}
|
||||
for _, c := range key {
|
||||
if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '-') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// q rebinds a query to the store's dialect (converts "?" to "$1", "$2", etc. for PostgreSQL).
|
||||
func (s *Store) q(query string) string {
|
||||
return s.dialect.Rebind(query)
|
||||
|
||||
Reference in New Issue
Block a user