Fix various quoting issues in sending query parameters to server.
authorHeikki Linnakangas <heikki.linnakangas@iki.fi>
Tue, 13 Jan 2015 21:29:48 +0000 (23:29 +0200)
committerHeikki Linnakangas <heikki.linnakangas@iki.fi>
Tue, 13 Jan 2015 22:50:18 +0000 (00:50 +0200)
The code used to just assume that if the parameter's SQL type is SQL_INTEGER
or SQL_SMALLINT, it doesn't need quoting. While that's true for valid
integers values, there were no safeguards that the bound string is actually
valid input. The server will just throw an error on invalid input, but we
need to quote it correctly to send it to the server in the first place.
For example, if the string "123, 'inject'" is bound to an SQL_INTEGER param,
we need to quote it or the server will interpret it as part of the query.

Also add a test case for this.

Reported by Jeremy Faith.

Makefile.am
convert.c
test/expected/param-conversions.out [new file with mode: 0644]
test/expected/param-conversions_1.out [new file with mode: 0644]
test/src/param-conversions-test.c [new file with mode: 0644]
test/tests

index d95049cca598c2ac3464f5c32e216f1b90c19c53..5740116e7ce79d632a3db6b6563285cc629721d3 100644 (file)
@@ -120,6 +120,9 @@ EXTRA_DIST = license.txt readme.txt readme_winbuild.txt \
    test/expected/numeric.out \
    test/expected/odbc-escapes.out \
    test/expected/params.out \
+   test/expected/params_1.out \
+   test/expected/param-conversions.out \
+   test/expected/param-conversions_1.out \
    test/expected/positioned-update.out \
    test/expected/prepare.out \
    test/expected/quotes.out \
@@ -167,6 +170,7 @@ EXTRA_DIST = license.txt readme.txt readme_winbuild.txt \
    test/src/numeric-test.c \
    test/src/odbc-escapes-test.c \
    test/src/params-test.c \
+   test/src/param-conversions-test.c \
    test/src/positioned-update-test.c \
    test/src/prepare-test.c \
    test/src/quotes-test.c \
index 269232c4d25a8db0cf316babff3fe6b1fe381238..ffdaa89311be5295f39fd227ef746766078aaea2 100644 (file)
--- a/convert.c
+++ b/convert.c
@@ -639,13 +639,15 @@ static char get_current_decimal_point(void)
  * decimal point from '.' to the correct decimal separator of the current
  * locale.
  */
-static void set_server_decimal_point(char *num)
+static void set_server_decimal_point(char *num, SQLLEN len)
 {
    char current_decimal_point = get_current_decimal_point();
    char *str;
+   SQLLEN i;
 
    if ('.' == current_decimal_point)
        return;
+   i = 0;
    for (str = num; '\0' != *str; str++)
    {
        if (*str == current_decimal_point)
@@ -653,6 +655,9 @@ static void set_server_decimal_point(char *num)
            *str = '.';
            break;
        }
+
+       if (len != SQL_NTS && i++ >= len)
+           break;
    }
 }
 
@@ -698,6 +703,34 @@ copy_and_convert_field_bindinfo(StatementClass *stmt, OID field_type, int atttyp
        LENADDR_SHIFT(bic->used, offset), LENADDR_SHIFT(bic->indicator, offset));
 }
 
+/*
+ * Is 'str' a valid integer literal, consisting only of ASCII characters
+ * 0-9 ?
+ *
+ * We don't check for overflow here. This is just to decide if we need to
+ * quote the value.
+ */
+static BOOL
+valid_int_literal(const char *str, SQLLEN len)
+{
+   SQLLEN i;
+
+   i = 0;
+
+   if (str[0] == '-')
+       i++;
+
+   for (; str[i] && (len == SQL_NTS || i < len); i++)
+   {
+       if (str[i] >= '0' && str[i] <= '9')
+           continue;
+
+       return FALSE;
+   }
+
+   return TRUE;
+}
+
 static double get_double_value(const char *str)
 {
    if (stricmp(str, NAN_STRING) == 0)
@@ -3862,6 +3895,7 @@ ResolveOneParam(QueryBuild *qb, QueryParse *qp, BOOL *isnull, BOOL *isbinary)
    SFLOAT      flv;
    SQL_INTERVAL_STRUCT *ivstruct;
    const char *ivsign;
+   BOOL        final_binary_convert = FALSE;
    RETCODE     retval = SQL_ERROR;
 
    *isnull = FALSE;
@@ -4110,7 +4144,7 @@ mylog("C_WCHAR=%s(%d)\n", buffer, used);
 #endif /* WIN32 */
            {
                sprintf(param_string, "%.*g", PG_DOUBLE_DIGITS, dbv);
-               set_server_decimal_point(param_string);
+               set_server_decimal_point(param_string, SQL_NTS);
            }
 #ifdef WIN32
            else if (_isnan(dbv))
@@ -4129,7 +4163,7 @@ mylog("C_WCHAR=%s(%d)\n", buffer, used);
 #endif /* WIN32 */
            {
                sprintf(param_string, "%.*g", PG_REAL_DIGITS, flv);
-               set_server_decimal_point(param_string);
+               set_server_decimal_point(param_string, SQL_NTS);
            }
 #ifdef WIN32
            else if (_isnan(flv))
@@ -4338,35 +4372,31 @@ mylog("cvt_null_date_string=%d pgtype=%d buf=%p\n", conn->connInfo.cvt_null_date
        retval = SQL_SUCCESS;
        goto cleanup;
    }
-   if (!req_bind)
+
+   /*
+    * We now have the value we want to print in one of these three canonical
+    * formats:
+    *
+    * 1. As a string in 'buf', with length indicated by 'used' (can be
+    *    SQL_NTS).
+    * 2. As a null-terminated string in 'param_string'.
+    * 3. Time-related fields in 'st'.
+    */
+
+   /*
+    * For simplicity, fold the param_string representation into 'buf'.
+    */
+   if (!buf && param_string[0])
    {
-       switch (param_sqltype)
-       {
-           case SQL_INTEGER:
-           case SQL_SMALLINT:
-               break;
-           case SQL_CHAR:
-           case SQL_VARCHAR:
-           case SQL_LONGVARCHAR:
-           case SQL_BINARY:
-           case SQL_VARBINARY:
-           case SQL_LONGVARBINARY:
-#ifdef UNICODE_SUPPORT
-           case SQL_WCHAR:
-           case SQL_WVARCHAR:
-           case SQL_WLONGVARCHAR:
-#endif /* UNICODE_SUPPORT */
-mylog("buf=%p flag=%d\n", buf, qb->flags);
-               if (buf && (qb->flags & FLGB_LITERAL_EXTENSION) != 0)
-               {
-                   CVT_APPEND_CHAR(qb, LITERAL_EXT);
-               }
-           default:
-               CVT_APPEND_CHAR(qb, LITERAL_QUOTE);
-               add_quote = TRUE;
-               break;
-       }
+       buf = param_string;
+       used = SQL_NTS;
    }
+
+   /*
+    * Do some further processing to create the final string we want to output.
+    * This will use the fields in 'st' to create a string if it's a time/date
+    * value, and do some other conversions.
+    */
    switch (param_sqltype)
    {
        case SQL_CHAR:
@@ -4378,43 +4408,37 @@ mylog("buf=%p flag=%d\n", buf, qb->flags);
        case SQL_WLONGVARCHAR:
 #endif /* UNICODE_SUPPORT */
 
+           /* Special handling for some column types */
            switch (param_pgtype)
            {
                case PG_TYPE_BOOL:
-                   /* consider True is -1 case */
-                   if (NULL != buf)
+                   /*
+                    * consider True is -1 case.
+                    *
+                    * FIXME: This actually matches anything that begins
+                    * with -1, like "-1234" or "-1foobar". Is that
+                    * intentional?
+                    */
+                   if (NULL != buf && '-' == buf[0] && '1' == buf[1])
                    {
-                       if ('-' == buf[0] &&
-                           '1' == buf[1])
-                           strcpy(buf, "1");
+                       buf = "1";
+                       used = 1;
                    }
-                   else if ('-' == param_string[0] &&
-                        '1' == param_string[1])
-                       strcpy(param_string, "1");
                    break;
                case PG_TYPE_FLOAT4:
                case PG_TYPE_FLOAT8:
                case PG_TYPE_NUMERIC:
                    if (NULL != buf)
-                       set_server_decimal_point(buf);
-                   else
-                       set_server_decimal_point(param_string);
+                       set_server_decimal_point(buf, used);
                    break;
            }
-           /* it was a SQL_C_CHAR */
-           if (buf)
-               CVT_SPECIAL_CHARS(qb, buf, used);
-           /* it was a numeric type */
-           else if (param_string[0] != '\0')
-               CVT_APPEND_STR(qb, param_string);
-
-           /* it was date,time,timestamp -- use m,d,y,hh,mm,ss */
-           else
+           if (!buf)
            {
+               /* it was date,time,timestamp -- use m,d,y,hh,mm,ss */
                snprintf(tmp, sizeof(tmp), "%.4d-%.2d-%.2d %.2d:%.2d:%.2d",
                        st.y, st.m, st.d, st.hh, st.mm, st.ss);
-
-               CVT_APPEND_STR(qb, tmp);
+               buf = tmp;
+               used = SQL_NTS;
            }
            break;
 
@@ -4431,7 +4455,8 @@ mylog("buf=%p flag=%d\n", buf, qb->flags);
            else
                sprintf(tmp, "%.4d-%.2d-%.2d", st.y, st.m, st.d);
            lastadd = "::date";
-           CVT_APPEND_STR(qb, tmp);
+           buf = tmp;
+           used = SQL_NTS;
            break;
 
        case SQL_TIME:
@@ -4444,7 +4469,8 @@ mylog("buf=%p flag=%d\n", buf, qb->flags);
 
            sprintf(tmp, "%.2d:%.2d:%.2d", st.hh, st.mm, st.ss);
            lastadd = "::time";
-           CVT_APPEND_STR(qb, tmp);
+           buf = tmp;
+           used = SQL_NTS;
            break;
 
        case SQL_TIMESTAMP:
@@ -4462,8 +4488,8 @@ mylog("buf=%p flag=%d\n", buf, qb->flags);
            /* Time zone stuff is unreliable */
            stime2timestamp(&st, tmp, sizeof(tmp), USE_ZONE, 6);
            lastadd = "::timestamp";
-           CVT_APPEND_STR(qb, tmp);
-
+           buf = tmp;
+           used = SQL_NTS;
            break;
 
        case SQL_BINARY:
@@ -4502,7 +4528,6 @@ mylog("buf=%p flag=%d\n", buf, qb->flags);
                if (0 != (qb->flags & FLGB_BINARY_AS_POSSIBLE))
                {
                    mylog("sending binary data leng=%d\n", used);
-                   CVT_APPEND_DATA(qb, buf, used);
                    *isbinary = TRUE;
                }
                else
@@ -4511,8 +4536,7 @@ mylog("buf=%p flag=%d\n", buf, qb->flags);
                     * converted to octal
                     */
                    mylog("SQL_VARBINARY: about to call convert_to_pgbinary, used = %d\n", used);
-
-                   CVT_APPEND_BINARY(qb, buf, used);
+                   final_binary_convert = TRUE;
                }
                break;
            }
@@ -4588,8 +4612,8 @@ mylog("buf=%p flag=%d\n", buf, qb->flags);
             */
            sprintf(param_string, "%u", lobj_oid);
            lastadd = "::lo";
-           CVT_APPEND_STR(qb, param_string);
-
+           buf = param_string;
+           used = SQL_NTS;
            break;
 
            /*
@@ -4599,49 +4623,67 @@ mylog("buf=%p flag=%d\n", buf, qb->flags);
            /* must be quoted (0 or 1 is ok to use inside the quotes) */
 
        case SQL_REAL:
-           if (buf)
-               my_strcpy(param_string, sizeof(param_string), buf, used);
-           set_server_decimal_point(param_string);
+           set_server_decimal_point(buf, used);
            lastadd = "::float4";
-           CVT_APPEND_STR(qb, param_string);
            break;
        case SQL_FLOAT:
        case SQL_DOUBLE:
-           if (buf)
-               my_strcpy(param_string, sizeof(param_string), buf, used);
-           set_server_decimal_point(param_string);
+           set_server_decimal_point(buf, used);
            lastadd = "::float8";
-           CVT_APPEND_STR(qb, param_string);
            break;
        case SQL_NUMERIC:
-           if (buf)
-           {
-               my_strcpy(cbuf, sizeof(cbuf), buf, used);
-           }
-           else
-               sprintf(cbuf, "%s", param_string);
-           CVT_APPEND_STR(qb, cbuf);
            break;
        default:            /* a numeric type or SQL_BIT */
+           break;
+   }
 
-           if (buf)
-           {
-               switch (used)
-               {
-                   case SQL_NULL_DATA:
-                       break;
-                   case SQL_NTS:
-                       CVT_APPEND_STR(qb, buf);
-                       break;
-                   default:
-                       CVT_APPEND_DATA(qb, buf, used);
-               }
-           }
-           else
-               CVT_APPEND_STR(qb, param_string);
+   /*
+    * Ok, we now have the final string representation in 'buf', length 'used'.
+    *
+    * Do we need to escape it?
+    */
+   if (!req_bind)
+   {
+       switch (param_sqltype)
+       {
+           case SQL_INTEGER:
+           case SQL_SMALLINT:
+               if (valid_int_literal(buf, used))
+                   break;
+               /* fall through */
+           case SQL_CHAR:
+           case SQL_VARCHAR:
+           case SQL_LONGVARCHAR:
+           case SQL_BINARY:
+           case SQL_VARBINARY:
+           case SQL_LONGVARBINARY:
+#ifdef UNICODE_SUPPORT
+           case SQL_WCHAR:
+           case SQL_WVARCHAR:
+           case SQL_WLONGVARCHAR:
+#endif /* UNICODE_SUPPORT */
+mylog("buf=%p flag=%d\n", buf, qb->flags);
+           default:
+               if ((qb->flags & FLGB_LITERAL_EXTENSION) != 0)
+                   CVT_APPEND_CHAR(qb, LITERAL_EXT);
+               CVT_APPEND_CHAR(qb, LITERAL_QUOTE);
+               add_quote = TRUE;
+               break;
+       }
+   }
 
-           break;
+   if (used == SQL_NTS)
+       used = strlen(buf);
+   if (add_quote)
+   {
+       if (final_binary_convert)
+           CVT_APPEND_BINARY(qb, buf, used);
+       else
+           CVT_SPECIAL_CHARS(qb, buf, used);
    }
+   else
+       CVT_APPEND_DATA(qb, buf, used);
+
    if (add_quote)
    {
        CVT_APPEND_CHAR(qb, LITERAL_QUOTE);
diff --git a/test/expected/param-conversions.out b/test/expected/param-conversions.out
new file mode 100644 (file)
index 0000000..f282396
--- /dev/null
@@ -0,0 +1,30 @@
+\! "./src/param-conversions-test"
+connected
+
+Testing conversions...
+Result set:
+0
+Result set:
+1
+Result set:
+0
+Result set:
+1
+SQLExecDirect failed
+22P02=ERROR: invalid input syntax for integer: "5 escapes: \ and '";
+Error while executing the query
+
+Testing conversions with invalid values...
+SQLExecDirect failed
+22P02=ERROR: invalid input syntax for integer: "2, 'injected, BAD!'";
+Error while executing the query
+SQLExecDirect failed
+22P02=ERROR: invalid input syntax for type numeric: "3', 'injected, BAD!', '1";
+Error while executing the query
+SQLExecDirect failed
+22P02=ERROR: invalid input syntax for type numeric: "4 \'bad', '1";
+Error while executing the query
+SQLExecDirect failed
+22003=ERROR: value "99999999999999999999999" is out of range for type integer;
+Error while executing the query
+disconnecting
diff --git a/test/expected/param-conversions_1.out b/test/expected/param-conversions_1.out
new file mode 100644 (file)
index 0000000..c75aee9
--- /dev/null
@@ -0,0 +1,29 @@
+\! "./src/param-conversions-test"
+connected
+
+Testing conversions...
+Result set:
+0
+Result set:
+1
+Result set:
+0
+Result set:
+1
+SQLExecDirect failed
+22P02=ERROR: invalid input syntax for integer: "5 escapes: \ and '";
+Error while executing the query
+
+Testing conversions with invalid values...
+SQLExecDirect failed
+22P02=ERROR: invalid input syntax for integer: "2, 'injected, BAD!'";
+Error while executing the query
+SQLExecDirect failed
+22P02=ERROR: invalid input syntax for type double precision: "3', 'injected, BAD!', '1";
+Error while executing the query
+SQLExecDirect failed
+22P02=ERROR: invalid input syntax for type double precision: "4 \'bad', '1";
+Error while executing the query
+Result set:
+0
+disconnecting
diff --git a/test/src/param-conversions-test.c b/test/src/param-conversions-test.c
new file mode 100644 (file)
index 0000000..47caee4
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+ * Test conversion of parameter values from C to SQL datatypes.
+ */
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "common.h"
+
+static void test_convert(const char *sql, SQLUSMALLINT c_type,
+                        SQLSMALLINT sql_type, char *value);
+
+static HSTMT hstmt = SQL_NULL_HSTMT;
+
+int main(int argc, char **argv)
+{
+   SQLRETURN rc;
+
+   test_connect();
+
+   rc = SQLAllocHandle(SQL_HANDLE_STMT, conn, &hstmt);
+   if (!SQL_SUCCEEDED(rc))
+   {
+       print_diag("failed to allocate stmt handle", SQL_HANDLE_DBC, conn);
+       exit(1);
+   }
+
+   /*** Test proper escaping of parameters  ***/
+   printf("\nTesting conversions...\n");
+
+   test_convert("SELECT 1 > ?", SQL_C_CHAR, SQL_INTEGER, "2");
+   test_convert("SELECT 1 > ?", SQL_C_CHAR, SQL_INTEGER, "-2");
+   test_convert("SELECT 2.2 > ?", SQL_C_CHAR, SQL_FLOAT, "2.3");
+   test_convert("SELECT 3.3 > ?", SQL_C_CHAR, SQL_DOUBLE, "3.01");
+   test_convert("SELECT 1 > ?", SQL_C_CHAR, SQL_CHAR, "5 escapes: \\ and '");
+
+   printf("\nTesting conversions with invalid values...\n");
+
+   test_convert("SELECT 2 > ?", SQL_C_CHAR, SQL_INTEGER, "2, 'injected, BAD!'");
+   test_convert("SELECT 1.3 > ?", SQL_C_CHAR, SQL_FLOAT, "3', 'injected, BAD!', '1");
+   test_convert("SELECT 1.4 > ?", SQL_C_CHAR, SQL_FLOAT, "4 \\'bad', '1");
+   test_convert("SELECT 1 > ?", SQL_C_CHAR, SQL_INTEGER, "99999999999999999999999");
+
+   /* Clean up */
+   test_disconnect();
+
+   return 0;
+}
+
+/*
+ * Execute a query with one parameter, with given C and SQL types. Print
+ * error or result.
+ */
+static void
+test_convert(const char *sql, SQLUSMALLINT c_type, SQLSMALLINT sql_type,
+             char *value)
+{
+   SQLRETURN rc;
+   SQLLEN cbParam = SQL_NTS;
+   int failed = 0;
+
+   /* a query with an SQL_INTEGER param. */
+   rc = SQLBindParameter(hstmt, 1, SQL_PARAM_INPUT,
+                         c_type,   /* value type */
+                         sql_type, /* param type */
+                         20,           /* column size */
+                         0,            /* dec digits */
+                         value,        /* param value ptr */
+                         0,            /* buffer len */
+                         &cbParam      /* StrLen_or_IndPtr */);
+   CHECK_STMT_RESULT(rc, "SQLBindParameter failed", hstmt);
+
+   rc = SQLExecDirect(hstmt, (SQLCHAR *) sql, SQL_NTS);
+   if (!SQL_SUCCEEDED(rc))
+   {
+       print_diag("SQLExecDirect failed", SQL_HANDLE_STMT, hstmt);
+       failed = 1;
+   }
+   else
+       print_result(hstmt);
+
+   rc = SQLFreeStmt(hstmt, SQL_CLOSE);
+   CHECK_STMT_RESULT(rc, "SQLFreeStmt failed", hstmt);
+
+   /*
+    * In error_on_rollback=0 mode, we don't currently recover from the error.
+    * I think that's a bug in the driver, but meanwhile, let's just force
+    * a rollback manually
+    */
+   if (failed)
+   {
+       rc = SQLExecDirect(hstmt, (SQLCHAR *) "ROLLBACK /* clean up after failed test */", SQL_NTS);
+       CHECK_STMT_RESULT(rc, "SQLExecDirect(ROLLBACK) failed", hstmt);
+   }
+}
+
index 5d359b74e996767908e9fe63080faa6eaacbf480..c3f415f88cd5fff9189fbc14f7ad8fa67835239a 100644 (file)
@@ -18,6 +18,7 @@ TESTBINS = src/connect-test \
    src/result-conversions-test \
    src/prepare-test \
    src/params-test \
+   src/param-conversions-test \
    src/notice-test \
    src/arraybinding-test \
    src/insertreturning-test \