Guide · Backend
PostgreSQL JDBC's October 2026 CVEs Explained: Fail-Open Auth and Padding Leaks
17 min readUpdated By Saket Bhatnagar
Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.
In short
- The PostgreSQL JDBC vulnerability of October 2026 is two bugs in pgjdbc, the PostgreSQL project's driver that Java apps use to talk to PostgreSQL. Version 42.7.14, released on 7 October 2026, fixes both.
- CVE-2026-107314 (a CVE is a public ID for one security bug) affects 42.7.11 to 42.7.13. The driver ignored requireAuth, its list of allowed login methods, when the list excluded every method. A fake server could then ask for the password in plain text.
- CVE-2026-107315 affects 42.7.4 to 42.7.13. When an app sent fewer bytes than it had promised, the driver filled the gap with leftover bytes from earlier messages, and the database stored them.
- Both are rated Moderate, with CVSS severity scores of 5.9 and 5.3 out of 10. Upgrade to 42.7.14, then audit requireAuth values and code that announces a data length before sending.
- Beginners can stop after the section on why a contradictory setting should fail closed. Engineers get the exact buffer state that skipped the wipe, the 8,192-byte leak ceiling, why length checks alone fail, a version check and a tested requireAuth audit script.
What goes wrong when a database driver quietly ignores a rule you set?
Your app believes it is protected while it is not. The PostgreSQL JDBC vulnerability CVE-2026-107314 is exactly that: a strict login rule the driver treated as no rule. Nothing crashed, so nobody noticed.
A JDBC driver is the library a Java program uses to talk to a database. pgjdbc is the PostgreSQL project's JDBC driver. Frameworks such as Spring Boot and Hibernate call a JDBC driver like pgjdbc underneath. You configure it with a JDBC URL, a connection string that names the server, the database and extra settings.
Picture a common setup. A team runs a GST invoicing service for small shops, with Java services on PostgreSQL and pgjdbc 42.7.12. GST is India's goods and services tax. The setup is made up, but both bugs are real.
In the same month, two problems reached the team. An audit asked them never to send a plain password to the database, so a contractor added a requireAuth value. Separately, a shop owner reported an invoice PDF whose last bytes held another shop's phone number.
The first problem is CVE-2026-107314 and the second is CVE-2026-107315. This guide follows both to the fix.
Which words do you need before reading the pgjdbc advisories?
You need these terms, here in plain words. Each one comes back later.
- Authentication method: how the database checks who you are. pgjdbc knows six: password (sent as plain text), md5 (an older hashed challenge), scram-sha-256, gss, sspi and none.
- SCRAM-SHA-256 is a challenge-response proof that never sends the password. GSS usually means Kerberos, a ticket-based single sign-on system used in company networks. SSPI is a Windows-only cousin of GSS, and none means trust, with no password at all.
- requireAuth: a pgjdbc connection property listing the methods the driver will accept. A name with ! in front rejects that method. pgjdbc's docs say you cannot mix allowed names and ! names.
- Man-in-the-middle: an attacker on the network between your app and the database who pretends to be the database.
- Fail-open and fail-closed: when a check cannot decide, fail-open lets the connection through and fail-closed refuses it.
- Send buffer: memory where the driver collects bytes before sending them in one go. pgjdbc reuses one buffer for every message on a connection.
- Declared length and padding: some driver calls announce a value's size before sending it. Padding is the filler the driver adds when the real data is shorter than that promise.
What is the October 2026 PostgreSQL JDBC vulnerability fixed in 42.7.14?
It is two separate bugs, published together on 7 October 2026 as two GitHub security advisories, GHSA-rhp9-mr79-r74h and GHSA-f64h-wr5q-3qf3. One is a login rule that failed open. The other is a send buffer that leaked old bytes into stored values.
| CVE-2026-107314 | CVE-2026-107315 | |
|---|---|---|
| What breaks | requireAuth that excludes every method, or names no method, is not enforced | Padding reuses bytes from earlier messages on the same connection |
| Affected versions | 42.7.11 to 42.7.13 | 42.7.4 to 42.7.13 |
| CVSS 3.1 score | 5.9, Moderate | 5.3, Moderate |
| Who can exploit it | A man-in-the-middle, no login needed | Someone who can make the app store a short value and read it back |
| What leaks | The database password | SQL text and parameter values of earlier statements |
| Weakness type | CWE-636, not failing securely | CWE-201 and CWE-226, sensitive data sent or not cleared before reuse |
| Turn it off without upgrading | Use a positive requireAuth list | No connection property; make declared lengths match the data |
CVSS is a 0 to 10 severity score. The CVSS vectors say each attack works over the network, is hard to pull off and harms only confidentiality. The padding bug also needs low privileges, such as a normal app account.
How did a requireAuth login setting that blocks everything end up allowing everything?
The driver used one value, null (Java's empty value), for two opposite meanings. The advisory names the function AuthMethod.parseRequireAuth. It returned null both when requireAuth was not set and when the value left no method allowed.
A second function, AuthMethod.checkAuth, treated null as no restriction. So the driver accepted whatever method the server asked for, including a plain-text password.
The advisory's example value is !password,!md5,!gss,!sspi,!scram-sha-256,!none, which excludes all six methods. The order of the names and any spaces between them do not matter.
That was the team's value. The contractor meant to block the weak methods, but put ! in front of every method in the docs, scram-sha-256 included. The driver ignored the rule, so everything kept working.
A value made only of commas, such as a single comma, names no method and failed the same way. Positive lists like scram-sha-256 and partial exclusions like !password,!md5 were enforced. Apps that never set the property were not affected.
How the old driver read a contradictory requireAuth
Drag to exploreNotice the two No arrows: the same question led to no restriction on old drivers and to a refusal on 42.7.14. The numbers 08004 and 22023 are error codes, explained below.
Diagram connections:
- requireAuth value to Not set (Property missing)
- requireAuth value to Any method left allowed? (Property present)
- Any method left allowed? to Enforce the list (Yes)
- Any method left allowed? to 42.7.11 to 42.7.13 (No, old driver)
- Any method left allowed? to 42.7.14 (No, fixed driver)
- requireAuth value: Read from the JDBC URL or connection properties
- Not set: No restriction: the normal default
- Any method left allowed?: Decided inside AuthMethod.parseRequireAuth
- Enforce the list: Positive lists and partial exclusions work
- 42.7.11 to 42.7.13: Returns null, checkAuth accepts any method
- 42.7.14: Refuses: 08004, or 22023 if no method is named
What the attacker in the middle does
At login, the PostgreSQL server picks the method and sends the client an authentication request. If it asks for a plain-text password, the protocol says the client sends the password as it is.
A man-in-the-middle can send that request itself. With the rule ignored, the team's driver would reply with its real database password. A rule that allowed only scram-sha-256 would have refused instead.
The password theft that requireAuth was meant to stop
Drag to exploreEach box is one message. On 42.7.14 the driver refuses at the second box instead of sending the password.
Diagram connections:
- Login request to Send plain password (Fake server asks)
- Send plain password to Password in clear text (Driver does not refuse)
- Password in clear text to Login with stolen password (Stolen password reused)
- Login request: Invoice app → fake server in the middle
- Send plain password: Fake server → invoice app
- Password in clear text: Invoice app → fake server, rule ignored
- Login with stolen password: Attacker → real PostgreSQL
Encryption alone does not save you. TLS is the protocol behind your browser's padlock. pgjdbc's default sslmode, prefer, does not verify the server's certificate, so a fake server can still speak TLS.
Why should a contradictory setting fail closed instead of open?
Because whoever wrote a strict rule wanted strictness. The team's value even included !none, which pgjdbc's docs describe as rejecting trust logins. A rule that allows no method and also forbids skipping login can only mean refuse.
A rule that fails open never produces an error. Tests pass, users stay happy and the gap stays invisible until someone attacks it.
The deeper mistake was a sentinel, a special value that stands for a situation. Null meant both nobody set a rule and the rule allows nothing. Code that reads it later cannot tell the two apart.
SQLState is a five-character error code that SQL databases and drivers share. In 42.7.14, a value excluding every method fails with SQLState 08004, the standard code for a server rejecting a connection. pgjdbc reuses it when the driver itself refuses.
The message reads: Authentication method is not allowed by requireAuth. With channelBinding=require also set, you see the channel binding error instead, per the release notes. Either one is your check that the fix is live.
A value that names no method, such as a single comma, fails as invalid with SQLState 22023, the standard code for an invalid parameter value. Its message is Invalid authentication method, followed by the value.
That is what happened to the team. When they upgraded staging to 42.7.14, the PDF service failed with SQLState 08004. Changing its value to requireAuth=scram-sha-256 fixed it.
How did the pgjdbc padding bug, CVE-2026-107315, leak old bytes?
When a value was shorter than its declared length, versions 42.7.4 to 42.7.13 could fill the gap with bytes left in the send buffer from earlier messages. The server stored them as part of the value.
How a reused send buffer works
Imagine a courier who reuses envelopes without emptying them. If your letter is shorter than promised, old pages already inside fill the gap.
Map it like this: the envelope is the send buffer, the old pages are bytes from earlier statements and the promise is the declared length. Where the picture breaks: the driver did wipe the part it reused while the buffer held unsent data. Only an empty buffer skipped the wipe.
The PostgreSQL wire protocol is the message format the driver and server exchange. In a Bind message, which carries a statement's parameter values, each value states its length first, then sends exactly that many bytes. COPY, PostgreSQL's bulk-load command, sends data messages that also carry their length first.
So once the driver has announced 50,000 bytes, it must send 50,000 even if the app supplies only 49,000. Versions 42.7.3 and earlier padded the gap with zeros.
The buffer state that skipped the wipe
The advisory names the padding routine PgBufferedOutputStream.writeZeros. From 42.7.4 it only zeroed the part of the buffer it overwrote while the buffer held data. When the buffer was empty on entry, it zeroed nothing and sent the old contents.
The advisory gives two ways the buffer ends up empty. The previous write was big enough to bypass the buffer, leaving older bytes behind. Or the data ended exactly at a buffer boundary, so the buffer still held the last bytes sent.
The model below is a simplified Python sketch of that one branch, not the driver's Java code. It flushes a buffer that just held shop 1's data, then pads a value for shop 2.
buf = bytearray(16) # one send buffer, reused for every message
count = 0 # bytes waiting in buf right now
def write(data):
global count
buf[count:count + len(data)] = data
count += len(data)
def flush():
global count
sent, count = bytes(buf[:count]), 0 # old bytes stay in buf
return sent
def write_zeros(n, fixed):
global count
if fixed or count > 0:
buf[count:count + n] = bytes(n) # wipe before reuse
count += n
write(b"shop1 PH 98xx01"); flush() # statement A, then a flush
write_zeros(15, fixed=False); print(flush()) # padding for shop 2, old driver
write_zeros(15, fixed=True); print(flush()) # padding for shop 2, fixed driverRun it with python3 and it prints two lines. The first is b'shop1 PH 98xx01', shop 1's data sent as shop 2's padding. The second is 15 zero bytes, which is what 42.7.14 sends.
What just happened: the flush reset the count of waiting bytes but left the memory alone. The old check only wiped when the count was above zero, so the stale bytes went out.
How one shop's data ended up inside another shop's PDF
Drag to exploreNotice the box where the buffer is empty: that one state skips the wipe, and every step runs on one shared, reused connection.
Diagram connections:
- Statement A sent to Buffer flushed (Data ends at buffer edge)
- Buffer flushed to Short PDF upload (Next request, same connection)
- Short PDF upload to writeZeros, empty buffer (1,000 bytes to pad)
- writeZeros, empty buffer to Old bytes sent as padding (No wipe)
- Old bytes sent as padding to Stored in invoice_pdfs (Server stores the value)
- Statement A sent: An INSERT with shop 1's phone number
- Buffer flushed: Count reset to zero, old bytes stay in memory
- Short PDF upload: Declared 50,000 bytes, only 49,000 arrive
- writeZeros, empty buffer: 42.7.4 to 42.7.13 skip the wipe
- Old bytes sent as padding: Up to 8,192 bytes with default settings
- Stored in invoice_pdfs: Shop 2's PDF now holds shop 1's data
Driver calls that declare a length
The advisory lists these entry points, and the driver accepts them without an error even when the data runs short.
- PreparedStatement.setObject with a ByteStreamWriter, a pgjdbc helper that streams bytes, when getLength() promises more than writeTo() writes.
- CopyIn.writeToCopy with a ByteStreamWriter or a byte array, and PGCopyOutputStream.write for writes over 64 KiB.
- LargeObject.write, and java.sql.Blob.setBytes, which calls it. Large objects are PostgreSQL's way to store big binary files.
For the byte-array calls, the trigger is a length that runs past the end of the array. The team's PDF service took its declared length from the upload's Content-Length header, the size the uploading client said it would send.
How much can leak, and why range checks are not enough
With default settings, each padded value can carry up to 8,192 bytes of earlier traffic. That is the default maxSendBufferSize, a connection property added in 42.7.4.
With GSS encryption turned on through gssEncMode, the advisory says padding can carry up to 16,320 bytes with MIT Kerberos. On a connection pool, a shared set of open connections, those bytes can come from other users' requests.
The advisory also explains why range checks alone would not fix it. A range check rejects an offset or length that runs past the end of a byte array. It cannot catch a ByteStreamWriter that writes less than it promised, because the driver has already sent the length. So the fix zeroes the buffer from its start up to the padding length, or all of it if the padding is longer.
Which pgjdbc versions need an upgrade, and what should you check?
Every version from 42.7.4 to 42.7.13 needs 42.7.14 for the padding bug, and 42.7.11 to 42.7.13 also have the requireAuth bug. Upgrading fixes the driver, but two checks remain in your own code and data.
Find the version you really run
Check the resolved dependency tree for org.postgresql:postgresql, pgjdbc's name in Maven Central, Java's main package registry. Framework dependency management or another library can pick a version for you, and app servers may hold their own copy.
Upgrade to 42.7.14
Pin 42.7.14 or later and redeploy every service, including batch jobs and admin tools.
Search every config for requireAuth
Check JDBC URLs, properties files, environment variables and secret stores. Replace all-excluded or comma-only values with a positive list.
Lock down the network path
Use sslmode=verify-full, with the server's CA certificate on the client. Where the server offers SCRAM-SHA-256-PLUS, the channel-binding form of SCRAM, add channelBinding=require.
Audit declared lengths
Find the calls listed above. Make each declared length match the bytes you send, and reject short uploads early.
Review values already stored
Rows written through those calls by 42.7.4 to 42.7.13 may hold leaked traffic. The next section covers what to do.
How to confirm the version you ship
In Gradle, run gradle -q dependencies --configuration runtimeClasspath (or ./gradlew with the same arguments) and look for the postgresql line. A fixed build shows org.postgresql:postgresql:42.7.14.
If your build asks for 42.7.14 and another library asks for an older one, Gradle prints both on one line with an arrow between them. The older one comes first and the version it chose second, here 42.7.12 then 42.7.14. A line with no arrow is the version you ship. When only a library brought in 42.7.12, our stub project on Gradle 8.14.3 printed a plain org.postgresql:postgresql:42.7.12, which still needs the upgrade.
In Maven, mvn dependency:tree -Dincludes=org.postgresql:postgresql prints a line in the form org.postgresql:postgresql:jar:VERSION:SCOPE, per the Maven dependency plugin docs.
A requireAuth audit you can run
This script checks requireAuth values against the advisory and the parser in pgjdbc's 42.7.13 source, AuthMethod.parseRequireAuth. Copy the values from your configs into service_values before running it.
METHODS = {"password", "md5", "gss", "sspi", "scram-sha-256", "none"}
def split_like_java(value):
parts = value.split(",")
if "," in value: # Java drops trailing empty pieces
while parts and parts[-1] == "":
parts.pop()
return [p.strip() for p in parts]
def check_require_auth(value):
parts = split_like_java(value)
if not parts:
return "BAD: names no method, fails open on 42.7.11 to 42.7.13"
negative = parts[0].startswith("!")
names = []
for p in parts:
if p.startswith("!") != negative:
return "ERROR: mixes allowed and ! names, the driver refuses to connect"
name = p[1:] if negative else p
if name not in METHODS or name in names:
return "ERROR: unknown or repeated name " + repr(name) + ", the driver refuses to connect"
names.append(name)
if not negative:
return "OK: positive list " + ", ".join(names)
if set(names) == METHODS:
return "BAD: blocks every method, fails open on 42.7.11 to 42.7.13"
return "OK, but a positive list is clearer: blocks " + ", ".join(names)
service_values = {
"invoice-api": "scram-sha-256",
"reports": ",",
"pdf-store": "!password,!md5,!gss,!sspi,!scram-sha-256,!none",
"billing": "scram-sha-256,!password",
"exports": "!password, !md5",
"search": "SCRAM-SHA-256",
}
for service, value in service_values.items():
print(service, "->", check_require_auth(value))On these sample values it prints six lines. It flags reports and pdf-store as BAD, because both fail open on 42.7.11 to 42.7.13. The billing and search values get ERROR, because the driver already refuses them. It passes invoice-api and suggests a positive list for exports.
We compiled AuthMethod.java from the 42.7.13 tag and ran it on these values. Method names are case-sensitive, so SCRAM-SHA-256 is rejected with SQLState 22023. Only a value made entirely of commas names no method, and an empty string is rejected with the same error. These details are undocumented as of 19 October 2026, so pin your version.
What should you do about data already stored by affected pgjdbc versions?
Treat any value written through a short declared length as possibly holding other people's data. The advisory warns that values already stored by an affected version can contain this traffic, and upgrading does not clean them.
Start with the code paths your length audit found. Where you know a value's true length, compare it with the stored length. Any extra bytes at the end are padding and may hold leaked traffic.
In the team's case, about 1 in 2,000 PDF uploads (made up) had been cut short. They trimmed those files, told the affected shops and made uploads reject short streams.
How do you explain the PostgreSQL JDBC vulnerability in a system design interview?
Treat the two bugs as case studies in safe configuration and memory reuse. Interviewers care more about the patterns than the CVE numbers.
State each bug in one line
A rule excluding every method was treated as no rule. Padding reused old send-buffer bytes, which the database stored.
Name the root causes
One null value meant both unset and nothing allowed. A reused buffer was not wiped when it was empty on entry.
Give the design rules
Contradictory settings fail closed and loudly. Reused buffers are cleared before reuse, whatever state they are in.
Add defence in depth
Add a second guard that holds if one layer fails. Verify the server with sslmode=verify-full or channel binding, so the requireAuth bug alone cannot hand your password to a fake server.
Close with the response plan
Upgrade, search configs, fix declared lengths and check data written in the affected window.
Questions people ask
- Am I affected by CVE-2026-107314 if I never set requireAuth?
- No. The property has no default, and the advisory says deployments that do not set it are unaffected. Positive lists and partial exclusions were also enforced. Only values excluding all six methods, or naming no method, failed open on 42.7.11 to 42.7.13.
- Will upgrading to pgjdbc 42.7.14 break my application?
- Only where a service has an all-excluded or comma-only requireAuth value. Such connections now fail with SQLState 08004, or 22023 for a value with no method. Replace the value with a positive list of the methods your server uses, such as scram-sha-256.
- Does TLS protect me from the requireAuth bug?
- Only if the driver verifies the server. pgjdbc's docs say sslmode prefer and require do not validate the server certificate, so a fake server can still complete TLS. The advisory recommends sslmode=verify-full, and channelBinding=require for SCRAM over TLS.
- Who can exploit the padding bug, CVE-2026-107315?
- Someone who can make your app store a value shorter than its declared length and then read it back. The CVSS vector says low privileges and high complexity. Apps whose declared lengths always match the data are not affected, per the advisory.
- Is psqlODBC, the ODBC driver, affected by the same bugs?
- No. psqlODBC is a separate driver for ODBC, another standard database interface, and its 14 October 2026 post does not mention these bugs. That post covers a different flaw, CVE-2026-109448, fixed in psqlODBC 18.00.0005. The driver kept the parameter count in a signed 16-bit field. The post says binding more than 32,768 parameters on one statement could corrupt memory.
- Is an older version like 42.7.3 safe from the padding bug?
- The advisory says 42.7.3 and earlier pad with zeros, so they do not leak traffic through padding. They also predate the requireAuth bug. That is no reason to stay there, because older releases miss other fixes. Move to 42.7.14.
Sources and further reading
- pgjdbc advisory GHSA-rhp9-mr79-r74h (CVE-2026-107314)
- pgjdbc advisory GHSA-f64h-wr5q-3qf3 (CVE-2026-107315)
- PostgreSQL JDBC Driver 42.7.14 release notes
- PostgreSQL.org news: PostgreSQL JDBC 42.7.14 security update
- pgjdbc documentation: Using SSL
- pgjdbc documentation: Initializing the driver and connection parameters
- PostgreSQL documentation: Message flow (authentication requests)
- PostgreSQL documentation: Message formats (Bind parameter lengths)
- PostgreSQL documentation: Error codes (08004, 22023)
- Maven dependency plugin: Filtering the dependency tree
- PostgreSQL.org news: psqlODBC 18.00.0005 security fix for CVE-2026-109448