From 86bdcf597fb8b1f820c77aabb6cc61a1ac9bb0a7 Mon Sep 17 00:00:00 2001
From: TJ Saunders <tj@castaglia.org>
Date: Tue, 3 Nov 2020 07:11:29 -0800
Subject: [PATCH] Issue #1149: Skip escaping of already-escaped SQL text.

The introduction of the Jot API added proper escaping of resolved text.
However, the mod_quotatab_sql module was already escaping some of its text
in INSERT statements (but, inconsistently, not in SELECT statements), thus
the Jot API refactoring caused a regression.
---
 contrib/mod_quotatab_sql.c                    | 46 +++++++++++-----
 contrib/mod_sql.c                             | 54 +++++++++++++++----
 .../ProFTPD/Tests/Modules/mod_quotatab_sql.pm |  2 +-
 3 files changed, 78 insertions(+), 24 deletions(-)

diff --git a/contrib/mod_quotatab_sql.c b/contrib/mod_quotatab_sql.c
index 5331a4f78d..dc15d73b2f 100644
--- a/contrib/mod_quotatab_sql.c
+++ b/contrib/mod_quotatab_sql.c
@@ -1,7 +1,7 @@
 /*
  * ProFTPD: mod_quotatab_sql -- a mod_quotatab sub-module for managing quota
  *                              data via SQL-based tables
- * Copyright (c) 2002-2017 TJ Saunders
+ * Copyright (c) 2002-2020 TJ Saunders
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -126,6 +126,11 @@ static int sqltab_create(quota_table_t *sqltab, void *ptr) {
    * files_in_used, files_out_used, files_xfer_used.
    */
 
+  /* NOTE: Per Issue #1149, we should NOT be adding the quotes to the
+   * text ourselves here.  It also makes the mod_quotatab_sql configuration
+   * inconsistent; the admin must quote these texts in the config for
+   * SELECTs, but not for INSERTs.
+   */
   pr_snprintf(tally_quota_name, 83, "'%s'",
     sqltab_get_name(tmp_pool, tally->name));
   tally_quota_name[82] = '\0';
@@ -237,7 +242,8 @@ static unsigned char sqltab_lookup(quota_table_t *sqltab, void *ptr,
   sql_res = pr_module_call(sql_cmdtab->m, sql_cmdtab->handler, sql_cmd);
 
   /* Check the results. */
-  if (!sql_res || MODRET_ISERROR(sql_res)) {
+  if (sql_res == NULL ||
+      MODRET_ISERROR(sql_res)) {
     quotatab_log("error processing NamedQuery '%s'", select_query);
     destroy_pool(tmp_pool);
     return FALSE;
@@ -300,33 +306,39 @@ static unsigned char sqltab_lookup(quota_table_t *sqltab, void *ptr,
     }
 
     tally->bytes_in_used = -1.0;
-    if (values[2])
+    if (values[2]) {
       tally->bytes_in_used = atof(values[2]);
+    }
 
     tally->bytes_out_used = -1.0;
-    if (values[3])
+    if (values[3]) {
       tally->bytes_out_used = atof(values[3]);
+    }
 
     tally->bytes_xfer_used = -1.0;
-    if (values[4])
+    if (values[4]) {
       tally->bytes_xfer_used = atof(values[4]);
+    }
 
     tally->files_in_used = 0;
-    if (values[5])
+    if (values[5]) {
       tally->files_in_used = atol(values[5]);
+    }
 
     tally->files_out_used = 0;
     if (values[6])
       tally->files_out_used = atol(values[6]);
 
     tally->files_xfer_used = 0;
-    if (values[7])
+    if (values[7]) {
       tally->files_xfer_used = atol(values[7]);
+    }
 
     destroy_pool(tmp_pool);
     return TRUE;
+  }
 
-  } else if (sqltab->tab_type == TYPE_LIMIT) {
+  if (sqltab->tab_type == TYPE_LIMIT) {
     quota_limit_t *limit = ptr;
     char **values = (char **) sql_data->elts;
 
@@ -397,28 +409,34 @@ static unsigned char sqltab_lookup(quota_table_t *sqltab, void *ptr,
     }
 
     limit->bytes_in_avail = -1.0;
-    if (values[4])
+    if (values[4]) {
       limit->bytes_in_avail = atof(values[4]);
+    }
 
     limit->bytes_out_avail = -1.0;
-    if (values[5])
+    if (values[5]) {
       limit->bytes_out_avail = atof(values[5]);
+    }
 
     limit->bytes_xfer_avail = -1.0;
-    if (values[6])
+    if (values[6]) {
       limit->bytes_xfer_avail = atof(values[6]);
+    }
 
     limit->files_in_avail = 0;
-    if (values[7])
+    if (values[7]) {
       limit->files_in_avail = atol(values[7]);
+    }
 
     limit->files_out_avail = 0;
-    if (values[8])
+    if (values[8]) {
       limit->files_out_avail = atol(values[8]);
+    }
 
     limit->files_xfer_avail = 0;
-    if (values[9])
+    if (values[9]) {
       limit->files_xfer_avail = atol(values[9]);
+    }
 
     destroy_pool(tmp_pool);
     return TRUE;
diff --git a/contrib/mod_sql.c b/contrib/mod_sql.c
index 4c4f669fc2..0081186c51 100644
--- a/contrib/mod_sql.c
+++ b/contrib/mod_sql.c
@@ -732,9 +732,28 @@ struct sql_resolved {
   int conn_flags;
 };
 
+static int is_escaped_text(const char *text, size_t text_len) {
+  register unsigned int i;
+
+  if (text[0] != '\'') {
+    return FALSE;
+  }
+
+  if (text[text_len-1] != '\'') {
+    return FALSE;
+  }
+
+  for (i = 1; i < text_len-1; i++) {
+    if (text[i] == '\'') {
+      return FALSE;
+    }
+  }
+
+  return TRUE;
+}
+
 static int sql_resolved_append_text(pool *p, struct sql_resolved *resolved,
     const char *text, size_t text_len) {
-  modret_t *mr;
   char *new_text;
   size_t new_textlen;
 
@@ -743,15 +762,32 @@ static int sql_resolved_append_text(pool *p, struct sql_resolved *resolved,
     return 0;
   }
 
-  mr = sql_dispatch(sql_make_cmd(p, 2, resolved->conn_name, text),
-    "sql_escapestring");
-  if (check_response(mr, resolved->conn_flags) < 0) {
-    errno = EIO;
-    return -1;
-  }
+  /* For backward compatibility (see Issue #1149), we indulge in a little
+   * heuristic here, and only escape the text if it hasn't already been
+   * escaped.  How to properly tell?  If the first and last characters of
+   * the given text are `'`, AND there are no other occurrences of that
+   * character in the text, assume it has already been quoted.
+   */
+  if (is_escaped_text(text, text_len) == FALSE) {
+    modret_t *mr;
+
+    mr = sql_dispatch(sql_make_cmd(p, 2, resolved->conn_name, text),
+      "sql_escapestring");
+    if (check_response(mr, resolved->conn_flags) < 0) {
+      errno = EIO;
+      return -1;
+    }
 
-  new_text = (char *) mr->data;
-  new_textlen = strlen(new_text);
+    new_text = (char *) mr->data;
+    new_textlen = strlen(new_text);
+
+  } else {
+    pr_trace_msg(trace_channel, 17,
+      "text '%s' is already escaped, skipping escaping it again", text);
+
+    new_text = (char *) text;
+    new_textlen = text_len;
+  }
 
   if (new_textlen > resolved->buflen) {
     new_textlen = resolved->buflen;
diff --git a/tests/t/lib/ProFTPD/Tests/Modules/mod_quotatab_sql.pm b/tests/t/lib/ProFTPD/Tests/Modules/mod_quotatab_sql.pm
index 49c5c20625..dd102fe181 100644
--- a/tests/t/lib/ProFTPD/Tests/Modules/mod_quotatab_sql.pm
+++ b/tests/t/lib/ProFTPD/Tests/Modules/mod_quotatab_sql.pm
@@ -1299,7 +1299,7 @@ EOS
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'DEFAULT:10',
+    Trace => 'DEFAULT:10 jot:30 sql:20',
 
     DefaultChdir => '~',
 
