CVE-2026-6478: Timing Channel in PostgreSQL MD5 Passwords


On May 14, 2026, PostgreSQL released security fixes for versions 18.4, 17.10, 16.14, 15.18, and 14.23. Among the vulnerabilities fixed is CVE-2026-6478, described by the project as a timing side channel in MD5 password comparison during authentication.

The core issue is simple: passwords stored in the legacy MD5 format can leak information through small differences in authentication response time. With repeated measurements, an attacker could recover enough credential material to authenticate. Passwords stored in scram-sha-256 format are not affected.

Official sources:


The problem in one sentence


Migrating your PostgreSQL configuration to SCRAM doesn’t automatically mean every role has stopped storing MD5 passwords.

This is the most common operational mistake.

An environment can have:

password_encryption = scram-sha-256

and still have legacy users with rolpassword starting with:

md5...

This happens because password_encryption only defines how new passwords will be written. It doesn’t automatically rewrite passwords already stored in the PostgreSQL catalog.


Where the password is stored


PostgreSQL roles live in the pg_authid catalog. The rolpassword column holds the password verifier, not the plaintext password.

For MD5 passwords, the format is:

md5<32 hexadecimal characters>

According to the PostgreSQL documentation, this hash is derived from the password concatenated with the username:

md5(password + username)

Conceptual example:

user:     joao
password: exemplo123
base:     exemplo123joao
hash:     md5(base)

The value stored in pg_authid.rolpassword would look like:

md5...

For SCRAM-SHA-256, the format is different:

SCRAM-SHA-256$<parameters>$<keys>

This format is more modern and is not affected by this CVE.


Why upgraded clusters can remain vulnerable


The typical scenario is:

  1. The company was running PostgreSQL 13 or earlier.
  2. Some roles had passwords stored as MD5.
  3. The cluster was upgraded to PostgreSQL 14, 15, 16, 17, or 18.
  4. The configuration was changed to use SCRAM.
  5. The old passwords were never reset.

Result: the current configuration looks correct, but old MD5-format credentials still exist.

This detail matters because CVE-2026-6478 specifically targets authentication involving MD5 passwords.


What a timing channel is


A timing channel is an indirect leak through time.

The system doesn’t explicitly return:

this character is correct

But it may respond a bit faster or a bit slower depending on the internal code path executed.

Picture a naive comparison between two values:

real secret: abcd
attempt:     x000

If the first character is already wrong, the program may stop immediately.

Now compare:

real secret: abcd
attempt:     a000

Here, the first character matches. The program moves on to the second before failing. This can take a bit longer.

Now:

real secret: abcd
attempt:     ab00

The program compares two correct characters before failing. It can take even longer.

Individually, these differences are tiny. On a real network, CPU noise, cache effects, latency, concurrency, and system variance can hide all of it. The risk appears when someone runs many attempts and aggregates the measurements.


How this kind of attack works at a high level


The attacker doesn’t necessarily need to know the original password. They need to turn the server into a timing oracle.

An oracle, in this context, is a system that doesn’t reveal the answer directly but provides observable clues. The clue here is response time.

The conceptual flow would be:

  1. Choose a target role that can attempt to authenticate.
  2. Send carefully controlled authentication attempts.
  3. Measure the response time of each attempt.
  4. Repeat many times to reduce noise.
  5. Compare time distributions across candidates.
  6. Infer which candidate appears to trigger a longer comparison.
  7. Repeat the process until enough material is recovered to authenticate.

This process is not a direct read of the password. It’s a statistical inference.

It’s also not simply testing words from a wordlist like a traditional brute-force attack. In timing-comparison vulnerabilities, the attacker tries to observe whether a given candidate makes the server progress deeper into the internal comparison.

Since MD5 values are represented in hexadecimal, the visible character space of a hash typically involves:

0 1 2 3 4 5 6 7 8 9 a b c d e f

But the practical detail of turning this into credential recovery depends on the protocol, the implementation, the exact point of the vulnerable comparison, and the ability to measure time precisely. For defensive purposes, the important part is understanding that response time can act as a probabilistic signal.


Why a single attempt proves nothing


A single measurement isn’t reliable.

Example:

candidate A: 10.3 ms
candidate B: 10.7 ms

This difference could be caused by anything: network latency, OS scheduling, cache, server load, or concurrency.

The attack depends on repetition:

candidate A: hundreds or thousands of measurements
candidate B: hundreds or thousands of measurements
candidate C: hundreds or thousands of measurements

After that, the attacker would compare means, medians, spread, and outliers to infer which candidate has consistently different behavior.

So the risk from this CVE isn’t the server “handing over the password” in a single response. The risk is the server allowing a tiny difference to be amplified statistically.


Who is affected


An environment should be considered exposed when these conditions combine:

  1. The PostgreSQL version predates the fixed release.
  2. Roles exist with rolpassword starting with md5.
  3. An attacker can attempt authentication against the server.

The fixed versions are:

PostgreSQL 18.4
PostgreSQL 17.10
PostgreSQL 16.14
PostgreSQL 15.18
PostgreSQL 14.23

Earlier versions within these majors are affected.


How to audit your environment


Connect as a superuser or with a user authorized to query pg_authid.

Check the version:

SELECT version();

Check the current configuration:

SHOW password_encryption;

List the stored password types:

SELECT
  rolname,
  rolcanlogin,
  CASE
    WHEN rolpassword IS NULL THEN 'no password'
    WHEN rolpassword LIKE 'md5%' THEN 'MD5'
    WHEN rolpassword LIKE 'SCRAM-SHA-256$%' THEN 'SCRAM'
    ELSE 'other'
  END AS password_type
FROM pg_authid
ORDER BY password_type, rolname;

List roles still on MD5:

SELECT rolname, rolcanlogin
FROM pg_authid
WHERE rolpassword LIKE 'md5%'
ORDER BY rolname;

If this query returns rows, MD5 verifiers still exist in the cluster.


How to fix it


The correct order is:

  1. Update PostgreSQL to the fixed version.
  2. Audit roles with MD5 passwords.
  3. Reset those passwords with password_encryption = 'scram-sha-256'.
  4. Adjust pg_hba.conf to SCRAM where possible.
  5. Validate that no MD5 hashes remain.

1. Apply the fixed minor release


Update to one of these versions or higher within the same major:

14.23
15.18
16.14
17.10
18.4

According to the PostgreSQL announcement, minor updates don’t require dump/restore or pg_upgrade. The procedure usually involves stopping the service, updating the binaries/packages, and starting it back up, following your distribution’s or vendor’s installation method.


2. Make SCRAM the default for new passwords


Configure:

ALTER SYSTEM SET password_encryption = 'scram-sha-256';
SELECT pg_reload_conf();
SHOW password_encryption;

Or adjust it directly in postgresql.conf, following your environment’s operational standard.


3. Reset legacy passwords


For each role that still shows up as MD5:

ALTER ROLE role_name PASSWORD 'strong_new_password';

You can also use psql’s interactive command:

\password role_name

The important part is that the reset must happen while password_encryption is set to scram-sha-256.


4. Validate the migration


After the resets:

SELECT rolname, rolcanlogin
FROM pg_authid
WHERE rolpassword LIKE 'md5%'
ORDER BY rolname;

The ideal result is zero rows.

It’s also worth checking roles without a password:

SELECT rolname, rolcanlogin
FROM pg_authid
WHERE rolpassword IS NULL
ORDER BY rolname;

Passwordless roles aren’t a direct target of this issue, but they should be reviewed to make sure the environment’s authentication model is intentional.


5. Review pg_hba.conf


Once all clients support SCRAM, prefer entries with:

scram-sha-256

Example:

host    all     all     10.0.0.0/8      scram-sha-256

Avoid keeping md5 as an authentication method for indefinite compatibility. The PostgreSQL documentation itself notes that MD5 password support is deprecated and will be removed in a future release.



Checklist for production:

  1. Inventory PostgreSQL 14 through 18 clusters.
  2. Identify the minor version of each cluster.
  3. Prioritize upgrading clusters below the fixed versions.
  4. Audit pg_authid on each cluster.
  5. Generate a list of roles with rolpassword LIKE 'md5%'.
  6. Validate impact with applications and client libraries.
  7. Ensure client-side SCRAM support.
  8. Define a password rotation window for affected roles.
  9. Reset passwords under password_encryption = 'scram-sha-256'.
  10. Change pg_hba.conf to scram-sha-256 where applicable.
  11. Reload the configuration.
  12. Validate that no MD5 verifiers remain.
  13. Record evidence of the mitigation.

Minimum evidence for internal documentation


Before:

SELECT version();
SHOW password_encryption;

SELECT rolname, rolcanlogin
FROM pg_authid
WHERE rolpassword LIKE 'md5%'
ORDER BY rolname;

After:

SELECT version();
SHOW password_encryption;

SELECT rolname, rolcanlogin
FROM pg_authid
WHERE rolpassword LIKE 'md5%'
ORDER BY rolname;

Acceptance criteria:

PostgreSQL version is patched.
No role with login privileges keeps an MD5 verifier.
New passwords are written as SCRAM-SHA-256.
pg_hba.conf doesn't force MD5 where SCRAM is supported.

Conclusion


CVE-2026-6478 is a good example of the gap between the configuration you intend and the actual state of the cluster.

It’s not enough to say:

we use SCRAM

You need to confirm:

no role still holds an MD5 verifier in pg_authid

The effective fix has two parts. First, apply the fixed minor release, because that’s what closes the timing side channel. Second, remove the legacy material that was the target of the vulnerability by resetting MD5 passwords under SCRAM.

In practical terms: the patch fixes the bug; password rotation removes the legacy risk.