
PostgreSQL の全文検索(tsvector/tsquery)に見つかった範囲外書き込み脆弱性 CVE-2026-14662 を、修正前(18.4)と修正後(18.6)を Docker で並べて動かして検証した記録と発表資料
This is a record of running the pre-fix (18.4) and post-fix (18.6) versions of PostgreSQL simultaneously in Docker and actually observing the difference in behavior regarding CVE-2026-14662, which was found in PostgreSQL's full-text search data types (tsvector / tsquery).
It was compiled for security learning purposes, and the presentation materials are included as well.
| Item | Details |
|---|---|
| CVE | CVE-2026-14662 |
| Target | PostgreSQL's tsvector / tsquery |
| Type | Insufficient allocation size due to integer wraparound and out-of-bounds write |
| CVSS | 8.8 / 10.0 (High) |
| Publication date | 2026-08-13 |
| Fixed versions | 18.5, 17.11, 16.15, 15.19, 14.24 |
The chain of weaknesses takes the following form.
CWE-190 (Integer Overflow) → CWE-131 (Incorrect Calculation of Buffer Size) → CWE-787 (Out-of-bounds Write)
=Root cause =Final impact
.
├── docs/
│ └── presentation.md Presentation materials (vulnerability explanation + CWE explanation)
└── docker/
├── README.md How to use the reproduction environment
├── docker-compose.yml
├── init/ Common initialization SQL applied to both versions
├── test/ PoC and demo SQL
└── run.sh Start → run PoC → display results
If you have Docker and Docker Compose, you can reproduce it with just the following.
cd docker
./run.sh
The 18.4 and 18.6 containers start up, the same initialization SQL is loaded, and then the same PoC is run against both, displaying the results side by side. See docker/README.md for details.
| Test | PostgreSQL 18.4 (pre-fix) | PostgreSQL 18.6 (post-fix) |
|---|---|---|
| Repeatedly self-OR-combining tsquery | Succeeds without error up to 18 times (final 8,650,748 bytes) | ERROR: tsquery is too large on the 17th time |
| Word array with total length of 1,200,000 bytes | Succeeds | ERROR: string is too long for tsvector (1200000 bytes, max 1048575 bytes) |
| A single 3,000-byte word | Succeeds | ERROR: word is too long (3000 bytes, max 2046 bytes) |
Before the fix, data exceeding the internal representation limit (MAXSTRPOS = 1,048,575, derived from a 20-bit bit field) is accepted without producing a single error.
The reason 18.6 stops at exactly "the 17th time" can be explained by calculation, and it has been confirmed that the theoretical value and the measured value match byte for byte (see the presentation materials for details).
To check the diff, I used clones of the official PostgreSQL repository, but since they are large (185MB each), they are not included in this repository. If needed, obtain them as follows.
git clone --branch REL_18_4 --depth 1 https://github.com/postgres/postgres.git postgres-18.4
git clone --branch REL_18_6 --depth 1 https://github.com/postgres/postgres.git postgres-18.6
The files that received the fix are the following three.
src/backend/utils/adt/tsquery_util.csrc/backend/utils/adt/tsvector.csrc/backend/utils/adt/tsvector_op.c