Как остановить уязвимость SQL-инъекций?

Было интересно, насколько это уязвимо для 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);
    }
}

Собственно вопрос: понимаете ли вы, что такое SQL-инъекция и чем она может быть опасна?

Tas 10.12.2020 01:44

@Tas Я считаю, что это может уничтожить всю базу данных playerinfo.db.

someguy 10.12.2020 01:51
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
2
541
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

При форматировании оператора 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 10.12.2020 02:38

@someguy "замена всех вхождений %s на \"%s\"" - это само по себе не решит проблему. Все, что нужно сделать злоумышленнику, — изменить ' на " в своих вредоносных данных. Я обновил свой пример, чтобы показать это. Как поясняет большая часть моего ответа, выполнение одной этой замены в строке формата SQL не является адекватной защитой. Вам нужно экранировать фактические строковые данные, которые передаются в %s

Remy Lebeau 10.12.2020 02:39

Спасибо за обновление, очень информативно.

someguy 10.12.2020 05:28

Вот обновленный код с предложениями @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);
    }
}

Другие вопросы по теме