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.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/prest/prest/v2Go | >= 0 | — |
Affected products
1Patches
147d02b878429fix(postgres): improve `_returning` param handling for SQL injection safety (#935)
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
4News mentions
0No linked articles in our index yet.