mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-10-03 03:51:59 +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:
+73
-58
@@ -646,77 +646,92 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id st
|
||||
// This may need to be handled differently per database adapter
|
||||
}
|
||||
|
||||
// Apply filters - validate and adjust for column types first
|
||||
// Group consecutive OR filters together to prevent OR logic from escaping
|
||||
for i := 0; i < len(options.Filters); {
|
||||
filter := &options.Filters[i]
|
||||
// Client-controlled conditions (filters + x-custom-sql-w) are built by a closure so they can
|
||||
// be wrapped in a single group together with x-custom-sql-or: the OR then only widens the
|
||||
// client's own conditions and can never escape the server-side filters ANDed around it.
|
||||
applyUserConds := func(query common.SelectQuery) common.SelectQuery {
|
||||
// Apply filters - validate and adjust for column types first
|
||||
// Group consecutive OR filters together to prevent OR logic from escaping
|
||||
for i := 0; i < len(options.Filters); {
|
||||
filter := &options.Filters[i]
|
||||
|
||||
// Validate and adjust filter based on column type
|
||||
castInfo := h.ValidateAndAdjustFilterForColumnType(filter, model)
|
||||
// Validate and adjust filter based on column type
|
||||
castInfo := h.ValidateAndAdjustFilterForColumnType(filter, model)
|
||||
|
||||
// Default to AND if LogicOperator is not set
|
||||
logicOp := filter.LogicOperator
|
||||
if logicOp == "" {
|
||||
logicOp = "AND"
|
||||
}
|
||||
|
||||
// Check if this is the start of an OR group
|
||||
if logicOp == "OR" {
|
||||
// Collect all consecutive OR filters
|
||||
orFilters := []*common.FilterOption{filter}
|
||||
orCastInfo := []ColumnCastInfo{castInfo}
|
||||
|
||||
j := i + 1
|
||||
for j < len(options.Filters) {
|
||||
nextFilter := &options.Filters[j]
|
||||
nextLogicOp := nextFilter.LogicOperator
|
||||
if nextLogicOp == "" {
|
||||
nextLogicOp = "AND"
|
||||
}
|
||||
if nextLogicOp == "OR" {
|
||||
nextCastInfo := h.ValidateAndAdjustFilterForColumnType(nextFilter, model)
|
||||
orFilters = append(orFilters, nextFilter)
|
||||
orCastInfo = append(orCastInfo, nextCastInfo)
|
||||
j++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
// Default to AND if LogicOperator is not set
|
||||
logicOp := filter.LogicOperator
|
||||
if logicOp == "" {
|
||||
logicOp = "AND"
|
||||
}
|
||||
|
||||
// Apply the OR group as a single grouped condition
|
||||
logger.Debug("Applying OR filter group with %d conditions", len(orFilters))
|
||||
query = h.applyOrFilterGroup(query, orFilters, orCastInfo, tableName, model)
|
||||
i = j
|
||||
} else {
|
||||
// Single AND filter - apply normally
|
||||
logger.Debug("Applying filter: %s %s %v (needsCast=%v, logic=%s)", filter.Column, filter.Operator, filter.Value, castInfo.NeedsCast, logicOp)
|
||||
query = h.applyFilter(query, *filter, tableName, castInfo.NeedsCast, logicOp, model)
|
||||
i++
|
||||
// Check if this is the start of an OR group
|
||||
if logicOp == "OR" {
|
||||
// Collect all consecutive OR filters
|
||||
orFilters := []*common.FilterOption{filter}
|
||||
orCastInfo := []ColumnCastInfo{castInfo}
|
||||
|
||||
j := i + 1
|
||||
for j < len(options.Filters) {
|
||||
nextFilter := &options.Filters[j]
|
||||
nextLogicOp := nextFilter.LogicOperator
|
||||
if nextLogicOp == "" {
|
||||
nextLogicOp = "AND"
|
||||
}
|
||||
if nextLogicOp == "OR" {
|
||||
nextCastInfo := h.ValidateAndAdjustFilterForColumnType(nextFilter, model)
|
||||
orFilters = append(orFilters, nextFilter)
|
||||
orCastInfo = append(orCastInfo, nextCastInfo)
|
||||
j++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the OR group as a single grouped condition
|
||||
logger.Debug("Applying OR filter group with %d conditions", len(orFilters))
|
||||
query = h.applyOrFilterGroup(query, orFilters, orCastInfo, tableName, model)
|
||||
i = j
|
||||
} else {
|
||||
// Single AND filter - apply normally
|
||||
logger.Debug("Applying filter: %s %s %v (needsCast=%v, logic=%s)", filter.Column, filter.Operator, filter.Value, castInfo.NeedsCast, logicOp)
|
||||
query = h.applyFilter(query, *filter, tableName, castInfo.NeedsCast, logicOp, model)
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
// Apply custom SQL WHERE clause (AND condition)
|
||||
if options.CustomSQLWhere != "" {
|
||||
logger.Debug("Applying custom SQL WHERE: %s", options.CustomSQLWhere)
|
||||
// First add table prefixes to unqualified columns (but skip columns inside function calls)
|
||||
prefixedWhere := common.AddTablePrefixToColumns(options.CustomSQLWhere, reflection.ExtractTableNameOnly(tableName))
|
||||
// Then sanitize and allow preload table prefixes since custom SQL may reference multiple tables
|
||||
sanitizedWhere := common.SanitizeWhereClause(prefixedWhere, reflection.ExtractTableNameOnly(tableName), &options.RequestOptions)
|
||||
// Ensure outer parentheses to prevent OR logic from escaping
|
||||
sanitizedWhere = common.EnsureOuterParentheses(sanitizedWhere)
|
||||
if sanitizedWhere != "" {
|
||||
query = query.Where(sanitizedWhere)
|
||||
}
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
// Apply custom SQL WHERE clause (AND condition)
|
||||
if options.CustomSQLWhere != "" {
|
||||
logger.Debug("Applying custom SQL WHERE: %s", options.CustomSQLWhere)
|
||||
// First add table prefixes to unqualified columns (but skip columns inside function calls)
|
||||
prefixedWhere := common.AddTablePrefixToColumns(options.CustomSQLWhere, reflection.ExtractTableNameOnly(tableName))
|
||||
// Then sanitize and allow preload table prefixes since custom SQL may reference multiple tables
|
||||
sanitizedWhere := common.SanitizeWhereClause(prefixedWhere, reflection.ExtractTableNameOnly(tableName), &options.RequestOptions)
|
||||
// Ensure outer parentheses to prevent OR logic from escaping
|
||||
sanitizedWhere = common.EnsureOuterParentheses(sanitizedWhere)
|
||||
if sanitizedWhere != "" {
|
||||
query = query.Where(sanitizedWhere)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply custom SQL WHERE clause (OR condition)
|
||||
sanitizedOr := ""
|
||||
if options.CustomSQLOr != "" {
|
||||
logger.Debug("Applying custom SQL OR: %s", options.CustomSQLOr)
|
||||
customOr := common.AddTablePrefixToColumns(options.CustomSQLOr, reflection.ExtractTableNameOnly(tableName))
|
||||
// Sanitize and allow preload table prefixes since custom SQL may reference multiple tables
|
||||
sanitizedOr := common.SanitizeWhereClause(customOr, reflection.ExtractTableNameOnly(tableName), &options.RequestOptions)
|
||||
sanitizedOr = common.SanitizeWhereClause(customOr, reflection.ExtractTableNameOnly(tableName), &options.RequestOptions)
|
||||
// Ensure outer parentheses to prevent OR logic from escaping
|
||||
sanitizedOr = common.EnsureOuterParentheses(sanitizedOr)
|
||||
}
|
||||
|
||||
if grouper, ok := query.(common.WhereGrouper); ok && sanitizedOr != "" && common.Hardening().SQLStrict {
|
||||
query = grouper.WhereGroup(func(q common.SelectQuery) common.SelectQuery {
|
||||
return applyUserConds(q).WhereOr(sanitizedOr)
|
||||
})
|
||||
} else {
|
||||
query = applyUserConds(query)
|
||||
if sanitizedOr != "" {
|
||||
query = query.WhereOr(sanitizedOr)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user