pREST v2.5.2 Security Release: Stored Query RCE, Catalog Auth and Plugin Path Fixes

pREST v2.5.2 fixes three advisories that hit every release up to v2.5.1. The worst, CVSS 9.0, turns two unauthenticated requests into a shell command on your database host.

Upgrading is one retag plus a few checks. Below: how to tell if you’re exposed, what changes, and the exact steps.

pREST v2 series: Architecture (rc6) → v2.0.0 / v2.1.0 GA → MCP tutorial → AI plugins → v2.2.0 → v2.3.0 → v2.4.0 → v2.4.1 + v2.4.2 → v2.5: MySQL → This post (v2.5.2)

New to pREST? The pREST v2 guide lists every release and where to start.

Upgrade first. Pull prest/prest:v2.5.2. Two fixes can break clients. Tables named prest_users or prest_queries are no longer reachable over REST. With auth on, the catalog routes now need a token.

Table of Contents

Are you affected?

Every release up to v2.5.1 is affected. How badly depends on your config:

Your setup on v2.5.1 or earlier Exposed to
queries.storage = "database", auth off Arbitrary SQL with no credentials, and OS commands if pREST connects as superuser
queries.storage = "database", auth on, restrict = false The same, for anyone holding a valid token
auth.enabled = true Your database, table and column map, readable without a token
/_PLUGIN reachable, JWT not enforced Loading any .so file on the server’s disk
PostgreSQL /databases listing every database on the server, not just yours

The official compose files connect as the PostgreSQL superuser. That is what turns stored SQL into code execution.

What v2.5.2 fixes

Stored queries were writable over REST

GHSA-2x9q-r7j9-g85f, critical, CVSS 9.0, reported by arkmarta.

With database-backed queries, script bodies live in prest_queries. Nothing stopped the generic table routes from writing it. The advisory’s proof of concept, with no credentials:

# store SQL in the queries table
curl -s -X POST http://target:3000/prest/public/prest_queries \
  -H 'Content-Type: application/json' \
  -d '{"location":"rpt","name":"d","write_sql":"DO $$ ... $$"}'   # DO block wrapping COPY ... FROM PROGRAM 'id'
# HTTP/1.1 201 Created

# run it, then read the output
curl -s -X POST http://target:3000/_QUERIES/rpt/d
curl -s http://target:3000/prest/public/p
# [{"l": "uid=999(postgres) gid=999(postgres) groups=999(postgres)"}]

Now prest_queries, prest_users and your configured queries.table and auth.table are reserved. Any schema, any case, no opt-out:

Request v2.5.2 response
Any method on /{db}/{schema}/{reserved}, /batch, /show 401 {"error":"authorization required"}, even for admins
_join to a reserved table 400 {"error":"table is reserved for pREST internal use"}
MCP select_table / describe_table JSON-RPC permission error

Manage stored queries through the admin-only /_QUERIES/registry API.

The catalog skipped auth

GHSA-fhf5-m388-4w4p, high, CVSS 7.5, reported by Hama1cco.

The catalog routes were registered outside the auth middleware. With PREST_AUTH_ENABLED=true and no token:

GET /prest/public/users       → 401  (CRUD, denied)
GET /tables                   → 200  every table and owner
GET /show/prest/public/users  → 200  every column, type and default

Now /databases, /schemas, /tables, /{database}/{schema} and /show/... run auth and table permissions. No token returns 401 {"error":"authorization token is empty"}. /show follows [access] even with auth off. /databases lists only the databases pREST serves.

Plugin names escaped pluginpath

GHSA-fmr6-9j48-xrqp, high, CVSS 8.1, reported by T4ran24.

/_PLUGIN/{file}/{func} joined {file} onto pluginpath unchecked. A {file} of ../../../../tmp/sample loaded /tmp/sample.so. The advisory’s test returned {"pwned":true}.

Now {file} must be letters, digits, _ or -, with no dots. The resolved path must stay inside pluginpath, symlinks included. A bad name returns 400 {"error": "invalid plugin name"}. With auth on, the route needs a token.

How to upgrade

  1. Retag and restart:
   docker pull prest/prest:v2.5.2
   # or
   go install github.com/prest/prest/v2/cmd/prestd@v2.5.2
  1. Audit stored queries if you used database storage. The patch blocks new writes but keeps planted rows:
   SELECT location, name, created_by, updated_at,
          read_sql, write_sql, update_sql, delete_sql
   FROM public.prest_queries
   ORDER BY updated_at DESC;

Read the SQL columns, not created_by; a REST write could set it to anything. Delete rows you didn’t create.

  1. Check whether pREST connects as a superuser:
   SELECT rolsuper FROM pg_roles WHERE rolname = current_user;

If it returns t, switch to a role without superuser or pg_execute_server_program. That role can’t run COPY ... FROM PROGRAM.

  1. Rename any app table called prest_users or prest_queries.

  2. Give catalog clients a token, or add the route to [jwt] whitelist on purpose:

   curl -s localhost:3000/tables -H "Authorization: Bearer $TOKEN"
  1. Find plugin files that the new name rules reject:
   find "$PLUGIN_PATH" -name '*.so' | grep -E '/[^/]*\.[^/]*\.so$'   # dots in the name
   find "$PLUGIN_PATH" -type l                                      # symlinks to check
  1. Replace pg.single (next section).

Full list: v2.5.2 release notes.

Replace pg.single with pg.allowed_databases

pg.single and PREST_PG_SINGLE are now ignored. Startup logs:

config key ignored: pg.single was removed; use [[databases]] or pg.allowed_databases

A [[databases]] registry now serves all its aliases. Without a registry, list extra databases on the same host:

[pg]
database = "prest"
allowed_databases = ["reporting", "archive"]
PREST_PG_ALLOWED_DATABASES=reporting,archive

Any other database name returns 400 database not registered: <name>. On MySQL without a registry, {schema} must equal {database}, or you get a 404.

Versions and limits

  • Affected: every release up to v2.5.1. Fixed: v2.5.2.
  • With auth off, /_PLUGIN stays open. Only the path checks apply.
  • Upgrading doesn’t remove planted stored queries. Step 2 does.
  • Reserved table names have no opt-out.

Also in this release

The multi-database rework (#1068) was co-authored by @samithiwat, starting from the brickstechnology/prest fork. An unreachable [[databases]] entry no longer stops startup; it answers 503 and reconnects on first use. prestd version reports the real version on go install builds. golang.org/x/crypto moves to 0.58.0.

FAQ

Is pREST v2.5.1 vulnerable?

Yes. All three advisories affect every release up to v2.5.1. v2.5.2 fixes them. The critical one needs queries.storage = "database".

How do I check if someone planted a stored query in pREST?

Query prest_queries and read the four SQL columns of every row. Don’t trust created_by, since a REST write could set it. Delete what you didn’t create.

Why does /databases return 401 after upgrading pREST?

Since v2.5.2, catalog routes use the same auth as CRUD routes. With auth on, send a bearer token. Or whitelist the route if it should be public.

What replaces pg.single in pREST?

A [[databases]] registry, or pg.allowed_databases without one. pg.single is ignored from v2.5.2, with a warning at startup.

Can I still name a table prest_users in pREST?

Not if you want it reachable through pREST. Those names are reserved on REST, _join and MCP. Rename the table.

Does turning on pREST auth protect plugins?

Since v2.5.2, yes: /_PLUGIN needs a token when auth is on. With auth off it stays open. Name and path checks apply either way.

Sources

Release: v2.5.2 on GitHub. Release notes: docs.prestd.com. Advisories: GHSA-2x9q-r7j9-g85f, GHSA-fhf5-m388-4w4p, GHSA-fmr6-9j48-xrqp. Fixes: #1070, #1068.

Related: pREST in 10 minutes, pREST v2.5 with MySQL, pREST v2 guide.