Introduction
A stack buffer overflow and a SQL injection in PostgreSQL's refint module give an unprivileged database user a path to arbitrary code execution at the operating system level. The vulnerability carries a CVSS score of 8.8. The flawed code is example code in contrib/spi, which ships with every PostgreSQL installation. The vendor describes contrib/spi as sample code, but it has found its way into production environments. There it is a serious risk on every supported major version from 14 through 18.
Technical Information
Vulnerable Component and Root Cause
The vulnerability is in the check_foreign_key() trigger function in contrib/spi/refint.c. This module implements referential-integrity checks through PostgreSQL's Server Programming Interface (SPI). According to the release notes, the function was insufficiently careful about quoting key values and used fixed-length buffers to construct queries. The way it built SQL statements dynamically had two separate flaws.
Flaw 1: Fixed-Size Stack Buffer (Stack Buffer Overflow, CWE-121)
The old code allocated a fixed 8 KB stack buffer to hold internally generated SQL queries:
char sql[8192];
It then built each query with repeated calls of this form:
snprintf(sql + strlen(sql), sizeof(sql) - strlen(sql), ...);
The query's size depends on user-controlled key values and column names. If it grew past 8192 bytes, the writes would overflow the stack buffer. That allows arbitrary code execution as the OS-level user that runs the PostgreSQL database service. The attacker needs only to be an unprivileged database user who can supply long enough key values to the trigger function.
Flaw 2: Missing Value Escaping (SQL Injection, CWE-89)
To build UPDATE queries for cascade actions, the old code decided whether a column needed quoting by comparing its type name against a hardcoded list (text, varchar, char, bpchar, date, timestamp). If the type matched, the code wrapped the value in bare single quotes and did no escaping at all. The vulnerable code path looked like this:
snprintf(sql + strlen(sql), sizeof(sql) - strlen(sql), " %s = %s%s%s %s ", args2[k], (is_char_type > 0) ? "'" : "", nv, (is_char_type > 0) ? "'" : "", (k < nkeys) ? ", " : "");
A value like '; DROP TABLE important; -- would go straight into the SQL string, so an attacker who controls the primary-key update value could execute arbitrary SQL. This second vector is exploitable when an application declares a user-controlled column as a refint cascade primary key and allows user-controlled updates to that column. The attacker's SQL then runs as the database user performing the primary-key update.
Attack Flow
- The attacker finds a PostgreSQL deployment that uses the
refintmodule, with cascade primary-key triggers configured on a table whose key values the attacker can influence. - For the buffer overflow path: The attacker supplies a key value long enough to exceed the 8192-byte buffer, through a normal database operation that fires the
check_foreign_key()trigger. The overflow corrupts the stack. With a crafted payload, the attacker gets code execution as the OS user running PostgreSQL. - For the SQL injection path: The attacker supplies a crafted primary-key update value that contains SQL metacharacters, such as single quotes followed by arbitrary SQL. The
is_char_typelogic wraps the value in bare quotes without escaping, so the injected SQL runs with the privileges of the database user performing the update.
Patch Information
The PostgreSQL Global Development Group fixed CVE-2026-6637 in a single commit (1ebda7da9), authored by Nathan Bossart and committed by Noah Misch on May 11, 2026. The fix was backported through the PostgreSQL 14 branch and published on May 14, 2026, in minor releases 18.4, 17.10, 16.14, 15.18, and 14.23.
The patch makes two changes.
Dynamic Buffer Allocation via StringInfo
The patch replaces every fixed-size char sql[8192] buffer with PostgreSQL's StringInfoData abstraction, initialized with initStringInfo(&sql). This buffer grows as needed, which removes the overflow risk entirely. Query fragments are now appended with appendStringInfo() and appendStringInfoString() instead of the fragile snprintf + strlen chain. The code frees the dynamically allocated memory after use with pfree(sql.data).
Proper Literal Quoting via quote_literal_cstr()
The patch removes the whole block of manual type-checking code (the is_char_type logic and the associated strcmp calls for text, varchar, etc.). New key values now go through quote_literal_cstr(nv), PostgreSQL's standard C utility for safely escaping and quoting literal values in SQL. That one call handles every data type correctly and escapes special characters, which closes the SQL injection vector.
The old vulnerable code in the cascade UPDATE path:
snprintf(sql + strlen(sql), sizeof(sql) - strlen(sql), " %s = %s%s%s %s ", args2[k], (is_char_type > 0) ? "'" : "", nv, (is_char_type > 0) ? "'" : "", (k < nkeys) ? ", " : "");
was replaced with:
appendStringInfo(&sql, " %s = %s ", args2[k], quote_literal_cstr(nv)); if (k < nkeys) appendStringInfoString(&sql, ", ");
The patch applies the same fixes to check_primary_key(), although that function was vulnerable only to the buffer overflow. It used parameterized queries for values rather than string interpolation. In both functions, the for loops also change from for (i = 0; i < nkeys; i++) to for (i = 1; i <= nkeys; i++). That matches the 1-based indexing of the argument arrays and simplifies the offset arithmetic.
Affected Systems and Versions
The vulnerability affects every PostgreSQL installation that uses the refint module in these version ranges:
| Major Version | Vulnerable Versions | Fixed Version | Fix Publication Date |
|---|---|---|---|
| 18 | Before 18.4 | 18.4 | 2026-05-14 |
| 17 | Before 17.10 | 17.10 | 2026-05-14 |
| 16 | Before 16.14 | 16.14 | 2026-05-14 |
| 15 | Before 15.18 | 15.18 | 2026-05-14 |
| 14 | Before 14.23 | 14.23 | 2026-05-14 |
The vulnerable configuration requires the contrib/spi module (refint) to be active. The SQL injection vector also requires the application to declare a user-controlled column as a refint cascade primary key and to allow user-controlled updates to that column.



