Было интересно, насколько это уязвимо для SQL-инъекций. Я слышал, что использование подготовленных операторов SQL может обойти эту уязвимость, но я также слышал, что использование двойных кавычек вместо одинарных также может предотвратить внедрение SQL. Я не эксперт по безопасности, и я не очень хорошо разбираюсь в sqlite. Кроме того, мне нужно инициализировать базу данных в другом месте и, вероятно, в конечном итоге использовать подготовленные операторы вместо sprintf, но я просто не совсем уверен, как сделать одну из этих вещей. Любая помощь приветствуется! Спасибо!
bool sql_console_msgs = false;
void QServ::savestats(clientinfo *ci)
{
if (enable_sqlite_db) {
sqlite3 *db;
char *zErrMsg = 0;
int rc;
const char *sql;
bool name_match;
const char* player_database_names;
char *p_name = ci->name;
char *p_ip = ci->ip;
rc = sqlite3_open("playerinfo.db", &db);
if ( rc ){
fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db));
exit(0);
}else{
if (sql_console_msgs) fprintf(stdout, "Opened database successfully\n");
}
if ( rc != SQLITE_OK ){
fprintf(stderr, "SQL Database Error: %s\n", zErrMsg);
sqlite3_free(zErrMsg);
}else{
sqlite3_stmt *stmt;
defformatstring(sqlstrprep)("SELECT NAME FROM PLAYERINFO");
rc = sqlite3_prepare_v2(db, sqlstrprep, -1, &stmt, NULL);
while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
player_database_names = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
if (!strcmp(player_database_names, p_name)) name_match = true;
else name_match = false;
}
}
sql = "CREATE TABLE IF NOT EXISTS PLAYERINFO(" \
"NAME TEXT NOT NULL," \
"FRAGS INT NOT NULL," \
"DEATHS INT NOT NULL," \
"FLAGS INT NOT NULL," \
"PASSES INT NOT NULL," \
"IP TEXT NOT NULL," \
"ACCURACY DECIMAL(4, 2) NOT NULL," \
"KPD DECIMAL(4, 2) NOT NULL);";
rc = sqlite3_exec(db, sql, callback, 0, &zErrMsg);
if ( rc != SQLITE_OK ){
fprintf(stderr, "SQLITE3 ERROR @ CREATE TABLE IF NOT EXISTS: %s\n", zErrMsg);
sqlite3_free(zErrMsg);
}else{
if (sql_console_msgs) {
if (!name_match) fprintf(stdout, "No previous record found under that name\n");
else fprintf(stdout, "Found name and IP already, updating record instead\n");
}
}
char sqlINSERT[256];
char sqlUPDATE[1000];
int p_frags = ci->state.frags;
int p_deaths = ci->state.deaths;
int p_flags = ci->state.flags;
int p_passes = ci->state.passes;
int p_acc = (ci->state.damage*100)/max(ci->state.shotdamage, 1);
int p_kpd = (ci->state.frags)/max(ci->state.deaths, 1);
//name and ip are different
if (!name_match) {
sprintf(sqlINSERT, "INSERT INTO PLAYERINFO( NAME,FRAGS,DEATHS,FLAGS,PASSES,IP,ACCURACY,KPD ) VALUES ('%s', %d, %d, %d, %d, '%s', %d, %d)",p_name,p_frags,p_deaths,p_flags,p_passes,p_ip,p_acc,p_kpd);
rc = sqlite3_exec(db, sqlINSERT, callback, 0, &zErrMsg);
}
//client name matches db record, update db if new info is > than db info
else if (name_match) {
sprintf(sqlUPDATE,
"UPDATE PLAYERINFO SET FRAGS = %d+(SELECT FRAGS FROM PLAYERINFO) WHERE NAME = '%s';" \
"UPDATE PLAYERINFO SET DEATHS = %d+(SELECT DEATHS FROM PLAYERINFO) WHERE NAME = '%s';" \
"UPDATE PLAYERINFO SET FLAGS = %d+(SELECT FLAGS FROM PLAYERINFO) WHERE NAME = '%s';" \
"UPDATE PLAYERINFO SET PASSES = %d+(SELECT PASSES FROM PLAYERINFO) WHERE NAME = '%s';" \
"UPDATE PLAYERINFO SET ACCURACY = %d+(SELECT PASSES FROM PLAYERINFO) WHERE NAME = '%s';" \
"UPDATE PLAYERINFO SET KPD = %d+(SELECT PASSES FROM PLAYERINFO) WHERE NAME = '%s';",
ci->state.frags, ci->name, ci->state.deaths, ci->name, ci->state.flags, ci->name, ci->state.passes, ci->name, p_acc, ci->name, p_kpd, ci->name);
rc = sqlite3_exec(db, sqlUPDATE, callback, 0, &zErrMsg);
}
if ( rc != SQLITE_OK ){
fprintf(stderr, "SQLITE3 ERROR @ INSERT & UPDATE: %s\n", zErrMsg);
sqlite3_free(zErrMsg);
}else{
if (sql_console_msgs) fprintf(stdout, "Playerinfo modified\n");
}
sqlite3_close(db);
}
}
void QServ::getstats(clientinfo *ci)
{
if (enable_sqlite_db) {
sqlite3 *db;
char *zErrMsg = 0;
int rc;
char *sql;
const char* data = "Callback function called";
rc = sqlite3_open("playerinfo.db", &db);
if ( rc ){
fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db));
exit(0);
}
if ( rc != SQLITE_OK ){
fprintf(stderr, "SQL Database Error: %s\n", zErrMsg);
sqlite3_free(zErrMsg);
}else{
sqlite3_stmt *stmt;
defformatstring(sqlstrprep)("SELECT NAME,FRAGS,ACCURACY,KPD FROM PLAYERINFO WHERE NAME == '%s';", ci->name);
rc = sqlite3_prepare_v2(db, sqlstrprep, -1, &stmt, NULL);
bool necho = false;
while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
const char* name = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
const char* allfrags = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1));
const char* avgacc = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 2));
const char* avgkpd = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 3));
if (!necho) {
if (avgacc == NULL) out(ECHO_SERV, "Name: \f0%s\f7, Total Frags: \f3%s\f7, Average KPD: \f6%s", name, allfrags, avgkpd);
else if (avgkpd == NULL) out(ECHO_SERV, "Name: \f0%s\f7, Total Frags: \f3%s\f7, Average Accuracy: \f2%s%%", name, allfrags, avgacc);
else out(ECHO_SERV, "Name: \f0%s\f7, Total Frags: \f3%s\f7, Average Accuracy: \f2%s%%\f7, Average KPD: \f6%s", name,allfrags,avgacc,avgkpd);
necho = true;
}
}
}
sqlite3_close(db);
}
}
void QServ::getnames(clientinfo *ci) {
if (enable_sqlite_db) {
sqlite3_stmt *stmt3;
sqlite3 *db;
int rc;
rc = sqlite3_open("playerinfo.db", &db);
defformatstring(sqlstrprep3)("SELECT group_concat(NAME, ', ') FROM PLAYERINFO WHERE IP == '%s';", ci->ip);
rc = sqlite3_prepare_v2(db, sqlstrprep3, -1, &stmt3, NULL);
while ((rc = sqlite3_step(stmt3)) == SQLITE_ROW) {
std::string names(reinterpret_cast<const char*>(sqlite3_column_text(stmt3, 0)));
defformatstring(nmsg)("Names from IP \f2%s\f7: %s", ci->ip, names.c_str());
out(ECHO_SERV, nmsg);
}
sqlite3_close(db);
}
}
@Tas Я считаю, что это может уничтожить всю базу данных playerinfo.db.
При форматировании оператора SQL вручную (что вам действительно НЕ СЛЕДУЕТ делать, когда задействованы входные параметры!), недостаточно просто заключить значения параметров в кавычки. Вам также необходимо экранировать зарезервированные символы внутри данных параметра, иначе вы все равно будете подвержены атакам путем внедрения. Злоумышленник может просто поместить совпадающую цитату внутри данных, закрывая вашу открывающую цитату, а затем остальные данные параметра могут содержать вредоносные инструкции.
Например:
const char *p_name = "'); DROP TABLE MyTable; --";
sprintf(sql, "INSERT INTO MyTable(NAME) VALUES ('%s')", p_name);
Или:
const char *p_name = "\"); DROP TABLE MyTable; --";
sprintf(sql, "INSERT INTO MyTable(NAME) VALUES (\"%s\")", p_name);
Это создаст следующие операторы SQL:
INSERT INTO MyTable(NAME) VALUES (''); DELETE TABLE MyTable; --')
INSERT INTO MyTable(NAME) VALUES (""); DELETE TABLE MyTable; --")
Скажите «до свидания» вашей таблице, когда SQL будет выполнен! (при условии, что пользователь, выполняющий SQL, имеет DELETE
доступ к таблице — это отдельная проблема безопасности).
В этом случае вам нужно будет удвоить любые символы в одинарных кавычках или экранировать косой чертой любые символы в двойных кавычках, которые находятся в данных параметра, например:
const char *p_name = "'); DROP TABLE MyTable; --";
char *p_escaped_name = sqlEscape(p_name); // <-- you have to implement this yourself!
sprintf(sql, "INSERT INTO MyTable(NAME) VALUES ('%s')", p_escaped_name);
// or:
// sprintf(sql, "INSERT INTO MyTable(NAME) VALUES (\"%s\")", p_escaped_name);
free(p_escaped_name);
Таким образом, результирующие операторы SQL будут выглядеть следующим образом:
INSERT INTO MyTable(NAME) VALUES ('''); DELETE TABLE MyTable; --')
INSERT INTO MyTable(NAME) VALUES ("\"); DELETE TABLE MyTable; --")
Таким образом, имя, вставленное в таблицу, будет '); DELETE TABLE MyTable; --
(или "); DELETE TABLE MyTable; --
). Некрасиво, но таблица будет спасена.
Некоторые фреймворки БД предлагают функции для этого экранирования за вас, но я не вижу их в sqlite, поэтому вам придется реализовать их вручную в своем собственном коде, например:
char* sqlEscape(const char *str)
{
int len = strlen(str);
int newlen = len;
for (int i = 0; i < len; ++i) {
switch (str[i]) {
case '\'':
case '"':
++newlen;
break;
}
}
if (newlen == len)
return strdup(str);
char *newstr = (char*) malloc(newlen + 1);
if (!newstr)
return NULL;
newlen = 0;
for (int i = 0; i < len; ++i) {
switch (str[i]) {
case '\'':
newstr[newlen++] = '\'';
break;
case '"':
newstr[newlen++] = '\\';
break;
}
newstr[newlen++] = str[i];
}
newstr[newlen] = '\0';
return newstr;
}
Подготовленный оператор позволяет избежать необходимости делать это вручную, позволяя механизму БД обрабатывать эти детали для вас при выполнении подготовленного оператора.
Кроме того, использование sprintf()
в вашем коде подвержено переполнению буфера, что еще хуже, потому что тщательно продуманное переполнение буфера может позволить злоумышленнику выполнить произвольный машинный код внутри вашего приложения, а не только в базе данных. Вместо этого используйте snprintf()
, чтобы избежать этого.
Спасибо за обширный ответ. :) Будет использоваться snprintf и замена всех вхождений '%s' на \"%s\".
@someguy "замена всех вхождений %s на \"%s\"" - это само по себе не решит проблему. Все, что нужно сделать злоумышленнику, — изменить '
на "
в своих вредоносных данных. Я обновил свой пример, чтобы показать это. Как поясняет большая часть моего ответа, выполнение одной этой замены в строке формата SQL не является адекватной защитой. Вам нужно экранировать фактические строковые данные, которые передаются в %s
Спасибо за обновление, очень информативно.
Вот обновленный код с предложениями @Remy Lebeau.
//no naughty sql injection
char* sqlEscape(const char *str)
{
int len = strlen(str);
int newlen = len;
for (int i = 0; i < len; ++i) {
switch (str[i]) {
case '\'':
case '"':
++newlen;
break;
}
}
if (newlen == len)
return strdup(str);
char *newstr = (char*) malloc(newlen + 1);
if (!newstr)
return NULL;
newlen = 0;
for (int i = 0; i < len; ++i) {
switch (str[i]) {
case '\'':
newstr[newlen++] = '\'';
break;
case '"':
newstr[newlen++] = '\\';
break;
}
newstr[newlen++] = str[i];
}
newstr[newlen] = '\0';
return newstr;
}
static int callback(void *NotUsed, int argc, char **argv, char **azColName){
int i;
for(i=0; i<argc; i++){
printf("%s = %s\n", azColName[i], argv[i] ? argv[i] : "NULL");
}
printf("\n");
return 0;
}
bool sql_console_msgs = false;
void QServ::savestats(clientinfo *ci)
{
if (enable_sqlite_db) {
sqlite3 *db;
char *zErrMsg = 0;
int rc;
const char *sql;
bool name_match;
const char* player_database_names;
char *p_name = ci->name;
char *p_ip = ci->ip;
rc = sqlite3_open("playerinfo.db", &db);
if ( rc ){
fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db));
exit(0);
}else{
if (sql_console_msgs) fprintf(stdout, "Opened database successfully\n");
}
if ( rc != SQLITE_OK ){
fprintf(stderr, "SQL Database Error: %s\n", zErrMsg);
sqlite3_free(zErrMsg);
}else{
sqlite3_stmt *stmt;
defformatstring(sqlstrprep)("SELECT NAME FROM PLAYERINFO");
rc = sqlite3_prepare_v2(db, sqlstrprep, -1, &stmt, NULL);
while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
player_database_names = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
if (!strcmp(player_database_names, p_name)) name_match = true;
else name_match = false;
}
}
sql = "CREATE TABLE IF NOT EXISTS PLAYERINFO(" \
"NAME TEXT NOT NULL," \
"FRAGS INT NOT NULL," \
"DEATHS INT NOT NULL," \
"FLAGS INT NOT NULL," \
"PASSES INT NOT NULL," \
"IP TEXT NOT NULL," \
"ACCURACY DECIMAL(4, 2) NOT NULL," \
"KPD DECIMAL(4, 2) NOT NULL);";
rc = sqlite3_exec(db, sql, callback, 0, &zErrMsg);
if ( rc != SQLITE_OK ){
fprintf(stderr, "SQLITE3 ERROR @ CREATE TABLE IF NOT EXISTS: %s\n", zErrMsg);
sqlite3_free(zErrMsg);
}else{
if (sql_console_msgs) {
if (!name_match) fprintf(stdout, "No previous record found under that name\n");
else fprintf(stdout, "Found name and IP already, updating record instead\n");
}
}
char sqlINSERT[256];
char sqlUPDATE[1000];
int p_frags = ci->state.frags;
int p_deaths = ci->state.deaths;
int p_flags = ci->state.flags;
int p_passes = ci->state.passes;
int p_acc = (ci->state.damage*100)/max(ci->state.shotdamage, 1);
int p_kpd = (ci->state.frags)/max(ci->state.deaths, 1);
//name and ip are different
if (!name_match) {
snprintf(sqlINSERT, sizeof(sqlINSERT), "INSERT INTO PLAYERINFO( NAME,FRAGS,DEATHS,FLAGS,PASSES,IP,ACCURACY,KPD ) VALUES (\"%s\", %d, %d, %d, %d, \"%s\", %d, %d)",p_name,p_frags,p_deaths,p_flags,p_passes,p_ip,p_acc,p_kpd);
sqlEscape(sqlINSERT);
rc = sqlite3_exec(db, sqlINSERT, callback, 0, &zErrMsg);
}
//client name matches db record, update db if new info is > than db info
else if (name_match) {
snprintf(sqlUPDATE, sizeof(sqlUPDATE),
"UPDATE PLAYERINFO SET FRAGS = %d+(SELECT FRAGS FROM PLAYERINFO) WHERE NAME = \"%s\";" \
"UPDATE PLAYERINFO SET DEATHS = %d+(SELECT DEATHS FROM PLAYERINFO) WHERE NAME = \"%s\";" \
"UPDATE PLAYERINFO SET FLAGS = %d+(SELECT FLAGS FROM PLAYERINFO) WHERE NAME = \"%s\";" \
"UPDATE PLAYERINFO SET PASSES = %d+(SELECT PASSES FROM PLAYERINFO) WHERE NAME = \"%s\";" \
"UPDATE PLAYERINFO SET ACCURACY = %d+(SELECT PASSES FROM PLAYERINFO) WHERE NAME = \"%s\";" \
"UPDATE PLAYERINFO SET KPD = %d+(SELECT PASSES FROM PLAYERINFO) WHERE NAME = \"%s\";",
ci->state.frags, ci->name, ci->state.deaths, ci->name, ci->state.flags, ci->name, ci->state.passes, ci->name, p_acc, ci->name, p_kpd, ci->name);
sqlEscape(sqlINSERT);
rc = sqlite3_exec(db, sqlUPDATE, callback, 0, &zErrMsg);
}
if ( rc != SQLITE_OK ){
fprintf(stderr, "SQLITE3 ERROR @ INSERT & UPDATE: %s\n", zErrMsg);
sqlite3_free(zErrMsg);
}else{
if (sql_console_msgs) fprintf(stdout, "Playerinfo modified\n");
}
sqlite3_close(db);
}
}
void QServ::getstats(clientinfo *ci)
{
if (enable_sqlite_db) {
sqlite3 *db;
char *zErrMsg = 0;
int rc;
char *sql;
const char* data = "Callback function called";
rc = sqlite3_open("playerinfo.db", &db);
if ( rc ){
fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db));
exit(0);
}
if ( rc != SQLITE_OK ){
fprintf(stderr, "SQL Database Error: %s\n", zErrMsg);
sqlite3_free(zErrMsg);
}else{
sqlite3_stmt *stmt;
defformatstring(sqlstrprep)("SELECT NAME,FRAGS,ACCURACY,KPD FROM PLAYERINFO WHERE NAME == \"%s\";", ci->name);
rc = sqlite3_prepare_v2(db, sqlstrprep, -1, &stmt, NULL);
bool necho = false;
while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
const char* name = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
const char* allfrags = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1));
const char* avgacc = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 2));
const char* avgkpd = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 3));
if (!necho) {
if (avgacc == NULL) out(ECHO_SERV, "Name: \f0%s\f7, Total Frags: \f3%s\f7, Average KPD: \f6%s", name, allfrags, avgkpd);
else if (avgkpd == NULL) out(ECHO_SERV, "Name: \f0%s\f7, Total Frags: \f3%s\f7, Average Accuracy: \f2%s%%", name, allfrags, avgacc);
else out(ECHO_SERV, "Name: \f0%s\f7, Total Frags: \f3%s\f7, Average Accuracy: \f2%s%%\f7, Average KPD: \f6%s", name,allfrags,avgacc,avgkpd);
necho = true;
}
}
}
sqlite3_close(db);
}
}
Собственно вопрос: понимаете ли вы, что такое SQL-инъекция и чем она может быть опасна?