080a160c7b9595d516c9c70e83689a09b60839d0 galt Mon Jun 3 12:16:53 2013 -0700 fix SQL Injection diff --git src/hg/lib/jksql.c src/hg/lib/jksql.c index 4dc7aba..986890c 100644 --- src/hg/lib/jksql.c +++ src/hg/lib/jksql.c @@ -7,30 +7,31 @@ /* * Configuration: */ #include "common.h" #include "portable.h" #include "errabort.h" #include <mysql.h> #include "dlist.h" #include "dystring.h" #include "jksql.h" #include "sqlNum.h" #include "hgConfig.h" +#include "cheapcgi.h" /* flags controlling sql monitoring facility */ static unsigned monitorInited = FALSE; /* initialized yet? */ static unsigned monitorFlags = 0; /* flags indicating what is traced */ static long monitorEnterTime = 0; /* time current tasked started */ static long long sqlTotalTime = 0; /* total real milliseconds */ static long sqlTotalQueries = 0; /* total number of queries */ static boolean monitorHandlerSet = FALSE; /* is exit handler installed? */ static unsigned traceIndent = 0; /* how much to indent */ static char *indentStr = " "; static boolean sqlParanoid = FALSE; /* extra squawking */ /* statistics */ static unsigned totalNumConnects = 0; @@ -551,116 +552,120 @@ { assert(!sc->isFree); return sc->conn->db; } char* sqlGetHost(struct sqlConnection *sc) /* Get the host associated with an connection. */ { assert(!sc->isFree); return sc->conn->host; } struct slName *sqlGetAllDatabase(struct sqlConnection *sc) /* Get a list of all database on the server */ { -struct sqlResult *sr = sqlGetResult(sc, "show databases"); +char query[32]; +sqlSafef(query, sizeof query, "show databases"); +struct sqlResult *sr = sqlGetResult(sc, query); char **row; struct slName *databases = NULL; while ((row = sqlNextRow(sr)) != NULL) { if (!startsWith("mysql", row[0])) /* Avoid internal databases. */ slSafeAddHead(&databases, slNameNew(row[0])); } sqlFreeResult(&sr); return databases; } struct slName *sqlListTables(struct sqlConnection *conn) /* Return list of tables in database associated with conn. */ { struct sqlResult *sr; char **row; struct slName *list = NULL, *el; char *cfgName = "showTableCache"; char *tableList = cfgOption(cfgName); if (tableList != NULL) { // mysql does not cache "show tables", so use a cached run of show tables which is stored in the showTableCache table. // See redmine 3780 for details. if(sqlTableExists(conn, tableList)) { char query[256]; - safef(query, sizeof(query), "select * from %s order by name desc", tableList); + sqlSafef(query, sizeof(query), "select * from %s order by name desc", tableList); sr = sqlGetResult(conn, query); while ((row = sqlNextRow(sr)) != NULL) { el = slNameNew(row[0]); slAddHead(&list, el); } } else { errAbort("%s option is misconfigured in hg.conf: table '%s' does not exist", cfgName, tableList); } } else { - sr = sqlGetResult(conn, "show tables"); + char query[256]; + sqlSafef(query, sizeof(query), "show tables"); + sr = sqlGetResult(conn, query); while ((row = sqlNextRow(sr)) != NULL) { el = slNameNew(row[0]); slAddHead(&list, el); } slReverse(&list); } sqlFreeResult(&sr); return list; } struct slName *sqlListFields(struct sqlConnection *conn, char *table) /* Return list of fields in table. */ { char query[256]; char **row; struct slName *list = NULL, *el; struct sqlResult *sr = NULL; -safef(query, sizeof(query), "describe %s", table); +sqlSafef(query, sizeof(query), "describe %s", table); sr = sqlGetResult(conn, query); while ((row = sqlNextRow(sr)) != NULL) { el = slNameNew(row[0]); slAddHead(&list, el); } sqlFreeResult(&sr); slReverse(&list); return list; } void sqlAddDatabaseFields(char *database, struct hash *hash) /* Add fields from the one database to hash. */ { struct sqlConnection *conn = sqlConnect(database); struct slName *table, *tableList = sqlListTables(conn); struct sqlResult *sr; char query[256]; char **row; char fullName[512]; for (table = tableList; table != NULL; table = table->next) { - safef(query, sizeof(query), "describe %s", table->name); + sqlSafef(query, sizeof(query), "describe %s", table->name); sr = sqlGetResult(conn, query); while ((row = sqlNextRow(sr)) != NULL) { safef(fullName, sizeof(fullName), "%s.%s.%s", database, table->name, row[0]); hashAdd(hash, fullName, NULL); } sqlFreeResult(&sr); } slFreeList(&tableList); sqlDisconnect(&conn); } struct hash *sqlAllFields(void) /* Get hash of all fields in database.table.field format. */ @@ -870,159 +875,182 @@ char *query, ResGetter *getter, boolean abort) /* Returns NULL if result was empty and getter==mysql_use_result. * Otherwise returns a structure that you can do sqlRow() on. * Watch out for subtle differences between mysql_store_result and mysql_use_result. * We seem to be only using mysql_use_result these days, * but mysql_store_result has left a big footprint in the code/comments. * In particular, mysql_store_result can return NULL indicating an empty resultset. * But mysql_use_result cannot do that. Instead NULL return means error * and the user must call next_row to see if there's anything in the resultset. */ { assert(!sc->isFree); MYSQL *conn = sc->conn; struct sqlResult *res = NULL; long deltaTime; +boolean fixedMultipleNOSQLINJ = FALSE; ++sqlTotalQueries; if (monitorFlags & JKSQL_TRACE) monitorPrintQuery(sc, query); +if (startsWith("NOSQLINJ ", query)) + { + query += strlen("NOSQLINJ "); // We know this query has been vetted for sql injection, skip over this tag. + } +else + { + sqlCheckError("Unvetted query: %s", query); + } + +// additional check finds errors of multiple NOSQLINJ tags +if (strstr(query, "NOSQLINJ ")) + { + sqlCheckError("Oops, multiple occurrences of NOSQLINJ tag in query: %s", query); + query = replaceChars(query, "NOSQLINJ ", ""); + fixedMultipleNOSQLINJ = TRUE; + } + monitorEnter(); if (mysql_real_query(conn, query, strlen(query)) != 0) { if (abort) { monitorLeave(); + dumpStack("oops DEBUG"); // DEBUG REMOVE sqlAbort(sc, "Can't start query:\n%s\n", query); } } else { MYSQL_RES *resSet; if ((resSet = getter(conn)) == NULL) { if (mysql_errno(conn) != 0) { monitorLeave(); sqlAbort(sc, "Can't use query:\n%s", query); } } else { AllocVar(res); res->conn = sc; res->result = resSet; res->node = dlAddValTail(sc->resultList, res); res->fetchTime = 0L; } } deltaTime = monitorLeave(); if (monitorFlags & JKSQL_TRACE) monitorPrint(sc, "SQL_TIME", "%0.3fs", ((double)deltaTime)/1000.0); +if (fixedMultipleNOSQLINJ) + freeMem(query); return res; } void sqlRenameTable(struct sqlConnection *sc, char *table1, char *table2) /* Rename table1 to table2 */ { char query[256]; -safef(query, sizeof(query), "rename table %s to %s", table1, table2); +sqlSafef(query, sizeof(query), "rename table %s to %s", table1, table2); sqlUpdate(sc, query); } void sqlDropTable(struct sqlConnection *sc, char *table) /* Drop table if it exists. */ { if (sqlTableExists(sc, table)) { char query[256]; - safef(query, sizeof(query), "drop table %s", table); + sqlSafef(query, sizeof(query), "drop table %s", table); sqlUpdate(sc, query); } } void sqlCopyTable(struct sqlConnection *sc, char *table1, char *table2) /* Copy table1 to table2 */ { char query[256]; if (table1 == NULL || table2 == NULL) return; -safef(query, sizeof(query), "create table %s like %s", table2, table1); +sqlSafef(query, sizeof(query), "create table %s like %s", table2, table1); sqlUpdate(sc, query); -safef(query, sizeof(query), "insert into %s select * from %s", table2, table1); +sqlSafef(query, sizeof(query), "insert into %s select * from %s", table2, table1); sqlUpdate(sc, query); } void sqlGetLock(struct sqlConnection *sc, char *name) /* Sets an advisory lock on the process for 1000s returns 1 if successful,*/ /* 0 if name already locked or NULL if error occurred */ /* blocks another client from obtaining a lock with the same name */ { char query[256]; struct sqlResult *res; char **row = NULL; -safef(query, sizeof(query), "select get_lock('%s', 1000)", name); +sqlSafef(query, sizeof(query), "select get_lock('%s', 1000)", name); res = sqlGetResult(sc, query); while ((row=sqlNextRow(res))) { if (sameWord(*row, "1")) break; else if (sameWord(*row, "0")) errAbort("Attempt to GET_LOCK timed out.\nAnother client may have locked this name, %s\n.", name); else if (*row == NULL) errAbort("Attempt to GET_LOCK of name, %s, caused an error\n", name); } sqlFreeResult(&res); } void sqlReleaseLock(struct sqlConnection *sc, char *name) /* Releases an advisory lock created by GET_LOCK in sqlGetLock */ { char query[256]; -safef(query, sizeof(query), "select release_lock('%s')", name); +sqlSafef(query, sizeof(query), "select release_lock('%s')", name); sqlUpdate(sc, query); } void sqlHardUnlockAll(struct sqlConnection *sc) /* Unlock any hard locked tables. */ { if (sc->hasHardLock) { - sqlUpdate(sc, "unlock tables"); + char query[32]; + sqlSafef(query, sizeof query, "unlock tables"); + sqlUpdate(sc, query); sc->hasHardLock = FALSE; } } void sqlHardLockTables(struct sqlConnection *sc, struct slName *tableList, boolean isWrite) /* Hard lock given table list. Unlock with sqlHardUnlockAll. */ { struct dyString *dy = dyStringNew(0); struct slName *table; char *how = (isWrite ? "WRITE" : "READ"); if (sc->hasHardLock) errAbort("sqlHardLockTables repeated without sqlHardUnlockAll."); -dyStringAppend(dy, "LOCK TABLES "); +sqlDyStringPrintf(dy, "LOCK TABLES "); for (table = tableList; table != NULL; table = table->next) { - dyStringPrintf(dy, "%s %s", table->name, how); + sqlDyStringPrintf(dy, "%s %s", table->name, how); if (table->next != NULL) dyStringAppendC(dy, ','); } sqlUpdate(sc, dy->string); sc->hasHardLock = TRUE; dyStringFree(&dy); } void sqlHardLockTable(struct sqlConnection *sc, char *table, boolean isWrite) /* Lock a single table. Unlock with sqlHardUnlockAll. */ { struct slName *list = slNameNew(table); sqlHardLockTables(sc, list, isWrite); slFreeList(&list); @@ -1055,57 +1083,74 @@ boolean sqlDatabaseExists(char *database) /* Return TRUE if database exists. */ { struct sqlConnection *conn = sqlMayConnect(database); boolean exists = (conn != NULL); sqlDisconnect(&conn); return exists; } boolean sqlTableExists(struct sqlConnection *sc, char *table) /* Return TRUE if a table exists. */ { char query[256]; struct sqlResult *sr; - -safef(query, sizeof(query), "SELECT 1 FROM %s LIMIT 0", table); +// TODO If the ability to supply a list of tables is hardly used, +// then we could switch it to simply %s below supporting a single +// table at a time more securely. +// DEBUG informational +if (strchr(table,',')) + dumpStack("sqlTableExists called on multiple tables with table=[%s]\n", table); +if (strchr(table,'%')) + { + // verbose is better than warn for early code calls? + dumpStack("jksql sqlTableExists: Buggy code is feeding me junk wildcards. table=[%s].\n", table); + return FALSE; + } +// DEBUG END +//verbose(1,"DEBUG sqlTableExists table=[%s]\n", table); // DEBUG REMOVE +sqlSafef(query, sizeof(query), "SELECT 1 FROM %-s LIMIT 0", sqlCkIl(table)); // DEBUG RESTORE +//safef(query, sizeof(query), "NOSQLINJ SELECT 1 FROM %s LIMIT 0", table); // DEBUG REMOVE if ((sr = sqlUseOrStore(sc,query,mysql_use_result, FALSE)) == NULL) return FALSE; +// TODO consider using sqlGetResultExt or something that would +// allow you to abort on all errors except the actual table not found: +// ERROR 1146 (42S02): Table 'hg19.chr_est' doesn't exist sqlFreeResult(&sr); return TRUE; } bool sqlColumnExists(struct sqlConnection *conn, char *tableName, char *column) /* return TRUE if column exists in table. tableName can contain sql wildcards */ { char query[1024]; - safef(query, 1024, "SHOW COLUMNS FROM `%s` LIKE '%s'", tableName, column); +sqlSafef(query, 1024, "SHOW COLUMNS FROM `%s` LIKE '%s'", tableName, column); char buf[1024]; char *ret = sqlQuickQuery(conn, query, buf, 1024); return (ret!=NULL); } int sqlTableSizeIfExists(struct sqlConnection *sc, char *table) /* Return row count if a table exists, -1 if it doesn't. */ { char query[256]; struct sqlResult *sr; char **row = 0; int ret = 0; -safef(query, sizeof(query), "select count(*) from %s", table); +sqlSafef(query, sizeof(query), "select count(*) from %s", table); if ((sr = sqlUseOrStore(sc,query,mysql_use_result, FALSE)) == NULL) return -1; row = sqlNextRow(sr); if (row != NULL && row[0] != NULL) ret = atoi(row[0]); sqlFreeResult(&sr); return ret; } boolean sqlTablesExist(struct sqlConnection *conn, char *tables) /* Check all tables in space delimited string exist. */ { char *dupe = cloneString(tables); char *s = dupe, *word; boolean ok = TRUE; @@ -1118,31 +1163,31 @@ } } freeMem(dupe); return ok; } boolean sqlTableWildExists(struct sqlConnection *sc, char *table) /* Return TRUE if table (which can include SQL wildcards) exists. * A bit slower than sqlTableExists. */ { char query[512]; struct sqlResult *sr; char **row; boolean exists; -safef(query, sizeof(query), "show tables like '%s'", table); +sqlSafef(query, sizeof(query), "show tables like '%s'", table); sr = sqlGetResult(sc, query); exists = ((row = sqlNextRow(sr)) != NULL); sqlFreeResult(&sr); return exists; } static char **sqlMaybeNextRow(struct sqlResult *sr, boolean *retOk) /* Get next row from query result; set retOk according to error status. */ { char** row = NULL; if (sr != NULL) { monitorEnter(); row = mysql_fetch_row(sr->result); sr->fetchTime += monitorLeave(); @@ -1238,46 +1283,48 @@ if ((info == NULL) || (numScan < 2)) errAbort("can't get info (maybe not an sql UPDATE): %s", query); sqlFreeResult(&sr); if (matched != NULL) *matched = numMatched; return numChanged; } void sqlWarnings(struct sqlConnection *conn, int numberOfWarnings) /* Show the number of warnings requested. New feature in mysql5. */ { struct sqlResult *sr; char **row; char query[256]; struct dyString *dy = dyStringNew(0); -safef(query,sizeof(query),"show warnings limit 0, %d", numberOfWarnings); +sqlSafef(query,sizeof(query),"show warnings limit 0, %d", numberOfWarnings); sr = sqlGetResult(conn, query); dyStringPrintf(dy, "Level Code Message\n"); while ((row = sqlNextRow(sr)) != NULL) { dyStringPrintf(dy, "%s %s %s\n", row[0], row[1], row[2]); } sqlFreeResult(&sr); warn("%s", dy->string); dyStringFree(&dy); } int sqlWarnCount(struct sqlConnection *conn) /* Return the number of warnings. New feature in mysql5. */ { -return sqlQuickNum(conn, "SHOW COUNT(*) WARNINGS"); +char query[32]; +sqlSafef(query, sizeof query, "SHOW COUNT(*) WARNINGS"); +return sqlQuickNum(conn, query); } void sqlLoadTabFile(struct sqlConnection *conn, char *path, char *table, unsigned options) /* Load a tab-seperated file into a database table, checking for errors. * Options are the SQL_TAB_* bit set. SQL_TAB_FILE_ON_SERVER is ignored if * sqlIsRemote() returns true. */ { assert(!conn->isFree); char tabPath[PATH_LEN]; char query[PATH_LEN+256]; int numScan, numRecs, numSkipped, numWarnings; char *localOpt, *concurrentOpt, *dupOpt; const char *info; @@ -1314,41 +1361,41 @@ { strcpy(tabPath, path); localOpt = "LOCAL"; } /* optimize for concurrent to others to access the table. */ if (options & SQL_TAB_FILE_CONCURRENT) concurrentOpt = "CONCURRENT"; else { concurrentOpt = ""; if (doDisableKeys) { /* disable update of indexes during load. Inompatible with concurrent, * since enable keys locks other's out. */ - safef(query, sizeof(query), "ALTER TABLE %s DISABLE KEYS", table); + sqlSafef(query, sizeof(query), "ALTER TABLE %s DISABLE KEYS", table); sqlUpdate(conn, query); } } if (options & SQL_TAB_REPLACE) dupOpt = "REPLACE"; else dupOpt = ""; -safef(query, sizeof(query), "LOAD DATA %s %s INFILE '%s' %s INTO TABLE %s", +sqlSafef(query, sizeof(query), "LOAD DATA %s %s INFILE '%s' %s INTO TABLE %s", concurrentOpt, localOpt, tabPath, dupOpt, table); sr = sqlGetResult(conn, query); monitorEnter(); info = mysql_info(conn->conn); monitorLeave(); if (info == NULL) errAbort("no info available for result of sql query: %s", query); numScan = sscanf(info, "Records: %d Deleted: %*d Skipped: %d Warnings: %d", &numRecs, &numSkipped, &numWarnings); if (numScan != 3) errAbort("can't parse sql load info: %s", info); sqlFreeResult(&sr); /* mysql 5.0 bug: mysql_info returns unreliable warnings count, so use this instead: */ @@ -1368,72 +1415,73 @@ } if (doAbort) errAbort("load of %s did not go as planned: %d record(s), " "%d row(s) skipped, %d warning(s) loading %s", table, numRecs, numSkipped, numWarnings, path); else warn("Warning: load of %s did not go as planned: %d record(s), " "%d row(s) skipped, %d warning(s) loading %s", table, numRecs, numSkipped, numWarnings, path); } if (((options & SQL_TAB_FILE_CONCURRENT) == 0) && doDisableKeys) { /* reenable update of indexes */ - safef(query, sizeof(query), "ALTER TABLE %s ENABLE KEYS", table); + sqlSafef(query, sizeof(query), "ALTER TABLE %s ENABLE KEYS", table); sqlUpdate(conn, query); } } boolean sqlExists(struct sqlConnection *conn, char *query) /* Query database and return TRUE if it had a non-empty result. */ { struct sqlResult *sr; if ((sr = sqlGetResult(conn,query)) == NULL) return FALSE; else { if(sqlNextRow(sr) == NULL) { sqlFreeResult(&sr); return FALSE; } else { sqlFreeResult(&sr); return TRUE; } } } boolean sqlRowExists(struct sqlConnection *conn, char *table, char *field, char *key) /* Return TRUE if row where field = key is in table. */ { char query[256]; -safef(query, sizeof(query), "select count(*) from %s where %s = '%s'", +sqlSafef(query, sizeof(query), "select count(*) from %s where %s = '%s'", table, field, key); return sqlQuickNum(conn, query) > 0; } int sqlRowCount(struct sqlConnection *conn, char *queryTblAndCondition) /* Return count of rows that match condition. The queryTblAndCondition * should contain everying after "select count(*) FROM " */ { char query[256]; -safef(query, sizeof(query), "select count(*) from %s",queryTblAndCondition); +sqlSafef(query, sizeof(query), "select count(*) from %-s",queryTblAndCondition); +// NOSQLINJ since we cannot check the queryTblAndCondition here, the users of this function have been fixed. return sqlQuickNum(conn, query); } struct sqlResult *sqlStoreResult(struct sqlConnection *sc, char *query) /* Returns NULL if result was empty. Otherwise returns a structure * that you can do sqlRow() on. Same interface as sqlGetResult, * but internally this keeps the entire result in memory. */ { return sqlUseOrStore(sc,query,mysql_store_result, TRUE); } char **sqlNextRow(struct sqlResult *sr) /* Get next row from query result. */ { @@ -1493,31 +1541,31 @@ { if (sr == NULL) return 0; return mysql_num_fields(sr->result); } int sqlCountColumnsInTable(struct sqlConnection *sc, char *table) /* Return the number of columns in a table */ { char query[256]; struct sqlResult *sr; char **row; int count; /* Read table description and count rows. */ -safef(query, sizeof(query), "describe %s", table); +sqlSafef(query, sizeof(query), "describe %s", table); sr = sqlGetResult(sc, query); count = 0; while ((row = sqlNextRow(sr)) != NULL) { count++; } sqlFreeResult(&sr); return count; } char *sqlQuickQuery(struct sqlConnection *sc, char *query, char *buf, int bufSize) /* Does query and returns first field in first row. Meant * for cases where you are just looking up one small thing. * Returns NULL if query comes up empty. */ { @@ -1706,67 +1754,67 @@ while ((row = sqlNextRow(sr)) != NULL) { n = slDoubleNew(atof(row[0])); slAddHead(&list, n); } sqlFreeResult(&sr); slReverse(&list); return list; } int sqlTableSize(struct sqlConnection *conn, char *table) /* Find number of rows in table. */ { char query[128]; -safef(query, sizeof(query), "select count(*) from %s", table); +sqlSafef(query, sizeof(query), "select count(*) from %s", table); return sqlQuickNum(conn, query); } int sqlFieldIndex(struct sqlConnection *conn, char *table, char *field) /* Returns index of field in a row from table, or -1 if it * doesn't exist. */ { char query[256]; struct sqlResult *sr; char **row; int i = 0, ix=-1; /* Read table description into hash. */ -safef(query, sizeof(query), "describe %s", table); +sqlSafef(query, sizeof(query), "describe %s", table); sr = sqlGetResult(conn, query); while ((row = sqlNextRow(sr)) != NULL) { if (sameString(row[0], field)) { ix = i; break; } ++i; } sqlFreeResult(&sr); return ix; } struct slName *sqlFieldNames(struct sqlConnection *conn, char *table) /* Returns field names from a table. */ { struct slName *list = NULL; char query[256]; struct sqlResult *sr; char **row; -safef(query, sizeof(query), "describe %s", table); +sqlSafef(query, sizeof(query), "describe %s", table); sr = sqlGetResult(conn, query); while ((row = sqlNextRow(sr)) != NULL) slNameAddHead(&list, row[0]); sqlFreeResult(&sr); slReverse(&list); return list; } unsigned int sqlLastAutoId(struct sqlConnection *conn) /* Return last automatically incremented id inserted into database. */ { assert(!conn->isFree); unsigned id; monitorEnter(); id = mysql_insert_id(conn->conn); @@ -2024,30 +2072,46 @@ if (!conn->inCache) errAbort("sqlConnCacheDealloc called on connection that is not associated with a cache"); assert(!conn->isFree); conn->isFree = TRUE; struct sqlConnCacheEntry *scce; for (scce = cache->entries; (scce != NULL) && (scce->conn != conn); scce = scce->next) continue; if (scce == NULL) errAbort("sqlConnCacheDealloc called on cache that doesn't contain " "the given connection"); scce->inUse = FALSE; *pConn = NULL; } } +// where am I using this? +void sqlDyAppendEscaped(struct dyString *dy, char *s) +/* Append to dy an escaped s */ +{ +dyStringBumpBufSize(dy, dy->stringSize + strlen(s)*2); +int realSize = sqlEscapeString3(dy->string+dy->stringSize, s); +dy->stringSize += realSize; +} + +unsigned long sqlEscapeString3(char *to, const char* from) +/* Prepares a string for inclusion in a sql statement. Output string + * must be 2*strlen(from)+1. Returns actual escaped size not counting term 0. */ +{ +return mysql_escape_string(to, from, strlen(from)); +} + char *sqlEscapeString2(char *to, const char* from) /* Prepares a string for inclusion in a sql statement. Output string * must be 2*strlen(from)+1 */ { mysql_escape_string(to, from, strlen(from)); return to; } char *sqlEscapeString(const char* from) /* Prepares string for inclusion in a SQL statement . Remember to free * returned string. Returned string contains strlen(length)*2+1 as many bytes * as orig because in worst case every character has to be escaped.*/ { int size = (strlen(from)*2) +1; char *to = needMem(size * sizeof(char)); @@ -2182,31 +2246,31 @@ *s++ = c; break; } } return retVal; } long sqlDateToUnixTime(char *sqlDate) /* Convert a SQL date such as "2003-12-09 11:18:43" to clock time * (seconds since midnight 1/1/1970 in UNIX). */ { struct tm *tm = NULL; long clockTime = 0; if (sqlDate == NULL) - errAbort("Null string passed to sqlDateToClockTime()"); + errAbort("Null string passed to sqlDateToUnixTime()"); AllocVar(tm); if (sscanf(sqlDate, "%4d-%2d-%2d %2d:%2d:%2d", &(tm->tm_year), &(tm->tm_mon), &(tm->tm_mday), &(tm->tm_hour), &(tm->tm_min), &(tm->tm_sec)) != 6) errAbort("Couldn't parse sql date \"%s\"", sqlDate); tm->tm_year -= 1900; tm->tm_mon -= 1; /* Ask mktime to determine whether Daylight Savings Time is in effect for * the given time: */ tm->tm_isdst = -1; clockTime = mktime(tm); if (clockTime < 0) errAbort("mktime failed (%d-%d-%d %d:%d:%d).", tm->tm_year, tm->tm_mon, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec); @@ -2219,31 +2283,31 @@ * to the string: "YYYY-MM-DD HH:MM:SS" * returned string is malloced, can be freed after use * boolean gmTime requests GMT time instead of local time */ { struct tm *tm; char *ret; if (gmTime) tm = gmtime(timep); else tm = localtime(timep); ret = (char *)needMem(25*sizeof(char)); /* 25 is good for a billion years */ -snprintf(ret, 25*sizeof(char), "%d-%02d-%02d %02d:%02d:%02d", +safef(ret, 25*sizeof(char), "%d-%02d-%02d %02d:%02d:%02d", 1900+tm->tm_year, 1+tm->tm_mon, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec); return(ret); } static int getUpdateFieldIndex(struct sqlResult *sr) /* Return index of update field. * Note: does NOT work on innoDB! */ { static int updateFieldIndex = -1; if (updateFieldIndex < 0) { int ix; char *name; for (ix=0; ;++ix) @@ -2257,78 +2321,80 @@ break; } } } return updateFieldIndex; } char *sqlTableUpdate(struct sqlConnection *conn, char *table) /* Get last update time for table as an SQL string * Note: does NOT work on innoDB! */ { char query[512], **row; struct sqlResult *sr; int updateIx; char *ret; -safef(query, sizeof(query), "show table status like '%s'", table); +sqlSafef(query, sizeof(query), "show table status like '%s'", table); sr = sqlGetResult(conn, query); updateIx = getUpdateFieldIndex(sr); row = sqlNextRow(sr); if (row == NULL) errAbort("Database table %s doesn't exist", table); ret = cloneString(row[updateIx]); sqlFreeResult(&sr); return ret; } time_t sqlTableUpdateTime(struct sqlConnection *conn, char *table) /* Get last update time for table. * Note: does NOT work on innoDB! */ { char *date = sqlTableUpdate(conn, table); time_t time = sqlDateToUnixTime(date); freeMem(date); return time; } char *sqlGetPrimaryKey(struct sqlConnection *conn, char *table) /* Get primary key if any for table, return NULL if none. */ { char query[512]; struct sqlResult *sr; char **row; char *key = NULL; -safef(query, sizeof(query), "describe %s", table); +sqlSafef(query, sizeof(query), "describe %s", table); sr = sqlGetResult(conn, query); while ((row = sqlNextRow(sr)) != NULL) { if (sameWord(row[3], "PRI")) { key = cloneString(row[0]); break; } } sqlFreeResult(&sr); return key; } char *sqlVersion(struct sqlConnection *conn) /* Return version of MySQL database. This will be something * of the form 5.0.18-standard. */ { +char query[32]; char **row; -struct sqlResult *sr = sqlGetResult(conn, "show variables like 'version'"); +sqlSafef(query, sizeof query, "show variables like 'version'"); +struct sqlResult *sr = sqlGetResult(conn, query); char *version = NULL; if ((row = sqlNextRow(sr)) != NULL) version = cloneString(row[1]); else errAbort("No mySQL version var."); sqlFreeResult(&sr); return version; } int sqlMajorVersion(struct sqlConnection *conn) /* Return major version of database. */ { char *s = sqlVersion(conn); int ver; if (!isdigit(s[0])) @@ -2356,31 +2422,31 @@ } char** sqlGetEnumDef(struct sqlConnection *conn, char* table, char* colName) /* Get the definitions of a enum column in a table, returning a * null-terminated array of enum values. Free array when finished. */ { static char *enumPrefix = "enum("; struct sqlResult *sr; char query[128]; char **row; char *defStr, *defStrCp; int numValues, i; char **enumDef; /* get enum definition */ -safef(query, sizeof(query), "describe %s", table); +sqlSafef(query, sizeof(query), "describe %s", table); sr = sqlGetResult(conn, query); while (((row = sqlNextRow(sr)) != NULL) && !sameString(row[0], colName)) continue; if (row == NULL) errAbort("can't find column %s in results of %s", colName, query); /* parse definition in the form: * enum('unpicked','candidate',... ,'cantSequence') */ if (!startsWith(enumPrefix, row[1])) errAbort("%s column %s isn't an enum: %s", table, colName, row[1]); defStr = row[1] + strlen(enumPrefix); /* build char** array with string space in same block */ numValues = chopString(defStr, ",", NULL, 0); enumDef = needMem(((numValues+1) * sizeof (char**)) + strlen(defStr)+1); @@ -2411,37 +2477,37 @@ char query[256], **row; struct sqlResult *sr; struct slName *list = NULL, *el; char seedString[256] = ""; /* The randomized-order, distinct-ing query can take a very long time on * very large tables. So create a smaller temporary table and use that. * The temporary table is visible only to the current connection, so * doesn't have to be very uniquely named, and will disappear when the * connection is closed. */ /* check if table has 'db.' prefix in it */ char *plainTable = strrchr(table, '.'); if (plainTable) plainTable++; else plainTable = table; -safef(query, sizeof(query), +sqlSafef(query, sizeof(query), "create temporary table hgTemp.tmp%s select %s from %s limit 100000", plainTable, field, table); sqlUpdate(conn, query); if (seed != -1) safef(seedString,sizeof(seedString),"%d",seed); -safef(query, sizeof(query), "select distinct %s from hgTemp.tmp%s " +sqlSafef(query, sizeof(query), "select distinct %s from hgTemp.tmp%s " "order by rand(%s) limit %d", field, plainTable, seedString, count); sr = sqlGetResult(conn, query); while ((row = sqlNextRow(sr)) != NULL) { el = slNameNew(row[0]); slAddHead(&list, el); } sqlFreeResult(&sr); return list; } struct slName *sqlRandomSampleWithSeed(char *db, char *table, char *field, int count, int seed) /* Get random sample from database specifiying rand number seed, or -1 for none */ { @@ -2474,31 +2540,31 @@ fi->allowsNull = sameString(row[2], "YES"); fi->key = cloneString(row[3]); fi->defaultVal = cloneString(row[4]); fi->extra = cloneString(row[5]); return fi; } struct sqlFieldInfo *sqlFieldInfoGet(struct sqlConnection *conn, char *table) /* get a list of objects describing the fields of a table */ { char query[512]; struct sqlResult *sr; char **row; struct sqlFieldInfo *fiList = NULL; -safef(query, sizeof(query), "SHOW COLUMNS FROM %s", table); +sqlSafef(query, sizeof(query), "SHOW COLUMNS FROM %s", table); sr = sqlGetResult(conn, query); while ((row = sqlNextRow(sr)) != NULL) slSafeAddHead(&fiList, sqlFieldInfoParse(row)); sqlFreeResult(&sr); slReverse(&fiList); return fiList; } static void sqlFieldInfoFree(struct sqlFieldInfo **fiPtr) /* Free a sqlFieldInfo object */ { struct sqlFieldInfo *fi = *fiPtr; if (fi != NULL) { freeMem(fi->field); @@ -2519,31 +2585,31 @@ sqlFieldInfoFree(&fi); } void *sqlVaQueryObjs(struct sqlConnection *conn, sqlLoadFunc loadFunc, unsigned opts, char *queryFmt, va_list args) /* Generate a query from format and args. Load one or more objects from rows * using loadFunc. Check the number of rows returned against the sqlQueryOpts * bit set. Designed for use with autoSql, although load function must be * cast to sqlLoadFunc. */ { char query[1024]; struct slList *objs = NULL; struct sqlResult *sr; char **row; -vasafef(query, sizeof(query), queryFmt, args); +vaSqlSafef(query, sizeof(query), queryFmt, args); sr = sqlGetResult(conn, query); while ((row = sqlNextRow(sr)) != NULL) { slSafeAddHead(&objs, loadFunc(row)); } sqlFreeResult(&sr); slReverse(&objs); /* check what we got against the options */ if (objs == NULL) { if (opts & sqlQueryMust) errAbort("no results return from query: %s", query); } else if ((opts & sqlQuerySingle) && (objs->next != NULL)) errAbort("one results, got %d, from query: %s", slCount(objs), query); @@ -2675,15 +2741,790 @@ fprintf(fh, "db: %s profile: %s\n", hel->name, ((struct sqlProfile*)hel->val)->name); struct dlNode *connNode; for (connNode = sqlOpenConnections->head; !dlEnd(connNode); connNode = connNode->next) sqlDumpConnection(connNode->val, fh); fprintf(fh, "%s\n", dashes); } void sqlPrintStats(FILE *fh) /* print statistic about the number of connections and other options done by * this process. */ { fprintf(fh, "sqlStats: connects: %d maxOpen: %d\n", totalNumConnects, maxNumConnections); } +/* --------- input checks to prevent sql injection --------------------------------------- */ + +// 0 means char is allowed +// 1 means char is disallowed + +// although the mysql escape function can escape binary data, +// we don't need to support escaping 0 for strings here. + +static boolean sqlCheckAllowedChars(char *s, char disAllowed[256]) +/* Check each character of input against allowed character set */ +{ +if (!s) + { + sqlCheckError("sqlCheckAllowedChars - Cannot check NULL"); + return FALSE; + } +char *sOriginal = s; +unsigned char c; +while((c = *s++) != 0) + { + if (disAllowed[c]) + { + verbose(1,"character %c disallowed in sql string part %s\n", c, sOriginal); // DEBUG REMOVE GALT + + // DEBUG REMOVE Temporary for trying to track down some weird error + // because the stackdump should appear but does not. + //dumpStack("character %c disallowed in sql string part %s\n", c, sOriginal); // DEBUG REMOVE GALT + + // TODO for some reason the warn stack is messed up sometimes very eary. -- happening in hgTables position search on brca + //warn("character %c disallowed in sql string part %s", c, sOriginal); + + return FALSE; // might want to look at hg.conf settings and if debugging, show details. + } + } +return TRUE; +} + +static void sqlCheckDisallowAllChars(char disAllowed[256]) +/* Disallow all chars by setting to 1 */ +{ +int i; +for(i=0;i<256;++i) + disAllowed[i] = 1; +} + +static void sqlCheckAllowAllChars(char disAllowed[256]) +/* Allow all chars by setting to 0 */ +{ +int i; +for(i=0;i<256;++i) + disAllowed[i] = 0; +} + +static void sqlCheckAllowLowerChars(char allowed[256]) +/* Allow lower case chars by setting to 0 */ +{ +unsigned char c; +for(c='a';c<='z';++c) + allowed[c] = 0; +} + +static void sqlCheckAllowUpperChars(char allowed[256]) +/* Allow upper case chars by setting to 0 */ +{ +unsigned char c; +for(c='A';c<='Z';++c) + allowed[c] = 0; +} + +static void sqlCheckAllowDigitChars(char allowed[256]) +/* Allow digit chars by setting to 0 */ +{ +unsigned char c; +for(c='0';c<='9';++c) + allowed[c] = 0; +} + +static void sqlCheckAllowChar(unsigned char c, char allowed[256]) +/* Allow a char by setting to 0 */ +{ +allowed[c] = 0; +} + +static void sqlCheckDisallowChar(unsigned char c, char allowed[256]) +/* Allow a char by setting to 0 */ +{ +allowed[c] = 1; +} + +static void sqlCheckAllowAlphaChars(char allowed[256]) +/* Allow all chars by setting to 0 */ +{ +sqlCheckAllowUpperChars(allowed); +sqlCheckAllowLowerChars(allowed); +} + +static void sqlCheckAllowAlphaNumChars(char allowed[256]) +/* Allow all chars by setting to 0 */ +{ +sqlCheckAllowAlphaChars(allowed); +sqlCheckAllowDigitChars(allowed); +} + +static boolean sqlCheckNeedsEscape(char *s) +/* Check if string s needs escaping. Usually it doesn't need it. */ +{ +static boolean init = FALSE; +static char disallowed[256]; +if (!init) + { + sqlCheckAllowAllChars(disallowed); + sqlCheckDisallowChar('\'' , disallowed); // single-quote + sqlCheckDisallowChar('"' , disallowed); // double-quote + sqlCheckDisallowChar('\\' , disallowed); // back-slash + sqlCheckDisallowChar('\r' , disallowed); // carriage-return + sqlCheckDisallowChar('\n' , disallowed); // newline or linefeed + sqlCheckDisallowChar('\x1a', disallowed); // ctrl-Z or 26 dec or 1a hex. + // technically, 0 can be escaped but we are doing strings and not general binary data here. + init = TRUE; + } +if (sqlCheckAllowedChars(s, disallowed)) + { + return FALSE; + } +return TRUE; +} + +// TODO This is probably not needed +char *sqlEscapeIfNeeded(char *s, char **pS) +/* Escape if needed. if *pS is not null, free it. */ +{ +char *ret = NULL; +if (sqlCheckNeedsEscape(s)) + { + ret = sqlEscapeString(s); + if (pS) + *pS = ret; + } +else + { + if (pS) + *pS = NULL; + ret = s; + } +return ret; +} + +char *sqlCheckQuotedLiteral(char *s) +/* Check that none of the chars needing to be escaped are in the string s */ +{ +if (sqlCheckNeedsEscape(s)) + { + sqlCheckError("Forbidden characters like quotes, newlines, or backslashes found in quoted string literal %s", s); + } +return s; +} + +char *sqlCheckAlphaNum(char *word) +/* Check that only valid alpha numeric characters are used in word */ +{ +static boolean init = FALSE; +static char allowed[256]; +if (!init) + { + sqlCheckDisallowAllChars(allowed); + sqlCheckAllowAlphaNumChars(allowed); + init = TRUE; + } +if (!sqlCheckAllowedChars(word, allowed)) + { + sqlCheckError("Illegal character found in %s", word); + } +return word; +} + +// TODO as much as I liked this function sqlCheckIdentifiersList, +// it may not be used much, so see if you can remove it +// and just add a little workaound for the remaining place(s) that use it. +char *sqlCheckIdentifiersList(char *identifiers) +/* Check that only valid identifier characters are used in a comma-separated list */ +{ +static boolean init = FALSE; +static char allowed[256]; +if (!init) + { + sqlCheckDisallowAllChars(allowed); + sqlCheckAllowAlphaNumChars(allowed); + sqlCheckAllowChar('.', allowed); + sqlCheckAllowChar('_', allowed); + // sqlTableExists looks like a single table check, but apparently it has become abused + // to support multiple tables e.g. sqlTableExists + sqlCheckAllowChar(' ', allowed); + sqlCheckAllowChar(',', allowed); + // NOTE it is important for security that no other characters be allowed here + init = TRUE; + } +if (!sqlCheckAllowedChars(identifiers, allowed)) + { + sqlCheckError("Illegal character found in identifier list %s", identifiers); + return identifiers; + } +// Unfortunately, just checking that the characters are legal is far from enough to ensure safety. +// the comma is required. Currently aliases and tick quotes are not supported. +int len = strlen(identifiers); +char c = 0; +int i = 0; +boolean needText = TRUE; +boolean spaceOk = FALSE; +// Currently identifiers list must start with an identifier, no leading spaces or comma allowed. +// Currently the comma must immediately follow the identifier +// Currently zero or one spaces may follow the comma +// List should end with an identifier. No trailing comma or space allowed. +// NOTE it is important for security that commas separate values. +// We do not want to support multiple words separated by spaces. +while (i < len) + { + c = identifiers[i]; + if (c == ' ') + { + if (!spaceOk) + { + sqlCheckError("Invalid Identifiers List [%s] unexpected space character", identifiers); + return identifiers; + } + spaceOk = FALSE; + } + else if (c == ',') + { + if (needText) + { + sqlCheckError("Invalid Identifiers List [%s] unexpected comma character", identifiers); + return identifiers; + } + spaceOk = TRUE; + needText = TRUE; + } + else // other chars are part of the identifier + { + needText = FALSE; + spaceOk = FALSE; + } + + ++i; + } +if (needText || spaceOk) + { + sqlCheckError("Invalid Identifiers List [%s] unexpected trailing comma or space character", identifiers); + return identifiers; + } + +return identifiers; +} + +static char *sqlCheckIdentifierKind(char *identifier, char *kind) +/* Check that only valid identifier characters are used */ +{ +static boolean init = FALSE; +static char allowed[256]; +if (!init) + { + sqlCheckDisallowAllChars(allowed); + sqlCheckAllowAlphaNumChars(allowed); + sqlCheckAllowChar('.', allowed); + sqlCheckAllowChar('_', allowed); + // NOTE it is important for security that no other characters be allowed here + init = TRUE; + } +if (!sqlCheckAllowedChars(identifier, allowed)) + { + sqlCheckError("Illegal character found in %s %s", kind, identifier); + } +return identifier; +} + +char *sqlCheckIdentifier(char *identifier) +/* Check that only valid identifier characters are used */ +{ +return sqlCheckIdentifierKind(identifier, "identifier"); +} + +// TODO not sure this one is really needed. Regular identifier check works well enough. +// included this one originally in case it was used alot and needed to be a +// little different from identifier check. +char *sqlCheckTableName(char *table) +/* Check that only valid table name characters are used */ +{ +return sqlCheckIdentifierKind(table, "table"); +} + +// TODO not sure we really need this function. could probably just use sqlCheckIdentifier instead. +char *sqlCheckCgiEncodedName(char *name) +/* Check that only valid cgi-encoded characters are used */ +{ +static boolean init = FALSE; +static char allowed[256]; +if (!init) + { + sqlCheckDisallowAllChars(allowed); + sqlCheckAllowAlphaNumChars(allowed); + sqlCheckAllowChar('.', allowed); + sqlCheckAllowChar('_', allowed); + sqlCheckAllowChar('%', allowed); + init = TRUE; + } +if (!sqlCheckAllowedChars(name, allowed)) + { + sqlCheckError("Illegal character found in table name %s", name); + } +return name; +} + + +/* --------------------------- */ + +int sqlEscapeAllStrings(char *buffer, char *s, int bufSize, char escPunc) +/* Escape all strings demarked by escPunc char. * + * Returns final size not including terminating 0. + * User needs to pre-allocate enough space that mysql_escape will never run out of space. + * This function should be efficient on statements with many strings to be escaped. */ +{ +char *sOrig = s; +int sz = 0; +int remainder = bufSize; +boolean done = FALSE; +while (1) + { + char *start = strchr(s, escPunc); + char *end = NULL; + if (start) + { + end = strchr(start+1, escPunc); // skip over punc mark + if (!end) + errAbort("Unexpected error in sqlEscapeAllStrings. s=[%s]", sOrig); + } + else + { + // just copy remainder of the input string to output + start = strchr(s, 0); // find end of string + done = TRUE; + } + // move any non-escaped part + int moveSize = start - s; + if (moveSize > remainder) + errAbort("Buffer too small in sqlEscapeAllStrings. s=[%s] bufSize = %d", sOrig, bufSize); + memmove(buffer, s, moveSize); + buffer += moveSize; + sz += moveSize; + remainder -= moveSize; + if (done) + { + if (remainder < 1) + errAbort("Buffer too small for termintating zero in sqlEscapeAllStrings. s=[%s] bufSize = %d", sOrig, bufSize); + --remainder; + *buffer++ = 0; // terminating 0 + // do not include term 0 in sz count; + break; + } + // escape the quoted part + s = start + 1; + *end = 0; // mark end of "input" string, replacing escPunc. input string is temporary anyway. + int inputSize = end - s; + int worstCase = inputSize*2 + 1; + if (worstCase > remainder) + errAbort("Buffer too small for escaping in sqlEscapeAllStrings. s=[%s] bufSize = %d", sOrig, bufSize); + int escSize = mysql_escape_string(buffer, s, inputSize); + buffer += escSize; + sz += escSize; + remainder -= escSize; + s = end + 1; + } +return sz; +} + + +int vaSqlSafefNoAbort(char* buffer, int bufSize, boolean newString, char *format, va_list args) +/* Format string to buffer, vsprintf style, only with buffer overflow + * checking. The resulting string is always terminated with zero byte. + * Scans string parameters for illegal sql chars. + * Automatically escapes quoted string values. + * This function should be efficient on statements with many strings to be escaped. */ +{ +va_list orig_args; +va_copy(orig_args, args); +int formatLen = strlen(format); + +char escPunc = 0x01; // using char 1 as special char to denote strings needing escaping +//char escPunc = '`'; // DEBUG REMOVE +char *newFormat = NULL; +int newFormatSize = 2*formatLen + 1; +if (newString) + newFormatSize += strlen("NOSQLINJ "); +newFormat = needMem(newFormatSize); +char *nf = newFormat; +if (newString) + nf += safef(newFormat, newFormatSize, "%s", "NOSQLINJ "); +char *lastPct = NULL; +int escStringsCount = 0; +int escStringsSize = 0; + +char c = 0; +int i = 0; +char quote = 0; +boolean inPct = FALSE; +boolean isLong = FALSE; +boolean isLongLong = FALSE; +boolean isNegated = FALSE; +while (i < formatLen) + { + c = format[i]; + *nf++ = c; + // start quote + if (quote==0 && (c == '\'' || c == '"' || c == '`')) + quote = c; + // end quote + else if (c == quote) + quote = 0; + else if (c == '%' && !inPct) + { + inPct = TRUE; + lastPct = nf - 1; // remember where the start was. + } + else if (c == '%' && inPct) + inPct = FALSE; + else if (inPct) + { + if (c == 'l') + { + if (isLong) + isLongLong = TRUE; + else + isLong = TRUE; + } + else if (strchr("diuoxXeEfFgGpcs",c)) + { + inPct = FALSE; + // convert to equivalent types + if (c == 'i') c = 'd'; + if (c == 'E') c = 'e'; + if (c == 'F') c = 'f'; + if (c == 'G') c = 'g'; + if (c == 'o') c = 'u'; + if (c == 'x') c = 'u'; + if (c == 'X') c = 'u'; + // we finally have the expected format + // for all except s, we just want to skip it, but va_arg is the only way to do it! + // signed integers + if (c == 'd' && !isLong) { va_arg(args, int); } + else if (c == 'd' && isLong && !isLongLong) { va_arg(args, long int); } + else if (c == 'd' && isLong && isLongLong) { va_arg(args, long long int); } + // unsigned integers + else if (c == 'u' && !isLong) { va_arg(args, unsigned int); } + else if (c == 'u' && isLong && !isLongLong) { va_arg(args, unsigned long int); } + else if (c == 'u' && isLong && isLongLong) { va_arg(args, unsigned long long int); } + else if (c == 'e') { va_arg(args, double); } + else if (c == 'f') { va_arg(args, double); } + else if (c == 'g') { va_arg(args, double); } + // pointer is void * + else if (c == 'p') { va_arg(args, void *); } + // char get promoted to int by varargs process + else if (c == 'c') { va_arg(args, int); } + // finally, the string we care about! + else if (c == 's') + { + char *s = va_arg(args, char *); + if (s == NULL) + sqlCheckError("%%s value is NULL which is incorrect."); + if (quote == 0) + { // check identifier + if (!isNegated) // Not a Pre-escaped String + sqlCheckIdentifier(s); + } + else + { // check quoted literal + if (!isNegated) // Not a Pre-escaped String + { + // go back and insert escPunc before the leading % char saved in lastPct + // move the accumulated %s descriptor + memmove(lastPct+1, lastPct, nf - lastPct); // this is typically very small, src and dest overlap. + ++nf; + *lastPct = escPunc; + *nf++ = escPunc; + ++escStringsCount; + if (s == NULL) + { + escStringsSize += strlen("(null)"); + } + else + { + escStringsSize += strlen(s); // TODO do we need this variable? + // DEBUG temporary check for signs of double-escaping, can remove later for a minor speedup: + //if (strstr(s, "\\\\\\\\")) // this is really 4 backslashes + if (strstr(s, "\\\\")) // this is really 2 backslashes + { + dumpStack("potential sign of double sql-escaping in string [%s]", s); + } + } + } + } + } + else + { + errAbort("unexpected error processing vaSqlSafef, format: %s", format); + } + + isLong = FALSE; + isLongLong = FALSE; + isNegated = FALSE; + } + else if (strchr("+-.1234567890",c)) + { + if (c == '-') + isNegated = TRUE; + } + else + errAbort("string format not understood in vaSqlSafef: %s", format); + } + ++i; + } + +verbose(2, "format=[%s]\nnewFormat=[%s]\n", format, newFormat); // DEBUG REMOVE + +int sz = 0; +if (escStringsCount > 0) + { + verbose(2, "newFormatSize=%d escStringsSize=%d \n", newFormatSize, escStringsSize); // DEBUG REMOVE + int tempSize = bufSize + 2*escStringsCount; // if it won't fit in this it will never fit. + verbose(2, "trying tempSize=%d\n", tempSize); // DEBUG REMOVE + char *tempBuf = needMem(tempSize); + sz = vsnprintf(tempBuf, tempSize, newFormat, orig_args); + /* note that some versions return -1 if too small */ + if (sz != -1 && sz + 1 <= tempSize) + { + verbose(2, "tempBuf=[%s] tempSize=%d strlen=%d sz=%d\n", tempBuf, tempSize, (int)strlen(tempBuf), sz); // DEBUG REMOVE + // unfortunately we have to copy the string 1 more time unless we want + // to force the user to allocate extra "safety space" for mysql_escape. + int tempSize2 = sz + 1 + escStringsSize; // handle worst-case + char *tempBuf2 = needMem(tempSize2); + sz = sqlEscapeAllStrings(tempBuf2, tempBuf, tempSize2, escPunc); + verbose(2, "sz after sqlEscapeAllStrings=%d tempSize2=%d final bufSize=%d\n", sz, tempSize2, bufSize); // DEBUG REMOVE + if (sz + 1 > tempSize2) + errAbort("unexpected error in vaSqlSafefNoAbort: tempBuf2 overflowed. tempSize2=%d sz=%d", tempSize, sz); + if (sz + 1 <= bufSize) // NO buffer overflow + { + verbose(2, "tempBuf2=[%s]\n", tempBuf2); // DEBUG REMOVE + // copy string to its final destination. + memmove(buffer, tempBuf2, sz+1); // +1 for terminating 0; + } + freeMem(tempBuf2); + } + freeMem(tempBuf); + } +else + { + sz = vsnprintf(buffer, bufSize, newFormat, orig_args); + /* note that some version return -1 if too small */ + } + +freeMem(newFormat); +va_end(orig_args); +va_end(args); + +return sz; + +} + + + + + +int vaSqlSafef(char* buffer, int bufSize, char *format, va_list args) +/* Format string to buffer, vsprintf style, only with buffer overflow + * checking. The resulting string is always terminated with zero byte. */ +{ +int sz = vaSqlSafefNoAbort(buffer, bufSize, TRUE, format, args); +if ((sz < 0) || (sz >= bufSize)) + { + buffer[bufSize-1] = (char) 0; + errAbort("buffer overflow, size %d, format: %s, buffer: '%s'", bufSize, format, buffer); + } +return sz; +} + +int sqlSafef(char* buffer, int bufSize, char *format, ...) +/* Format string to buffer, vsprintf style, only with buffer overflow + * checking. The resulting string is always terminated with zero byte. + * Scans string parameters for illegal sql chars. */ +{ +int sz; +va_list args; +va_start(args, format); +sz = vaSqlSafef(buffer, bufSize, format, args); +va_end(args); +return sz; +} + + +int vaSqlSafefFrag(char* buffer, int bufSize, char *format, va_list args) +/* Format string to buffer, vsprintf style, only with buffer overflow + * checking. The resulting string is always terminated with zero byte. + * This version does not add the tag since it is assumed to be just a fragment of + * the entire sql string. */ +{ +int sz = vaSqlSafefNoAbort(buffer, bufSize, FALSE, format, args); +if ((sz < 0) || (sz >= bufSize)) + { + buffer[bufSize-1] = (char) 0; + errAbort("buffer overflow, size %d, format: %s, buffer: '%s'", bufSize, format, buffer); + } +return sz; +} + +int sqlSafefFrag(char* buffer, int bufSize, char *format, ...) +/* Format string to buffer, vsprintf style, only with buffer overflow + * checking. The resulting string is always terminated with zero byte. + * Scans string parameters for illegal sql chars. + * This version does not add the tag since it is assumed to be just a fragment of + * the entire sql string. */ +{ +int sz; +va_list args; +va_start(args, format); +sz = vaSqlSafefFrag(buffer, bufSize, format, args); +va_end(args); +return sz; +} + + + +/* --------------------------- */ + + +void sqlDyStringVaPrintfExt(struct dyString *ds, boolean isFrag, char *format, va_list args) +/* VarArgs Printf to end of dyString after scanning string parameters for illegal sql chars. */ +{ +/* attempt to format the string in the current space. If there + * is not enough room, increase the buffer size and try again */ +int avail, sz; +while (TRUE) + { + va_list argscp; + va_copy(argscp, args); + avail = ds->bufSize - ds->stringSize; + if (avail <= 0) + { + /* Don't pass zero sized buffers to vsnprintf, because who knows + * if the library function will handle it. */ + dyStringBumpBufSize(ds, ds->bufSize+ds->bufSize); + avail = ds->bufSize - ds->stringSize; + } + sz = vaSqlSafefNoAbort(ds->string + ds->stringSize, avail, ds->stringSize==0 && !isFrag, format, argscp); + va_end(argscp); + + /* note that some version return -1 if too small */ + if ((sz < 0) || (sz >= avail)) + { + dyStringBumpBufSize(ds, ds->bufSize+ds->bufSize); + } + else + { + ds->stringSize += sz; + break; + } + } +} + +void sqlDyStringVaPrintf(struct dyString *ds, char *format, va_list args) +/* VarArgs Printf to end of dyString after scanning string parameters for illegal sql chars. */ +{ +sqlDyStringVaPrintfExt(ds, FALSE, format, args); +} + +void sqlDyStringPrintf(struct dyString *ds, char *format, ...) +/* Printf to end of dyString after scanning string parameters for illegal sql chars. */ +{ +va_list args; +va_start(args, format); +sqlDyStringVaPrintf(ds, format, args); +va_end(args); +} + +void sqlDyStringVaPrintfFrag(struct dyString *ds, char *format, va_list args) +/* VarArgs Printf to end of dyString after scanning string parameters for illegal sql chars. NOSLQINJ tag is not added. */ +{ +sqlDyStringVaPrintfExt(ds, TRUE, format, args); +} + +void sqlDyStringPrintfFrag(struct dyString *ds, char *format, ...) +/* Printf to end of dyString after scanning string parameters for illegal sql chars. NOSLQINJ tag is not added. */ +{ +va_list args; +va_start(args, format); +sqlDyStringVaPrintfFrag(ds, format, args); +va_end(args); +} + + +void sqlDyStringAppend(struct dyString *ds, char *string) +/* Append zero terminated string to end of dyString. + * Makes sure the NOSQLINJ prefix gets added if needed */ +{ +if (ds->stringSize == 0) + dyStringAppend(ds, "NOSQLINJ "); +dyStringAppendN(ds, string, strlen(string)); +} + + +// TODO probably do not need this one now that we have sqlDyStringPrintfFrag +char *sqlDyStringFrag(struct dyString *ds) +/* If ds is only a sql fragment, do not need leading NOSQLINJ tag */ +{ +if (startsWith("NOSQLINJ ", ds->string)) + return ds->string + strlen("NOSQLINJ "); +return ds->string; +} + +struct dyString *sqlDyStringCreate(char *format, ...) +/* Create a dyString with a printf style initial content + * Makes sure the NOSQLINJ prefix gets added if needed */ +{ +int len = strlen(format) * 3; +struct dyString *ds = newDyString(len); +va_list args; +va_start(args, format); +sqlDyStringVaPrintf(ds, format, args); +va_end(args); +return ds; +} + + +void sqlCheckError(char *format, ...) +/* A sql injection error has occurred. Check for settings and respond + * as appropriate with error, warning, ignore, dumpstack. + * Then abort if needed. NOTE: unless it aborts, this function will return! */ +{ +va_list args; +va_start(args, format); + +char *noSqlInjLevel = cfgOption("noSqlInj.level"); +char *noSqlInjDumpStack = cfgOption("noSqlInj.dumpStack"); +char *browserDumpStack = cfgOption("browser.dumpStack"); + +char *scriptName = cgiScriptName(); + +if (noSqlInjLevel) + { + // don't dump if if we are going to do it during errAbort anyway + if (sameOk(noSqlInjDumpStack, "on") + && (!(sameString(noSqlInjLevel, "abort") + && cgiIsOnWeb() + && sameOk(browserDumpStack, "on")) + || endsWith(scriptName, "hgSuggest") + ) // note: this doesn't work for hgSuggest because it doesn't set the dumpStack handler. + // TODO find or add a better method to tell if it would already dumpStack on abort. + ) + { + va_list dump_args; + va_copy(dump_args, args); + vaDumpStack(format, dump_args); + va_end(dump_args); + } + + if (sameString(noSqlInjLevel, "warn")) + { + vaWarn(format, args); + } + + if (sameString(noSqlInjLevel, "abort")) + { + vaErrAbort(format, args); + } + } + +va_end(args); + +}