VYPR
Critical severityOSV Advisory· Published Sep 8, 2025· Updated Apr 15, 2026

CVE-2025-58450

CVE-2025-58450

Description

pREST (PostgreSQL REST), is an API that delivers an application on top of a Postgres database. SQL injection is possible in versions prior to 2.0.0-rc3. The validation present in versions prior to 2.0.0-rc3 does not provide adequate protection from injection attempts. Version 2.0.0-rc3 contains a patch to mitigate such attempts.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/prest/prest/v2Go
>= 0

Affected products

1

Patches

1
47d02b878429

fix(postgres): improve `_returning` param handling for SQL injection safety (#935)

https://github.com/prest/prestAvelinoSep 8, 2025via ghsa
4 files changed · +85 9
  • adapters/postgres/postgres.go+33 6 modified
    @@ -310,12 +310,23 @@ func (adapter *Postgres) ReturningByRequest(r *http.Request) (returningSyntax st
     	// https://docs.prestd.com/api-reference/parameters
     	queries := r.URL.Query()["_returning"]
     	if len(queries) > 0 {
    -		for i, q := range queries {
    -			if i > 0 && i < len(queries) {
    -				returningSyntax += ", "
    +		cols := make([]string, 0, len(queries))
    +		for _, q := range queries {
    +			if q == "*" {
    +				cols = append(cols, "*")
    +				continue
    +			}
    +			if chkInvalidIdentifier(q) {
    +				err = errors.Wrap(ErrInvalidIdentifier, "Returning")
    +				return
     			}
    -			returningSyntax += q
    +			parts := strings.Split(q, ".")
    +			for i := range parts {
    +				parts[i] = `"` + parts[i] + `"`
    +			}
    +			cols = append(cols, strings.Join(parts, "."))
     		}
    +		returningSyntax = strings.Join(cols, ", ")
     	}
     	return
     }
    @@ -533,6 +544,14 @@ func (adapter *Postgres) JoinByRequest(r *http.Request) (values []string, err er
     		return
     	}
     
    +	// whitelist join types
    +	jt := strings.ToUpper(joinArgs[0])
    +	allowed := map[string]bool{"INNER": true, "LEFT": true, "RIGHT": true, "FULL": true, "CROSS": true}
    +	if !allowed[jt] {
    +		err = ErrInvalidJoinClause
    +		return
    +	}
    +
     	if chkInvalidIdentifier(joinArgs[1], joinArgs[2], joinArgs[4]) {
     		err = ErrInvalidIdentifier
     		return
    @@ -556,7 +575,7 @@ func (adapter *Postgres) JoinByRequest(r *http.Request) (values []string, err er
     		err = errJoin
     		return
     	}
    -	joinQuery := fmt.Sprintf(` %s JOIN "%s" ON "%s"."%s" %s "%s"."%s" `, strings.ToUpper(joinArgs[0]), joinArgs[1], spl[0], spl[1], op, splj[0], splj[1])
    +	joinQuery := fmt.Sprintf(` %s JOIN "%s" ON "%s"."%s" %s "%s"."%s" `, jt, joinArgs[1], spl[0], spl[1], op, splj[0], splj[1])
     	values = append(values, joinQuery)
     	return
     }
    @@ -1555,7 +1574,15 @@ func (adapter *Postgres) GroupByClause(r *http.Request) (groupBySQL string) {
     			return
     		}
     
    -		havingQuery := fmt.Sprintf(statements.Having, groupFunc, operator, params[4])
    +		// sanitize having value: numeric stays raw, string gets single-quoted and escaped
    +		val := params[4]
    +		if _, errNum := strconv.ParseFloat(val, 64); errNum == nil {
    +			havingQuery := fmt.Sprintf(statements.Having, groupFunc, operator, val)
    +			groupBySQL = fmt.Sprintf("%s %s", fmt.Sprintf(statements.GroupBy, groupFieldQuery[0]), havingQuery)
    +			return
    +		}
    +		safe := strings.ReplaceAll(val, "'", "''")
    +		havingQuery := fmt.Sprintf(statements.Having, groupFunc, operator, fmt.Sprintf("'%s'", safe))
     		groupBySQL = fmt.Sprintf("%s %s", fmt.Sprintf(statements.GroupBy, groupFieldQuery[0]), havingQuery)
     		return
     	}
    
  • adapters/postgres/postgres_test.go+5 3 modified
    @@ -272,8 +272,8 @@ func TestReturningByRequest(t *testing.T) {
     	}{
     		{"Returning by request with nothing", "/prest-test/public/test_group_by_table", []string{""}, nil},
     		{"Returning by request with _returning=*", "/prest-test/public/test_group_by_table?_returning=*", []string{"RETURNING *"}, nil},
    -		{"Returning by request with _returning=field", "/prest-test/public/test_group_by_table?_returning=age", []string{"RETURNING age"}, nil},
    -		{"Returning by request with multiple _returning=field", "/prest-test/public/test_group_by_table?_returning=age&_returning=salary", []string{"RETURNING age, salary"}, nil},
    +		{"Returning by request with _returning=field", "/prest-test/public/test_group_by_table?_returning=age", []string{"RETURNING \"age\""}, nil},
    +		{"Returning by request with multiple _returning=field", "/prest-test/public/test_group_by_table?_returning=age&_returning=salary", []string{"RETURNING \"age\", \"salary\""}, nil},
     	}
     	for _, tc := range testCases {
     		t.Log(tc.description)
    @@ -309,6 +309,7 @@ func TestGroupByClause(t *testing.T) {
     		// having tests
     		{"Group by clause with having clause", "/prest-test/public/test5?_groupby=celphone->>having:sum:salary:$gt:500", `GROUP BY "celphone" HAVING SUM("salary") > 500`, false},
     		{"Group by clause with having clause", "/prest-test/public/test5?_groupby=c.celphone->>having:sum:salary:$gt:500", `GROUP BY "c"."celphone" HAVING SUM("salary") > 500`, false},
    +		{"Group by clause with having clause string value with quotes", "/prest-test/public/test5?_groupby=celphone->>having:sum:name:$eq:O'Brian", `GROUP BY "celphone" HAVING SUM("name") = 'O''Brian'`, false},
     
     		// having errors, but continue with group by
     		{"Group by clause with wrong having clause (insufficient params)", "/prest-test/public/test5?_groupby=celphone->>having:sum:salary", `GROUP BY "celphone"`, false},
    @@ -755,6 +756,7 @@ func TestJoinByRequest(t *testing.T) {
     		{"Join missing param", "/prest-test/public/test?_join=inner:test2:test2.name:$eq", []string{}, true},
     		{"Join invalid operator", "/prest-test/public/test?_join=inner:test2:test2.name:notexist:test.name", []string{}, true},
     		{"Join invalid fields", "/prest-test/public/test?_join=inner:0test2:test2.name:notexist:test.name", []string{}, true},
    +		{"Join invalid type", "/prest-test/public/test?_join=weird:test2:test2.name:$eq:test.name", []string{}, true},
     	}
     
     	for _, tc := range testCases {
    @@ -801,7 +803,7 @@ func TestJoinByRequest(t *testing.T) {
     	joinStr := strings.Join(join, " ")
     
     	if !strings.Contains(joinStr, ` INNER JOIN "test2" ON "test2"."name" = "test"."name"`) {
    -		t.Errorf(`expected %s in INNER JOIN "test2" ON "test2"."name" = "test"."name", but no was!`, joinStr)
    +		t.Errorf(`expected %s in INNER JOIN "test2" ON "test2"."name" = "test"."name"`, joinStr)
     	}
     
     	where, values, err := config.PrestConf.Adapter.WhereByRequest(r, 1)
    
  • adapters/postgres/queries.go+1 0 modified
    @@ -65,6 +65,7 @@ func (adapter *Postgres) ParseScript(scriptPath string, templateData map[string]
     	}
     
     	sqlQuery = buff.String()
    +	values = funcs.Args
     	return
     }
     
    
  • template/funcregistry.go+46 0 modified
    @@ -3,6 +3,7 @@ package template
     import (
     	"fmt"
     	"net/url"
    +	"regexp"
     	"strconv"
     	"strings"
     	"text/template"
    @@ -11,6 +12,8 @@ import (
     // FuncRegistry registry func for templates
     type FuncRegistry struct {
     	TemplateData map[string]interface{}
    +	Args         []interface{}
    +	next         int
     }
     
     // RegistryAllFuncs for template
    @@ -22,6 +25,10 @@ func (fr *FuncRegistry) RegistryAllFuncs() (funcs template.FuncMap) {
     		"unEscape":       fr.unEscape,
     		"split":          fr.split,
     		"limitOffset":    fr.limitOffset,
    +		// secure SQL helpers
    +		"sqlVal":         fr.sqlVal,
    +		"sqlList":        fr.sqlList,
    +		"ident":          fr.ident,
     	}
     	return
     }
    @@ -83,3 +90,42 @@ func (fr *FuncRegistry) limitOffset(pageNumber, pageSize string) (value string)
     	}
     	return
     }
    +
    +// sqlVal returns a positional placeholder for a single value and stores it in Args
    +func (fr *FuncRegistry) sqlVal(key string) string {
    +	v := fr.TemplateData[key]
    +	fr.Args = append(fr.Args, v)
    +	fr.next++
    +	return fmt.Sprintf("$%d", fr.next)
    +}
    +
    +// sqlList returns a parenthesized, comma-separated list of placeholders for a slice value
    +func (fr *FuncRegistry) sqlList(key string) string {
    +	if s, ok := fr.TemplateData[key].([]string); ok {
    +		ph := make([]string, len(s))
    +		for i := range s {
    +			fr.Args = append(fr.Args, s[i])
    +			fr.next++
    +			ph[i] = fmt.Sprintf("$%d", fr.next)
    +		}
    +		return fmt.Sprintf("(%s)", strings.Join(ph, ","))
    +	}
    +	fr.Args = append(fr.Args, fr.TemplateData[key])
    +	fr.next++
    +	return fmt.Sprintf("($%d)", fr.next)
    +}
    +
    +var identRe = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$`)
    +
    +// ident validates and safely quotes an identifier (optionally dotted path)
    +func (fr *FuncRegistry) ident(key string) (string, error) {
    +	s, _ := fr.TemplateData[key].(string)
    +	if !identRe.MatchString(s) {
    +		return "", fmt.Errorf("invalid identifier: %s", s)
    +	}
    +	parts := strings.Split(s, ".")
    +	for i := range parts {
    +		parts[i] = `"` + parts[i] + `"`
    +	}
    +	return strings.Join(parts, "."), nil
    +}
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.