Fix incautious handling of possibly-miscoded strings in client code.
authorTom Lane
Mon, 7 Jun 2021 18:15:25 +0000 (14:15 -0400)
committerTom Lane
Mon, 7 Jun 2021 18:15:25 +0000 (14:15 -0400)
An incorrectly-encoded multibyte character near the end of a string
could cause various processing loops to run past the string's
terminating NUL, with results ranging from no detectable issue to
a program crash, depending on what happens to be in the following
memory.

This isn't an issue in the server, because we take care to verify
the encoding of strings before doing any interesting processing
on them.  However, that lack of care leaked into client-side code
which shouldn't assume that anyone has validated the encoding of
its input.

Although this is certainly a bug worth fixing, the PG security team
elected not to regard it as a security issue, primarily because
any untrusted text should be sanitized by PQescapeLiteral or
the like before being incorporated into a SQL or psql command.
(If an app fails to do so, the same technique can be used to
cause SQL injection, with probably much more dire consequences
than a mere client-program crash.)  Those functions were already
made proof against this class of problem, cf CVE-2006-2313.

To fix, invent PQmblenBounded() which is like PQmblen() except it
won't return more than the number of bytes remaining in the string.
In HEAD we can make this a new libpq function, as PQmblen() is.
It seems imprudent to change libpq's API in stable branches though,
so in the back branches define PQmblenBounded as a macro in the files
that need it.  (Note that just changing PQmblen's behavior would not
be a good idea; notably, it would completely break the escaping
functions' defense against this exact problem.  So we just want a
version for those callers that don't have any better way of handling
this issue.)

Per private report from houjingyi.  Back-patch to all supported branches.

15 files changed:
src/bin/psql/common.c
src/bin/psql/psqlscanslash.l
src/bin/psql/stringutils.c
src/bin/psql/tab-complete.c
src/bin/scripts/common.c
src/common/jsonapi.c
src/common/wchar.c
src/fe_utils/print.c
src/fe_utils/string_utils.c
src/include/mb/pg_wchar.h
src/interfaces/libpq/exports.txt
src/interfaces/libpq/fe-misc.c
src/interfaces/libpq/fe-print.c
src/interfaces/libpq/fe-protocol3.c
src/interfaces/libpq/libpq-fe.h

index 7a95465111ad1f1e3485350f680053ecb28df820..9a00499510929c97925fb053fbcc81c557808322 100644 (file)
@@ -1846,7 +1846,7 @@ skip_white_space(const char *query)
 
    while (*query)
    {
-       int         mblen = PQmblen(query, pset.encoding);
+       int         mblen = PQmblenBounded(query, pset.encoding);
 
        /*
         * Note: we assume the encoding is a superset of ASCII, so that for
@@ -1883,7 +1883,7 @@ skip_white_space(const char *query)
                    query++;
                    break;
                }
-               query += PQmblen(query, pset.encoding);
+               query += PQmblenBounded(query, pset.encoding);
            }
        }
        else if (cnestlevel > 0)
@@ -1918,7 +1918,7 @@ command_no_begin(const char *query)
     */
    wordlen = 0;
    while (isalpha((unsigned char) query[wordlen]))
-       wordlen += PQmblen(&query[wordlen], pset.encoding);
+       wordlen += PQmblenBounded(&query[wordlen], pset.encoding);
 
    /*
     * Transaction control commands.  These should include every keyword that
@@ -1949,7 +1949,7 @@ command_no_begin(const char *query)
 
        wordlen = 0;
        while (isalpha((unsigned char) query[wordlen]))
-           wordlen += PQmblen(&query[wordlen], pset.encoding);
+           wordlen += PQmblenBounded(&query[wordlen], pset.encoding);
 
        if (wordlen == 11 && pg_strncasecmp(query, "transaction", 11) == 0)
            return true;
@@ -1983,7 +1983,7 @@ command_no_begin(const char *query)
 
        wordlen = 0;
        while (isalpha((unsigned char) query[wordlen]))
-           wordlen += PQmblen(&query[wordlen], pset.encoding);
+           wordlen += PQmblenBounded(&query[wordlen], pset.encoding);
 
        if (wordlen == 8 && pg_strncasecmp(query, "database", 8) == 0)
            return true;
@@ -1999,7 +1999,7 @@ command_no_begin(const char *query)
 
            wordlen = 0;
            while (isalpha((unsigned char) query[wordlen]))
-               wordlen += PQmblen(&query[wordlen], pset.encoding);
+               wordlen += PQmblenBounded(&query[wordlen], pset.encoding);
        }
 
        if (wordlen == 5 && pg_strncasecmp(query, "index", 5) == 0)
@@ -2010,7 +2010,7 @@ command_no_begin(const char *query)
 
            wordlen = 0;
            while (isalpha((unsigned char) query[wordlen]))
-               wordlen += PQmblen(&query[wordlen], pset.encoding);
+               wordlen += PQmblenBounded(&query[wordlen], pset.encoding);
 
            if (wordlen == 12 && pg_strncasecmp(query, "concurrently", 12) == 0)
                return true;
@@ -2027,7 +2027,7 @@ command_no_begin(const char *query)
 
        wordlen = 0;
        while (isalpha((unsigned char) query[wordlen]))
-           wordlen += PQmblen(&query[wordlen], pset.encoding);
+           wordlen += PQmblenBounded(&query[wordlen], pset.encoding);
 
        /* ALTER SYSTEM isn't allowed in xacts */
        if (wordlen == 6 && pg_strncasecmp(query, "system", 6) == 0)
@@ -2050,7 +2050,7 @@ command_no_begin(const char *query)
 
        wordlen = 0;
        while (isalpha((unsigned char) query[wordlen]))
-           wordlen += PQmblen(&query[wordlen], pset.encoding);
+           wordlen += PQmblenBounded(&query[wordlen], pset.encoding);
 
        if (wordlen == 8 && pg_strncasecmp(query, "database", 8) == 0)
            return true;
@@ -2065,7 +2065,7 @@ command_no_begin(const char *query)
            query = skip_white_space(query);
            wordlen = 0;
            while (isalpha((unsigned char) query[wordlen]))
-               wordlen += PQmblen(&query[wordlen], pset.encoding);
+               wordlen += PQmblenBounded(&query[wordlen], pset.encoding);
 
            /*
             * REINDEX [ TABLE | INDEX ] CONCURRENTLY are not allowed in
@@ -2084,7 +2084,7 @@ command_no_begin(const char *query)
 
            wordlen = 0;
            while (isalpha((unsigned char) query[wordlen]))
-               wordlen += PQmblen(&query[wordlen], pset.encoding);
+               wordlen += PQmblenBounded(&query[wordlen], pset.encoding);
 
            if (wordlen == 12 && pg_strncasecmp(query, "concurrently", 12) == 0)
                return true;
@@ -2104,7 +2104,7 @@ command_no_begin(const char *query)
 
        wordlen = 0;
        while (isalpha((unsigned char) query[wordlen]))
-           wordlen += PQmblen(&query[wordlen], pset.encoding);
+           wordlen += PQmblenBounded(&query[wordlen], pset.encoding);
 
        if (wordlen == 3 && pg_strncasecmp(query, "all", 3) == 0)
            return true;
@@ -2140,7 +2140,7 @@ is_select_command(const char *query)
     */
    wordlen = 0;
    while (isalpha((unsigned char) query[wordlen]))
-       wordlen += PQmblen(&query[wordlen], pset.encoding);
+       wordlen += PQmblenBounded(&query[wordlen], pset.encoding);
 
    if (wordlen == 6 && pg_strncasecmp(query, "select", 6) == 0)
        return true;
index 4bb18f132f4f46da92f7ab348e91a1a135cb5c39..51aa33e1611ef5047d9f2ff1b2cd5f91b8ccfc26 100644 (file)
@@ -753,7 +753,7 @@ dequote_downcase_identifier(char *str, bool downcase, int encoding)
        {
            if (downcase && !inquotes)
                *cp = pg_tolower((unsigned char) *cp);
-           cp += PQmblen(cp, encoding);
+           cp += PQmblenBounded(cp, encoding);
        }
    }
 }
index 0acc53801cb4a17ccb8c585e0e3f2fd878c38797..3a141cdc2b39367af988ebf6fac6bbb64ada0cbe 100644 (file)
@@ -143,7 +143,7 @@ strtokx(const char *s,
        /* okay, we have a quoted token, now scan for the closer */
        char        thisquote = *p++;
 
-       for (; *p; p += PQmblen(p, encoding))
+       for (; *p; p += PQmblenBounded(p, encoding))
        {
            if (*p == escape && p[1] != '\0')
                p++;            /* process escaped anything */
@@ -262,7 +262,7 @@ strip_quotes(char *source, char quote, char escape, int encoding)
        else if (c == escape && src[1] != '\0')
            src++;              /* process escaped character */
 
-       i = PQmblen(src, encoding);
+       i = PQmblenBounded(src, encoding);
        while (i--)
            *dst++ = *src++;
    }
@@ -324,7 +324,7 @@ quote_if_needed(const char *source, const char *entails_quote,
        else if (strchr(entails_quote, c))
            need_quotes = true;
 
-       i = PQmblen(src, encoding);
+       i = PQmblenBounded(src, encoding);
        while (i--)
            *dst++ = *src++;
    }
index 109b22acb6ba5385906e3e172805919c5ffeb75a..32c1bdfdca74312e74e98a58a27e687621307e6c 100644 (file)
@@ -4397,7 +4397,7 @@ _complete_from_query(const char *simple_query,
        while (*pstr)
        {
            char_length++;
-           pstr += PQmblen(pstr, pset.encoding);
+           pstr += PQmblenBounded(pstr, pset.encoding);
        }
 
        /* Free any prior result */
index c86c19eae28b3ca446c92e2deb1567e290b2b529..79cdc6cf330764ac01032bc2af22c9c4b3916431 100644 (file)
@@ -52,7 +52,7 @@ splitTableColumnsSpec(const char *spec, int encoding,
            cp++;
        }
        else
-           cp += PQmblen(cp, encoding);
+           cp += PQmblenBounded(cp, encoding);
    }
    *table = pnstrdup(spec, cp - spec);
    *columns = cp;
index 1bf38d7b4295e40355d5efe1fb24ae83f34ee10f..d376ab152d48d9e99e8cb2085f5e0348aced9046 100644 (file)
@@ -740,7 +740,7 @@ json_lex_string(JsonLexContext *lex)
                        ch = (ch * 16) + (*s - 'A') + 10;
                    else
                    {
-                       lex->token_terminator = s + pg_encoding_mblen(lex->input_encoding, s);
+                       lex->token_terminator = s + pg_encoding_mblen_bounded(lex->input_encoding, s);
                        return JSON_UNICODE_ESCAPE_FORMAT;
                    }
                }
@@ -846,7 +846,7 @@ json_lex_string(JsonLexContext *lex)
                    default:
                        /* Not a valid string escape, so signal error. */
                        lex->token_start = s;
-                       lex->token_terminator = s + pg_encoding_mblen(lex->input_encoding, s);
+                       lex->token_terminator = s + pg_encoding_mblen_bounded(lex->input_encoding, s);
                        return JSON_ESCAPING_INVALID;
                }
            }
@@ -860,7 +860,7 @@ json_lex_string(JsonLexContext *lex)
                 * shown it's not a performance win.
                 */
                lex->token_start = s;
-               lex->token_terminator = s + pg_encoding_mblen(lex->input_encoding, s);
+               lex->token_terminator = s + pg_encoding_mblen_bounded(lex->input_encoding, s);
                return JSON_ESCAPING_INVALID;
            }
 
index 6e7d731e020fee224865cf6323f57f737176e3b7..0636b8765ba358a382fccabab376adabe75e44c9 100644 (file)
@@ -1911,6 +1911,11 @@ const pg_wchar_tbl pg_wchar_table[] = {
 
 /*
  * Returns the byte length of a multibyte character.
+ *
+ * Caution: when dealing with text that is not certainly valid in the
+ * specified encoding, the result may exceed the actual remaining
+ * string length.  Callers that are not prepared to deal with that
+ * should use pg_encoding_mblen_bounded() instead.
  */
 int
 pg_encoding_mblen(int encoding, const char *mbstr)
@@ -1920,6 +1925,16 @@ pg_encoding_mblen(int encoding, const char *mbstr)
            pg_wchar_table[PG_SQL_ASCII].mblen((const unsigned char *) mbstr));
 }
 
+/*
+ * Returns the byte length of a multibyte character; but not more than
+ * the distance to end of string.
+ */
+int
+pg_encoding_mblen_bounded(int encoding, const char *mbstr)
+{
+   return strnlen(mbstr, pg_encoding_mblen(encoding, mbstr));
+}
+
 /*
  * Returns the display length of a multibyte character.
  */
index 273b1bfe4a49f9baec51533741c8545f33deaf9c..d48fcc4a0328de76bd6b13d6313805e54eca118f 100644 (file)
@@ -3636,6 +3636,9 @@ strlen_max_width(unsigned char *str, int *target_width, int encoding)
        curr_width += char_width;
 
        str += PQmblen((char *) str, encoding);
+
+       if (str > end)          /* Don't overrun invalid string */
+           str = end;
    }
 
    *target_width = curr_width;
index 5b206c7481d79de4c64c70385de6584c0d6fc628..3efee4e7eed7f4c5c94e74182151ca4a07d5fa6c 100644 (file)
@@ -1072,12 +1072,9 @@ patternToSQLRegex(int encoding, PQExpBuffer dbnamebuf, PQExpBuffer schemabuf,
                appendPQExpBufferChar(curbuf, '\\');
            else if (ch == '[' && cp[1] == ']')
                appendPQExpBufferChar(curbuf, '\\');
-           i = PQmblen(cp, encoding);
-           while (i-- && *cp)
-           {
-               appendPQExpBufferChar(curbuf, *cp);
-               cp++;
-           }
+           i = PQmblenBounded(cp, encoding);
+           while (i--)
+               appendPQExpBufferChar(curbuf, *cp++);
        }
    }
    appendPQExpBufferStr(curbuf, ")$");
index 0f31e683189d7d3b985e839f6ec12e99d36cc9b2..d93ccac263338c751347c3c5a25e56ef34127711 100644 (file)
@@ -574,6 +574,7 @@ extern int  pg_valid_server_encoding_id(int encoding);
  * earlier in this file are also available from libpgcommon.
  */
 extern int pg_encoding_mblen(int encoding, const char *mbstr);
+extern int pg_encoding_mblen_bounded(int encoding, const char *mbstr);
 extern int pg_encoding_dsplen(int encoding, const char *mbstr);
 extern int pg_encoding_verifymbchar(int encoding, const char *mbstr, int len);
 extern int pg_encoding_verifymbstr(int encoding, const char *mbstr, int len);
index a00701f2c5fe66047327515d0a4677a6f3012a86..9ef99f6de127bb5495ebfc03efad559be7f5fdcb 100644 (file)
@@ -184,3 +184,4 @@ PQexitPipelineMode        181
 PQpipelineSync            182
 PQpipelineStatus          183
 PQtraceSetFlags           184
+PQmblenBounded              185
index b347d7f8479375379ad21758365e5b6b18f51b0d..9a2a97029340f81f17b564e28aca08e57b0f3ed5 100644 (file)
@@ -1180,8 +1180,13 @@ pqSocketPoll(int sock, int forRead, int forWrite, time_t end_time)
  */
 
 /*
- * returns the byte length of the character beginning at s, using the
+ * Returns the byte length of the character beginning at s, using the
  * specified encoding.
+ *
+ * Caution: when dealing with text that is not certainly valid in the
+ * specified encoding, the result may exceed the actual remaining
+ * string length.  Callers that are not prepared to deal with that
+ * should use PQmblenBounded() instead.
  */
 int
 PQmblen(const char *s, int encoding)
@@ -1190,7 +1195,17 @@ PQmblen(const char *s, int encoding)
 }
 
 /*
- * returns the display length of the character beginning at s, using the
+ * Returns the byte length of the character beginning at s, using the
+ * specified encoding; but not more than the distance to end of string.
+ */
+int
+PQmblenBounded(const char *s, int encoding)
+{
+   return strnlen(s, pg_encoding_mblen(encoding, s));
+}
+
+/*
+ * Returns the display length of the character beginning at s, using the
  * specified encoding.
  */
 int
index 94219b1825bcbcabeee342f99c318aa1bf89210d..fc7d84844e104ff831164308b6ae900f6c968b98 100644 (file)
@@ -365,7 +365,7 @@ do_field(const PQprintOpt *po, const PGresult *res,
            /* Detect whether field contains non-numeric data */
            char        ch = '0';
 
-           for (p = pval; *p; p += PQmblen(p, res->client_encoding))
+           for (p = pval; *p; p += PQmblenBounded(p, res->client_encoding))
            {
                ch = *p;
                if (!((ch >= '0' && ch <= '9') ||
index b45fb7e70593a40edc9f4a54f13b1bc941fa7c73..2e8330534873ad8c01ba51de8310ea831f69e8ba 100644 (file)
@@ -1296,7 +1296,7 @@ reportErrorPosition(PQExpBuffer msg, const char *query, int loc, int encoding)
            if (w <= 0)
                w = 1;
            scroffset += w;
-           qoffset += pg_encoding_mblen(encoding, &wquery[qoffset]);
+           qoffset += PQmblenBounded(&wquery[qoffset], encoding);
        }
        else
        {
@@ -1364,7 +1364,7 @@ reportErrorPosition(PQExpBuffer msg, const char *query, int loc, int encoding)
         * width.
         */
        scroffset = 0;
-       for (; i < msg->len; i += pg_encoding_mblen(encoding, &msg->data[i]))
+       for (; i < msg->len; i += PQmblenBounded(&msg->data[i], encoding))
        {
            int         w = pg_encoding_dsplen(encoding, &msg->data[i]);
 
index 227adde5a5e420dcf44cb31e085f0498141cdd62..845b4c04c9c2f5159ee47fdc61022530a3c2f5d4 100644 (file)
@@ -625,6 +625,9 @@ extern int  PQlibVersion(void);
 /* Determine length of multibyte encoded char at *s */
 extern int PQmblen(const char *s, int encoding);
 
+/* Same, but not more than the distance to the end of string s */
+extern int PQmblenBounded(const char *s, int encoding);
+
 /* Determine display length of multibyte encoded char at *s */
 extern int PQdsplen(const char *s, int encoding);