Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wFExf-004sOm-1e for pgsql-bugs@arkaria.postgresql.org; Tue, 21 Apr 2026 17:32:35 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1wFExd-009vA1-1p for pgsql-bugs@arkaria.postgresql.org; Tue, 21 Apr 2026 17:32:33 +0000 Received: from magus.postgresql.org ([2a02:c0:301:0:ffff::29]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wFExd-009v9s-10 for pgsql-bugs@lists.postgresql.org; Tue, 21 Apr 2026 17:32:33 +0000 Received: from sss.pgh.pa.us ([68.162.161.243]) by magus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.98.2) (envelope-from ) id 1wFExa-00000002KmR-3p7J for pgsql-bugs@lists.postgresql.org; Tue, 21 Apr 2026 17:32:33 +0000 Received: from sss1.sss.pgh.pa.us (localhost [127.0.0.1]) by sss.pgh.pa.us (8.15.2/8.15.2) with ESMTP id 63LHWOMP641712 for ; Tue, 21 Apr 2026 13:32:24 -0400 From: Tom Lane To: pgsql-bugs@lists.postgresql.org Subject: Potential buffer overrun in spell.c's CheckAffix() MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="----- =_aaaaaaaaaa0" Content-ID: <641653.1776792703.0@sss.pgh.pa.us> Date: Tue, 21 Apr 2026 13:32:24 -0400 Message-ID: <641711.1776792744@sss.pgh.pa.us> List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk ------- =_aaaaaaaaaa0 Content-Type: text/plain; charset="us-ascii" Content-ID: <641653.1776792703.1@sss.pgh.pa.us> CheckAffix is used by our ispell text search dictionaries to attach a prefix or suffix to a given base word. The input word is known to be no longer than MAXNORMLEN (256), and an output buffer of size MAXNORMLEN * 2 is provided. But there's not any a-priori limit on the length of a prefix or suffix string, so in principle a buffer overflow could occur. In practice these limits seem like more than plenty for any real-world word, so I think it's sufficient to just reject the prefix or suffix if an overflow would occur, as attached. This bug was reported to pgsql-security by Xint Code as a potential security issue. However we decided it doesn't seem worth the CVE treatment, because exploiting it would require getting a malicious ispell dictionary installed in a PG server. Putting the .dict file into the installation's file tree would require superuser privileges, and so would creating a text dictionary SQL object that references it. Maybe an attacker could persuade a gullible DBA to do that, but there are plenty of other attack pathways available if you're that persuasive. Despite that, it seems worth fixing as a run-of-the-mill bug. Any objections to the attached? regards, tom lane ------- =_aaaaaaaaaa0 Content-Type: text/x-diff; name*0="v1-0001-Prevent-buffer-overrun-in-spell.c-s-CheckAffix.patc"; name*1="h"; charset="us-ascii" Content-ID: <641653.1776792703.2@sss.pgh.pa.us> Content-Description: v1-0001-Prevent-buffer-overrun-in-spell.c-s-CheckAffix.patch Content-Transfer-Encoding: quoted-printable =46rom 977c82d2501465789ccda052f0b718183e89d816 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Sun, 12 Apr 2026 20:42:15 -0400 Subject: [PATCH v1] Prevent buffer overrun in spell.c's CheckAffix(). This function writes into a caller-supplied buffer of length 2 * MAXNORMLEN, which should be plenty in real-world cases. However a malicious affix file could supply an affix long enough to overrun that. Defend by just rejecting the match if it would overrun the buffer. I also inserted checks of the input word length against Affix->replen, just to be sure we won't index off the buffer, though it would be caller error for that not to be true. The lack of documentation in this code makes my head hurt, so I also reverse-engineered a basic header comment for CheckAffix. Reported-by: Xint Code Author: Tom Lane Backpatch-through: 14 --- src/backend/tsearch/spell.c | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/backend/tsearch/spell.c b/src/backend/tsearch/spell.c index a1bfd2a9f9b..79a2a459406 100644 --- a/src/backend/tsearch/spell.c +++ b/src/backend/tsearch/spell.c @@ -2065,9 +2065,29 @@ FindAffixes(AffixNode *node, const char *word, int = wrdlen, int *level, int type) return NULL; } = +/* + * Checks to see if affix applies to word, transforms word if so. + * + * word: input word + * len: length of input word + * Affix: affix to consider + * flagflags: context flags showing whether we are handling a compound wo= rd + * newword: output buffer (MUST be of length 2 * MAXNORMLEN) + * baselen: input/output argument + * + * If baselen isn't NULL, then *baselen is used to return the length of + * the non-changed part of the word when applying a suffix, and is used + * to detect whether the input contained only a prefix and suffix when + * later applying a prefix. + * + * Returns newword on success, or NULL if the affix can't be applied. + * On success, the modified word is stored into newword. + */ static char * CheckAffix(const char *word, size_t len, AFFIX *Affix, int flagflags, cha= r *newword, int *baselen) { + size_t findlen; + /* * Check compound allow flags */ @@ -2103,8 +2123,15 @@ CheckAffix(const char *word, size_t len, AFFIX *Aff= ix, int flagflags, char *neww /* * make replace pattern of affix */ + Assert(len =3D=3D strlen(word)); + findlen =3D strlen(Affix->find); if (Affix->type =3D=3D FF_SUFFIX) { + /* protect against buffer overrun */ + if (len < Affix->replen || len >=3D 2 * MAXNORMLEN || + len - Affix->replen + findlen >=3D 2 * MAXNORMLEN) + return NULL; + strcpy(newword, word); strcpy(newword + len - Affix->replen, Affix->find); if (baselen) /* store length of non-changed part of word */ @@ -2112,11 +2139,16 @@ CheckAffix(const char *word, size_t len, AFFIX *Af= fix, int flagflags, char *neww } else { + /* protect against buffer overrun */ + if (len < Affix->replen || + findlen + len - Affix->replen >=3D 2 * MAXNORMLEN) + return NULL; + /* * if prefix is an all non-changed part's length then all word * contains only prefix and suffix, so out */ - if (baselen && *baselen + strlen(Affix->find) <=3D Affix->replen) + if (baselen && *baselen + findlen <=3D Affix->replen) return NULL; strcpy(newword, Affix->find); strcat(newword, word + Affix->replen); -- = 2.43.7 ------- =_aaaaaaaaaa0--