Bind.Decode Negative Parameter Length Bypass in pgx/v5 (CWE-129)
Summary
github.com/jackc/pgx/v5 v5.8.0 contains a memory-safety vulnerability in the inlined pgproto3 package. A server-controlled Bind message can crash the client process immediately via an unrecoverable runtime panic.
| Package | github.com/jackc/pgx/v5 |
| File | pgproto3/bind.go |
| Affected Version | v5.8.0 (current stable) |
| CWE | CWE-129 — Improper Validation of Array Index |
| CVSS 3.1 | 7.5 High — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H |
Vulnerability Details
The parameter length field is read correctly as a signed integer:
msgSize := int(int32(binary.BigEndian.Uint32(src[rp:])))
The null sentinel check (msgSize == -1) is correct. However, the bounds check that follows is:
if len(src[rp:]) < msgSize {
return &invalidMessageFormatErr{...}
}
len() always returns a non-negative value. When msgSize is any negative value other than -1, the comparison is always false and the guard is bypassed entirely. Execution continues to:
dst.Parameters[i] = src[rp : rp+msgSize]
A negative slice upper bound causes an immediate, unrecoverable runtime panic:
panic: runtime error: slice bounds out of range [:-13619142]
Root Cause
The fix for this exact class of bug is already present in DataRow.Decode within the same package:
} else if valueLen < 0 {
return &invalidMessageFormatErr{messageType: "DataRow"}
}
The guard was applied to DataRow.Decode but not to Bind.Decode — a copy-paste omission that left the analogous code path unprotected.
Proof of Concept
A malicious or compromised PostgreSQL server sends a Bind message with a parameter length field set to any negative value other than -1 (for example 0xFFFFEB7A which decodes as int32 = -5254). The pgx client decodes the message, bypasses the bounds check, and panics immediately.
// Crafted wire bytes: 1 parameter, length = 0xFFFFEB7A
payload := []byte{
0x00, 0x00, 0x00, 0x00, // destination portal (empty string)
0x00, 0x00, 0x00, 0x00, // prepared statement (empty string)
0x00, 0x00, // format codes count = 0
0x00, 0x01, // parameter count = 1
0xFF, 0xFF, 0xEB, 0x7A, // parameter length = -5254 (not -1)
// no further bytes -- bounds check bypassed, panic triggered
}
Fix
A single additional condition in the existing bounds check:
// Before:
if len(src[rp:]) < msgSize {
// After:
if msgSize < 0 || len(src[rp:]) < msgSize {
This is directly consistent with the guard already present in DataRow.Decode.
Impact
- Any pgx/v5 client connecting to an attacker-controlled server is subject to immediate process termination.
- In microservice architectures where the database DSN can be influenced via environment variable injection, DNS poisoning, or SSRF to an internal endpoint, this enables unauthenticated denial-of-service.
- The panic is unrecoverable from normal application code unless every database call is wrapped in a
recover()goroutine, which is non-standard in Go applications. - No authentication, credentials, or prior access to the client are required — only the ability to route the client to an attacker-controlled PostgreSQL endpoint.