mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-10-04 04:21:57 +00:00
fix(common): harden CORS, sort, raw-SQL WHERE and x-custom-sql-or
Add a `hardening` config section (RESOLVESPEC_HARDENING_*) so each
fix can be switched off to restore the previous behaviour:
- cors_strict_origins: only reflect origins listed in
cors.allowed_origins / server URLs, with credentials; `*` never
sends credentials; fix shared-slice append of expose headers.
- sort_strict: join aliases must match `alias.identifier` (empty alias
no longer matches everything); sort expressions reject dangerous
functions/catalogs; cql* columns must be identifier-safe.
- sql_strict: client raw-SQL fragments must have balanced parens and
quotes, no comments/`;`/`$$`, DML keywords, dangerous functions or
system catalogs; a rejected fragment now fails closed ("(1=0)")
instead of dropping the filter. Subqueries stay allowed unless
sql_block_subqueries is set.
- x-custom-sql-or is grouped together with the client's own
conditions (new optional WhereGrouper, implemented for bun and gorm)
so it can no longer OR past server-side filters.
This commit is contained in:
@@ -148,6 +148,93 @@ func validateWhereClauseSecurity(where string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
reStrictDML = regexp.MustCompile(`(?i)\b(delete|update|truncate|drop|alter|create|insert|grant|revoke|exec|execute|copy|call|do|merge|vacuum|listen|notify|set|returning|into)\b`)
|
||||
// reStrictSubquery is only enforced when hardening.sql_block_subqueries is on.
|
||||
reStrictSubquery = regexp.MustCompile(`(?i)\b(select|union|lateral|with)\b`)
|
||||
// reStrictDangerousFunc matches functions/schemas that enable DoS or data
|
||||
// exfiltration through a WHERE fragment (sleep, file/large-object access,
|
||||
// dblink, config access, catalogs).
|
||||
reStrictDangerousFunc = regexp.MustCompile(`(?i)\b(pg_[a-z0-9_]*|lo_[a-z0-9_]*|dblink[a-z0-9_]*|set_config|current_setting|query_to_xml[a-z_]*|xpath[a-z_]*|generate_series|repeat|crypt|information_schema|sleep|benchmark)\b`)
|
||||
)
|
||||
|
||||
// stripSQLLiterals blanks out single-quoted literals (honouring ”) and
|
||||
// double-quoted identifiers so structural checks only see SQL syntax.
|
||||
// ok is false when a quote is left unterminated.
|
||||
func stripSQLLiterals(s string) (out string, ok bool) {
|
||||
var b strings.Builder
|
||||
for i := 0; i < len(s); i++ {
|
||||
ch := s[i]
|
||||
if ch != '\'' && ch != '"' {
|
||||
b.WriteByte(ch)
|
||||
continue
|
||||
}
|
||||
q := ch
|
||||
closed := false
|
||||
for i++; i < len(s); i++ {
|
||||
if s[i] == q {
|
||||
if i+1 < len(s) && s[i+1] == q { // escaped quote
|
||||
i++
|
||||
continue
|
||||
}
|
||||
closed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !closed {
|
||||
return "", false
|
||||
}
|
||||
b.WriteString("''")
|
||||
}
|
||||
return b.String(), true
|
||||
}
|
||||
|
||||
// validateWhereClauseStrict is the hardened check for client raw-SQL fragments
|
||||
// (hardening.sql_strict). It inspects syntax outside string literals: quotes
|
||||
// and parentheses must be balanced, and comments, statement separators,
|
||||
// dollar-quoting, DML keywords, dangerous functions and system catalogs are
|
||||
// rejected. Subqueries and ordinary functions stay allowed unless
|
||||
// hardening.sql_block_subqueries is set. isJoin (custom joins) still gets all
|
||||
// checks except the subquery block, since joins legitimately use subqueries.
|
||||
func validateWhereClauseStrict(where string, isJoin bool) error {
|
||||
stripped, ok := stripSQLLiterals(where)
|
||||
if !ok {
|
||||
return fmt.Errorf("unterminated quote")
|
||||
}
|
||||
for _, bad := range []string{"--", "/*", "*/", ";", "$$", "\\"} {
|
||||
if strings.Contains(stripped, bad) {
|
||||
return fmt.Errorf("forbidden token %q", bad)
|
||||
}
|
||||
}
|
||||
depth := 0
|
||||
for i := 0; i < len(stripped); i++ {
|
||||
switch stripped[i] {
|
||||
case '(':
|
||||
depth++
|
||||
case ')':
|
||||
depth--
|
||||
if depth < 0 {
|
||||
return fmt.Errorf("unbalanced parentheses")
|
||||
}
|
||||
}
|
||||
}
|
||||
if depth != 0 {
|
||||
return fmt.Errorf("unbalanced parentheses")
|
||||
}
|
||||
if m := reStrictDML.FindString(stripped); m != "" {
|
||||
return fmt.Errorf("forbidden keyword %q", strings.ToLower(m))
|
||||
}
|
||||
if m := reStrictDangerousFunc.FindString(stripped); m != "" {
|
||||
return fmt.Errorf("forbidden function or schema %q", strings.ToLower(m))
|
||||
}
|
||||
if !isJoin && Hardening().SQLBlockSubqueries {
|
||||
if m := reStrictSubquery.FindString(stripped); m != "" {
|
||||
return fmt.Errorf("subqueries not allowed (%q)", strings.ToLower(m))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SanitizeWhereClause removes trivial conditions and fixes incorrect table prefixes
|
||||
// This function should be used everywhere a WHERE statement is sent to ensure clean, efficient SQL
|
||||
//
|
||||
@@ -174,7 +261,14 @@ func SanitizeWhereClause(where string, tableName string, options ...*RequestOpti
|
||||
where = strings.TrimSpace(where)
|
||||
|
||||
// Validate that the WHERE clause doesn't contain dangerous SQL statements
|
||||
if err := validateWhereClauseSecurity(where); err != nil {
|
||||
if Hardening().SQLStrict {
|
||||
// Strict mode: fail closed. A rejected client fragment must not turn into
|
||||
// "no filter", so substitute a clause that matches no rows.
|
||||
if err := validateWhereClauseStrict(where, tableName == ""); err != nil {
|
||||
logger.Warn("Rejected client SQL fragment (%v): %s", err, where)
|
||||
return "(1=0)"
|
||||
}
|
||||
} else if err := validateWhereClauseSecurity(where); err != nil {
|
||||
logger.Debug("Security validation failed for WHERE clause: %v", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user