mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-10-05 04:51:58 +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:
+85
-12
@@ -20,7 +20,15 @@ func DefaultCORSConfig() CORSConfig {
|
||||
configManager := config.GetConfigManager()
|
||||
cfg, _ := configManager.GetConfig()
|
||||
hosts := make([]string, 0)
|
||||
// hosts = append(hosts, "*")
|
||||
if cfg == nil {
|
||||
return CORSConfig{
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
|
||||
AllowedHeaders: GetHeadSpecHeaders(),
|
||||
MaxAge: 86400,
|
||||
}
|
||||
}
|
||||
// Explicitly configured origins (cors.allowed_origins); "*" allows any origin without credentials
|
||||
hosts = append(hosts, cfg.CORS.AllowedOrigins...)
|
||||
|
||||
_, _, ipsList := config.GetIPs()
|
||||
|
||||
@@ -113,25 +121,63 @@ func GetHeadSpecHeaders() []string {
|
||||
}
|
||||
}
|
||||
|
||||
// SetCORSHeaders sets CORS headers on a response writer
|
||||
// originAllowed reports whether origin matches config.AllowedOrigins exactly
|
||||
// (case-insensitive, trailing slash ignored). wildcard is true when the list
|
||||
// contains "*".
|
||||
func originAllowed(origin string, allowed []string) (ok bool, wildcard bool) {
|
||||
norm := strings.ToLower(strings.TrimRight(strings.TrimSpace(origin), "/"))
|
||||
for _, a := range allowed {
|
||||
a = strings.ToLower(strings.TrimRight(strings.TrimSpace(a), "/"))
|
||||
if a == "*" {
|
||||
wildcard = true
|
||||
continue
|
||||
}
|
||||
if a != "" && a == norm {
|
||||
return true, wildcard
|
||||
}
|
||||
}
|
||||
return false, wildcard
|
||||
}
|
||||
|
||||
// SetCORSHeaders sets CORS headers on a response writer.
|
||||
//
|
||||
// The request Origin is only reflected (and credentials only allowed) when it
|
||||
// is listed in config.AllowedOrigins. A "*" entry allows any origin but never
|
||||
// with credentials. Unlisted origins get no CORS headers, so browsers block
|
||||
// the cross-origin read.
|
||||
func SetCORSHeaders(w ResponseWriter, r Request, config CORSConfig) {
|
||||
// Reflect the request origin; fall back to wildcard only when no origin is present
|
||||
if !Hardening().CORSStrictOrigins {
|
||||
setCORSHeadersLegacy(w, r, config)
|
||||
return
|
||||
}
|
||||
origin := r.Header("Origin")
|
||||
if origin == "" {
|
||||
origin = "*"
|
||||
// Not a cross-origin browser request; nothing to protect.
|
||||
w.SetHeader("Access-Control-Allow-Origin", "*")
|
||||
} else {
|
||||
// Vary must be set so caches don't serve one origin's response to another
|
||||
httpW := w.UnderlyingResponseWriter()
|
||||
httpW.Header().Set("Vary", "Origin")
|
||||
w.UnderlyingResponseWriter().Header().Set("Vary", "Origin")
|
||||
|
||||
ok, wildcard := originAllowed(origin, config.AllowedOrigins)
|
||||
switch {
|
||||
case ok:
|
||||
w.SetHeader("Access-Control-Allow-Origin", origin)
|
||||
w.SetHeader("Access-Control-Allow-Credentials", "true")
|
||||
case wildcard:
|
||||
w.SetHeader("Access-Control-Allow-Origin", "*")
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
w.SetHeader("Access-Control-Allow-Origin", origin)
|
||||
|
||||
// Set allowed methods
|
||||
if len(config.AllowedMethods) > 0 {
|
||||
w.SetHeader("Access-Control-Allow-Methods", strings.Join(config.AllowedMethods, ", "))
|
||||
}
|
||||
|
||||
// Reflect the preflight request headers when present; otherwise use the explicit config list
|
||||
// The origin is trusted at this point, so reflecting the preflight request
|
||||
// headers is safe (the config list contains "X-Foo-*" patterns that browsers
|
||||
// cannot match literally).
|
||||
requestedHeaders := r.Header("Access-Control-Request-Headers")
|
||||
if requestedHeaders != "" {
|
||||
w.SetHeader("Access-Control-Allow-Headers", requestedHeaders)
|
||||
@@ -144,13 +190,40 @@ func SetCORSHeaders(w ResponseWriter, r Request, config CORSConfig) {
|
||||
w.SetHeader("Access-Control-Max-Age", fmt.Sprintf("%d", config.MaxAge))
|
||||
}
|
||||
|
||||
// Allow credentials only when a specific origin is reflected (not wildcard)
|
||||
// Expose headers that clients can read (fresh slice: avoid appending into
|
||||
// config.AllowedHeaders' backing array)
|
||||
exposeHeaders := make([]string, 0, len(config.AllowedHeaders)+3)
|
||||
exposeHeaders = append(exposeHeaders, config.AllowedHeaders...)
|
||||
exposeHeaders = append(exposeHeaders, "Content-Range", "X-Api-Range-Total", "X-Api-Range-Size")
|
||||
w.SetHeader("Access-Control-Expose-Headers", strings.Join(exposeHeaders, ", "))
|
||||
}
|
||||
|
||||
// setCORSHeadersLegacy is the pre-hardening behaviour (reflect any origin with
|
||||
// credentials). Used only when hardening.cors_strict_origins is false.
|
||||
func setCORSHeadersLegacy(w ResponseWriter, r Request, config CORSConfig) {
|
||||
origin := r.Header("Origin")
|
||||
if origin == "" {
|
||||
origin = "*"
|
||||
} else {
|
||||
w.UnderlyingResponseWriter().Header().Set("Vary", "Origin")
|
||||
}
|
||||
w.SetHeader("Access-Control-Allow-Origin", origin)
|
||||
if len(config.AllowedMethods) > 0 {
|
||||
w.SetHeader("Access-Control-Allow-Methods", strings.Join(config.AllowedMethods, ", "))
|
||||
}
|
||||
if requested := r.Header("Access-Control-Request-Headers"); requested != "" {
|
||||
w.SetHeader("Access-Control-Allow-Headers", requested)
|
||||
} else if len(config.AllowedHeaders) > 0 {
|
||||
w.SetHeader("Access-Control-Allow-Headers", strings.Join(config.AllowedHeaders, ", "))
|
||||
}
|
||||
if config.MaxAge > 0 {
|
||||
w.SetHeader("Access-Control-Max-Age", fmt.Sprintf("%d", config.MaxAge))
|
||||
}
|
||||
if origin != "*" {
|
||||
w.SetHeader("Access-Control-Allow-Credentials", "true")
|
||||
}
|
||||
|
||||
// Expose headers that clients can read
|
||||
exposeHeaders := config.AllowedHeaders
|
||||
exposeHeaders := make([]string, 0, len(config.AllowedHeaders)+3)
|
||||
exposeHeaders = append(exposeHeaders, config.AllowedHeaders...)
|
||||
exposeHeaders = append(exposeHeaders, "Content-Range", "X-Api-Range-Total", "X-Api-Range-Size")
|
||||
w.SetHeader("Access-Control-Expose-Headers", strings.Join(exposeHeaders, ", "))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user