Fix incautious handling of possibly-miscoded strings in client code.
authorTom Lane <[email protected]>
Mon, 7 Jun 2021 18:15:25 +0000 (14:15 -0400)
committerTom Lane <[email protected]>
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.

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/fe_utils/print.c
src/interfaces/libpq/fe-print.c
src/interfaces/libpq/fe-protocol3.c

index 4e342c180a6c187990c9c5bcf028ab0ed0323d0e..9a9dfa94ab1b9829d840a91e1486d14f072c767f 100644 (file)
@@ -29,6 +29,8 @@
 #include "fe_utils/mbprint.h"
 
 
+#define PQmblenBounded(s, e)  strnlen(s, PQmblen(s, e))
+
 static bool ExecQueryUsingCursor(const char *query, double *elapsed_msec);
 static bool command_no_begin(const char *query);
 static bool is_select_command(const char *query);
@@ -1805,7 +1807,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
@@ -1842,7 +1844,7 @@ skip_white_space(const char *query)
                    query++;
                    break;
                }
-               query += PQmblen(query, pset.encoding);
+               query += PQmblenBounded(query, pset.encoding);
            }
        }
        else if (cnestlevel > 0)
@@ -1877,7 +1879,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
@@ -1908,7 +1910,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;
@@ -1942,7 +1944,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;
@@ -1958,7 +1960,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)
@@ -1969,7 +1971,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;
@@ -1986,7 +1988,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)
@@ -2009,7 +2011,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;
@@ -2027,7 +2029,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;
@@ -2047,7 +2049,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;
@@ -2083,7 +2085,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 8d62fe29faf233b1136c1ea9155d247c49c87b52..244406cec809539fe855fbb19e4792b6a2a79531 100644 (file)
@@ -27,6 +27,8 @@
 %{
 #include "fe_utils/psqlscan_int.h"
 
+#define PQmblenBounded(s, e)  strnlen(s, PQmblen(s, e))
+
 /*
  * We must have a typedef YYSTYPE for yylex's first argument, but this lexer
  * doesn't presently make use of that argument, so just declare it as int.
@@ -730,7 +732,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 959381d085204ac964101e0d4e23f836ab21d11c..922a03e792f96540f3fba38e4cd40d95836c0e7f 100644 (file)
@@ -12,6 +12,8 @@
 #include "common.h"
 #include "stringutils.h"
 
+#define PQmblenBounded(s, e)  strnlen(s, PQmblen(s, e))
+
 
 /*
  * Replacement for strtok() (a.k.a. poor man's flex)
@@ -143,7 +145,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 +264,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++;
    }
@@ -322,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 08431ded1d2a099496d2815e6edcd2f0d17d6c54..c9cc9658e8b2875a53e9d0bd0d321448814766ad 100644 (file)
@@ -60,6 +60,8 @@ extern char *filename_completion_function();
 #define completion_matches rl_completion_matches
 #endif
 
+#define PQmblenBounded(s, e)  strnlen(s, PQmblen(s, e))
+
 /* word break characters */
 #define WORD_BREAKS        "\t\n@$><=;|&{() "
 
@@ -3787,7 +3789,7 @@ _complete_from_query(int is_schema_query, const char *text, int state)
        while (*pstr)
        {
            char_length++;
-           pstr += PQmblen(pstr, pset.encoding);
+           pstr += PQmblenBounded(pstr, pset.encoding);
        }
 
        /* Free any prior result */
index ad70ca4ff932d26e9c3391c789d311578c87b036..797a97f5dd5f883327f3bfaffc9e7c4c55d9a13a 100644 (file)
@@ -21,6 +21,8 @@
 #include "fe_utils/connect.h"
 #include "fe_utils/string_utils.h"
 
+#define PQmblenBounded(s, e)  strnlen(s, PQmblen(s, e))
+
 
 static PGcancel *volatile cancelConn = NULL;
 bool       CancelRequested = false;
@@ -303,7 +305,7 @@ split_table_columns_spec(const char *spec, int encoding,
            cp++;
        }
        else
-           cp += PQmblen(cp, encoding);
+           cp += PQmblenBounded(cp, encoding);
    }
    *table = pg_strdup(spec);
    (*table)[cp - spec] = '\0'; /* no strndup */
index f520114bc6c753cb5220b9fef24aeef4aa04980f..eb6a333178389a2aa8e5089ad72188494c9bdff0 100644 (file)
@@ -3526,6 +3526,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 89bc4c54299d0ecf0b2c06991a369f8462a87d83..a54b31d4161b3431a3934e0239baeaf8656c09e4 100644 (file)
@@ -36,6 +36,7 @@
 #include "libpq-fe.h"
 #include "libpq-int.h"
 
+#define PQmblenBounded(s, e)  strnlen(s, PQmblen(s, e))
 
 static void do_field(const PQprintOpt *po, const PGresult *res,
         const int i, const int j, const int fs_len,
@@ -358,7 +359,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 110c5988a2d08d83b06b92381518caca14a795ba..009a53725328c0542f5df8755910787916b8e9a4 100644 (file)
@@ -42,6 +42,8 @@
    ((id) == 'T' || (id) == 'D' || (id) == 'd' || (id) == 'V' || \
     (id) == 'E' || (id) == 'N' || (id) == 'A')
 
+#define PQmblenBounded(s, e)  strnlen(s, PQmblen(s, e))
+
 
 static void handleSyncLoss(PGconn *conn, char id, int msgLength);
 static int getRowDescriptions(PGconn *conn, int msgLength);
@@ -1228,7 +1230,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
        {
@@ -1296,7 +1298,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]);