> For the complete documentation index, see [llms.txt](https://docs.prestd.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.prestd.com/releases/v2.5.2.md).

# v2.5.2

pREST v2.5.2 fixes three advisories (stored-query RCE, unauthenticated catalog, plugin path traversal) and replaces pg.single with pg.allowed\_databases.

Released: October 18, 2026

[GitHub tag](https://github.com/prest/prest/releases/tag/v2.5.2) · [Compare v2.5.1...v2.5.2](https://github.com/prest/prest/compare/v2.5.1...v2.5.2)

**v2.5.2** is a **security** release. It fixes three advisories published against v2.5.1 and earlier ([#1070](https://github.com/prest/prest/pull/1070)). It also reworks how pREST picks which databases to serve when no registry is configured ([#1068](https://github.com/prest/prest/pull/1068)): `pg.single` is removed and `pg.allowed_databases` replaces it. MySQL README and sample config wording ([#1062](https://github.com/prest/prest/pull/1062)) and dependency bumps ([#1067](https://github.com/prest/prest/pull/1067), [#1071](https://github.com/prest/prest/pull/1071)) ship in the same tag.

* **Docker:** `prest/prest:v2.5.2` (linux/amd64 and linux/arm64)
* **Go install:** `go install github.com/prest/prest/v2/cmd/prestd@v2.5.2`

{% hint style="danger" %}
**Upgrade from v2.5.1 or earlier now.** Every release up to v2.5.1 is affected by [GHSA-2x9q-r7j9-g85f](https://github.com/prest/prest/security/advisories/GHSA-2x9q-r7j9-g85f) (critical, CVSS 9.0), [GHSA-fmr6-9j48-xrqp](https://github.com/prest/prest/security/advisories/GHSA-fmr6-9j48-xrqp) (high, CVSS 8.1), and [GHSA-fhf5-m388-4w4p](https://github.com/prest/prest/security/advisories/GHSA-fhf5-m388-4w4p) (high, CVSS 7.5).

Two fixes change behavior you may rely on:

* **Tables named `prest_users` or `prest_queries`** (or your configured `auth.table` / `queries.table`) can no longer be reached through REST, `_join`, or MCP. There is no opt-out.
* **With `auth.enabled = true`, the catalog routes** (`/databases`, `/schemas`, `/tables`, `/{database}/{schema}`, `/show/...`) and `/_PLUGIN` now require a token.

See [Upgrading from v2.5.1](#upgrading-from-v251).
{% endhint %}

***

## Security

### Stored queries can no longer be written over REST ([GHSA-2x9q-r7j9-g85f](https://github.com/prest/prest/security/advisories/GHSA-2x9q-r7j9-g85f))

**Critical, CVSS 9.0.** Reported by arkmarta.

With [database-backed queries](/api-reference/custom-queries.md#database-backed-storage-v220) (`queries.storage = "database"`), script bodies live in the `prest_queries` table. Before v2.5.2 nothing stopped the generic table routes from writing that table. With the default `[access] restrict = false`, one `POST /{db}/public/prest_queries` stored arbitrary SQL, and a second request to `/_QUERIES/{location}/{name}` ran it. When pREST connects as a PostgreSQL superuser (as the sample compose files do), that SQL can run OS commands through `COPY ... FROM PROGRAM`. The same routes could also read `prest_users`.

v2.5.2 adds a **reserved table** list. These names are matched case-insensitively, in any schema:

* `prest_queries`
* `prest_users`
* the configured `queries.table`
* the configured `auth.table` (`PREST_AUTH_TABLE`)

They are blocked on every data route, regardless of `restrict`, `ignore_table`, per-user rules, or whether auth and database-backed queries are enabled:

| Request                                                              | Response                                                               |
| -------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| Any method on `/{db}/{schema}/{reserved}`, `/batch/...`, `/show/...` | **401** `{"error":"authorization required"}`, even with an admin token |
| `_join` that targets a reserved table                                | **400** `{"error":"table is reserved for pREST internal use"}`         |
| MCP `select_table` / `describe_table` on a reserved table            | JSON-RPC permission error                                              |

Manage stored queries through the [`/_QUERIES/registry` API](/api-reference/custom-queries.md#registry-api) (admin-only). Manage users with SQL or `prestd migrate`. See [Permissions — Reserved tables](/get-started/permissions.md#reserved-tables-v252).

{% hint style="warning" %}
The patch blocks new writes. **It does not remove rows already stored.** A planted row still runs every time its `/_QUERIES` URL is called. If you ran database-backed queries on v2.5.1 or earlier without auth, audit the table (step 3 of [Upgrading](#upgrading-from-v251)).
{% endhint %}

### Catalog routes require auth ([GHSA-fhf5-m388-4w4p](https://github.com/prest/prest/security/advisories/GHSA-fhf5-m388-4w4p))

**High, CVSS 7.5.** Reported by Hama1cco.

The catalog routes were registered outside every middleware stack. With `auth.enabled = true`, a caller with no token got `200` from `/databases`, `/schemas`, `/tables`, `/{database}/{schema}`, and `/show/{database}/{schema}/{table}`, while CRUD on the same tables correctly returned `401`. On PostgreSQL, `/databases` read `pg_database`, so it listed every database on the server, including ones pREST was not configured to serve.

From v2.5.2 these routes run through authentication and table access control:

* **With `auth.enabled = true`:** no token is **401** `{"error":"authorization token is empty"}`.
* **`/show` follows `[access]`:** with `restrict = true`, a table not granted `read` is **401**, as it is on CRUD. This applies with auth off too.
* **`/databases` lists only served databases:** registry aliases, or `pg.database` plus `pg.allowed_databases` ([#1068](https://github.com/prest/prest/pull/1068)).

Only `/auth`, `/_health`, `/_ready`, and `/_studio` stay public. To re-expose a route on purpose, add it to [`[jwt] whitelist`](/api-reference/auth.md#whitelist). See [Auth — Routes that require auth](/api-reference/auth.md#routes-that-require-auth-v252).

### Plugin names are confined to `pluginpath` ([GHSA-fmr6-9j48-xrqp](https://github.com/prest/prest/security/advisories/GHSA-fmr6-9j48-xrqp))

**High, CVSS 8.1.** Reported by T4ran24.

`/_PLUGIN/{file}/{func}` joined `{file}` onto `pluginpath` without a containment check, so a traversal such as `../../tmp/x` loaded any `.so` file on disk. The route also had no authentication of its own.

From v2.5.2:

* `{file}` must be letters, digits, `_`, or `-`, 1–63 characters, with no dots.
* `{func}` must be a Go identifier.
* The resolved path must stay inside `pluginpath`, after symlinks are resolved. A symlinked `.so` that points outside is rejected.
* An invalid name is **400** `{"error": "invalid plugin name"}`. The rejected name is not echoed. A missing library is still **404**.
* With `auth.enabled = true`, `/_PLUGIN` requires a token. **With auth off the route stays open**; only the path checks apply.
* The same name rules apply to `[[pluginmiddlewarelist]]` `file` / `func` values.

See [Endpoint Plugin — Name rules and auth](/plugins/endpoint-plugin.md#name-rules-and-auth-v252).

***

## Multi-database ([#1068](https://github.com/prest/prest/pull/1068))

Co-authored by [@samithiwat](https://github.com/samithiwat). Most of the initial changes came from the [brickstechnology/prest](https://github.com/brickstechnology/prest) fork.

### `pg.single` removed

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

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

What changes:

* **A registry is no longer limited to `pg.database`.** Before, a registry with `pg.single = true` (the default) accepted only the default alias. Now a registry serves all of its aliases.
* **Without a registry, only listed databases are served.** Before, `pg.single = false` let any database on the `[pg]` host be reached by putting its name in the path. Now only `pg.database` plus `pg.allowed_databases` are served. Any other name is **400** `database not registered: <name>`.
* **`/databases` and MCP `prest.list_databases` list only served databases.**
* **On MySQL without a registry, `{schema}` must equal `{database}`.** On MySQL a schema is a database, so this stops one served name from reading another database the login can see. `/{db}/{other}/...` is **404** `schema not served by this database`.

### `pg.allowed_databases`

Extra databases on the `[pg]` host, using the `[pg]` credentials, served when no `[[databases]]` registry is configured. Works on PostgreSQL and MySQL.

```toml
[pg]
database = "prest"
allowed_databases = ["reporting", "archive"]
```

```sh
PREST_PG_DATABASE=prest
PREST_PG_ALLOWED_DATABASES=reporting,archive
# PREST_DB_ALLOWED_DATABASES is read first when both are set
```

See [Multi-database — No-registry mode](/get-started/multi-database.md#no-registry-mode-v252).

### Unreachable registry entries

In v2.5.1 any `[[databases]]` entry that failed to connect at startup stopped the server. From v2.5.2:

* A network or DNS failure registers the alias **lazily**. It answers **503** `{"error":"database unavailable"}` and reconnects on first use. The response does not include driver text such as host and port.
* Startup still fails on invalid config or auth errors, or when **every** entry is unreachable.
* The default adapter is the first reachable alias.
* `/_ready` pings every alias, so it returns **503** while any one is down.
* Timescale detection and startup `migrate_on_startup` steps skip a lazy alias until the next restart.

### Smaller changes

* PostgreSQL keeps at most **64** distinct pools per process. A request that would open another gets **503**.
* Entries from `DATABASE_ALIAS_N` / `DATABASE_URL_N` inherit the global `maxopenconn` / `maxidleconn`. Before, `0` meant unlimited.
* `/databases` returns aliases sorted.
* A connection or DNS failure is **503** `database unavailable` on CRUD, `/_QUERIES/registry`, and MCP.
* `/auth` returns a generic **401** `{"error":"authentication failed"}` when the user lookup hits a SQL or driver error. `user not found` is unchanged.
* MySQL `/schemas` in registry mode lists only the registry entries on the same host and port, not every database the login can see ([#1070](https://github.com/prest/prest/pull/1070)).
* `prestd version` on a `go install` build reports the module version (for example `v2.5.2`) instead of a hard-coded `2.0.0`. Builds with no version info report `devel`.
* The Kubernetes manifest image is `prest/prest:v2` (was `v1`).

***

## Dependencies

* `golang.org/x/crypto` 0.57.0 → 0.58.0 ([#1067](https://github.com/prest/prest/pull/1067)).
* pREST Studio dependency updates ([#1058](https://github.com/prest/prest/pull/1058)–[#1061](https://github.com/prest/prest/pull/1061), [#1069](https://github.com/prest/prest/pull/1069)), applied in [#1071](https://github.com/prest/prest/pull/1071). The embedded `/_studio` bundle is rebuilt; no Studio behavior change.

***

## Changes since v2.5.1

| PR                                                | Summary                                                                                                                                                                                                                                           |
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [#1070](https://github.com/prest/prest/pull/1070) | Security hardening: reserved tables, catalog and `/show` auth/ACL, `/_PLUGIN` confinement and auth                                                                                                                                                |
| [#1068](https://github.com/prest/prest/pull/1068) | Multi-database fixups: `pg.single` removed, `pg.allowed_databases`, lazy 503 aliases, scoped `/databases` (co-authored by [@samithiwat](https://github.com/samithiwat), from [brickstechnology/prest](https://github.com/brickstechnology/prest)) |
| [#1062](https://github.com/prest/prest/pull/1062) | README and sample config for MySQL                                                                                                                                                                                                                |
| [#1067](https://github.com/prest/prest/pull/1067) | `golang.org/x/crypto` 0.58.0                                                                                                                                                                                                                      |
| [#1071](https://github.com/prest/prest/pull/1071) | Studio dependency updates                                                                                                                                                                                                                         |

Full detail: [compare v2.5.1...v2.5.2](https://github.com/prest/prest/compare/v2.5.1...v2.5.2). Commit list: [Changes since v2.5.1](/releases/main-since-v2.5.1.md).

***

## Upgrading from v2.5.1

1. **Retag** to `prest/prest:v2.5.2` or `go install github.com/prest/prest/v2/cmd/prestd@v2.5.2`.
2. **Check for reserved table names.** If an application table is named `prest_users` or `prest_queries`, or matches your `auth.table` / `queries.table`, rename it. REST, `_join`, and MCP can no longer reach it.
3. **Audit stored queries.** If you ran `queries.storage = "database"` on v2.5.1 or earlier without auth, look for rows you did not create, and delete them:

   ```sql
   SELECT database_alias, location, name, created_by, created_at, updated_at,
          read_sql, write_sql, update_sql, delete_sql
   FROM public.prest_queries
   ORDER BY updated_at DESC;
   ```

   Use your `queries.schema` / `queries.table` if you changed them. Do not trust `created_by`: a REST write could set it to any value. Read the SQL columns themselves.
4. **Use a least-privilege database role.** Connect as a role without superuser, `pg_read_server_files`, or `pg_execute_server_program`. A non-superuser role cannot run `COPY ... FROM PROGRAM`, which limits what any stored-SQL bug can do.
5. **Give catalog clients a token.** With `auth.enabled = true`, anything that calls `/databases`, `/schemas`, `/tables`, `/{database}/{schema}`, `/show/...`, or `/_PLUGIN` without a token now gets `401`. Send a bearer token, or whitelist the route on purpose.
6. **Check plugin names.** Rename `.so` files whose basename contains a dot, and replace symlinks that resolve outside `pluginpath`.
7. **Replace `pg.single`.** Use a `[[databases]]` registry, or list extra databases in `pg.allowed_databases`. Requests for any other database now get `400`.
8. **MySQL without a registry:** paths where `{schema}` differs from `{database}` now get `404`.
9. **If you skipped** [**v2.4.2**](/releases/v2.4.2.md)**,** check your `jwt.key` length. A short HMAC key does not stop startup; it disables `/auth`. See [v2.4.2 — Upgrading](/releases/v2.4.2.md#upgrading-from-v241).

***

## Related

* [v2.5.1 release notes](/releases/v2.5.1.md)
* [Auth](/api-reference/auth.md)
* [Permissions](/get-started/permissions.md)
* [Custom Queries](/api-reference/custom-queries.md)
* [Multi-database](/get-started/multi-database.md)
* [Endpoint Plugin](/plugins/endpoint-plugin.md)
* [MySQL](/databases/mysql.md)
* [Acronyms](/readme/acronyms.md) ([JWT](/readme/acronyms.md#jwt), [MCP](/readme/acronyms.md#mcp), [REST](/readme/acronyms.md#rest), [SQL](/readme/acronyms.md#sql))
