0 added
0 removed
Original
2026-01-01
Modified
2026-02-21
1
<p><a>#Руководства</a></p>
1
<p><a>#Руководства</a></p>
2
<ul><li>25 сен 2019</li>
2
<ul><li>25 сен 2019</li>
3
<li>0</li>
3
<li>0</li>
4
</ul><p>Запросы к базе данных - уязвимая часть любого приложения. Объясняем, как защитить их от возможных атак и последующих ошибок и сбоев.</p>
4
</ul><p>Запросы к базе данных - уязвимая часть любого приложения. Объясняем, как защитить их от возможных атак и последующих ошибок и сбоев.</p>
5
<p> vlada_maestro / shutterstock</p>
5
<p> vlada_maestro / shutterstock</p>
6
<p>Пишет о программировании, в свободное время создаёт игры. Мечтает открыть свою студию и выпускать ламповые RPG.</p>
6
<p>Пишет о программировании, в свободное время создаёт игры. Мечтает открыть свою студию и выпускать ламповые RPG.</p>
7
<p>Если в приложении нет защиты от атак или ошибок, то оно будет не только нестабильным, но и опасным. Например, оно может закрыться в самый неподходящий момент - и пропадут не сохраненные пользователем данные, его могут взломать - и информацию из СУБД украдут или удалят.</p>
7
<p>Если в приложении нет защиты от атак или ошибок, то оно будет не только нестабильным, но и опасным. Например, оно может закрыться в самый неподходящий момент - и пропадут не сохраненные пользователем данные, его могут взломать - и информацию из СУБД украдут или удалят.</p>
8
<p>Невозможно написать приложение, которое будет абсолютно стабильно и безопасно работать с базой данных. Однако есть простые способы защиты от большинства угроз:</p>
8
<p>Невозможно написать приложение, которое будет абсолютно стабильно и безопасно работать с базой данных. Однако есть простые способы защиты от большинства угроз:</p>
9
<ul><li>проверка вводимой информации;</li>
9
<ul><li>проверка вводимой информации;</li>
10
<li>обработка исключений;</li>
10
<li>обработка исключений;</li>
11
<li>использование параметров и процедур;</li>
11
<li>использование параметров и процедур;</li>
12
<li>шифрование данных и установка паролей.</li>
12
<li>шифрование данных и установка паролей.</li>
13
</ul><p>Рассмотрим эти способы на примере регистрации и аутентификации в WPF-приложении (полный код приложения можно найти в этом<a>репозитории на GitHub</a>).</p>
13
</ul><p>Рассмотрим эти способы на примере регистрации и аутентификации в WPF-приложении (полный код приложения можно найти в этом<a>репозитории на GitHub</a>).</p>
14
<p>Валидация - это проверка вводимых данных: в идеале нужно проверять всё, что только вносится в базу. К примеру, смотрим:</p>
14
<p>Валидация - это проверка вводимых данных: в идеале нужно проверять всё, что только вносится в базу. К примеру, смотрим:</p>
15
<ul><li>правильный ли выбран тип - цифры в int, а не в string, и даты, выбранные с помощью DatePicker, преобразованы в DateTime из DateTime?;</li>
15
<ul><li>правильный ли выбран тип - цифры в int, а не в string, и даты, выбранные с помощью DatePicker, преобразованы в DateTime из DateTime?;</li>
16
<li>находится ли значение в допустимых пределах - строки не превышают указанную длину, цифры умещаются в свой тип, а даты находятся в заданных пределах (например, день рождения может быть от 1900 до 2019);</li>
16
<li>находится ли значение в допустимых пределах - строки не превышают указанную длину, цифры умещаются в свой тип, а даты находятся в заданных пределах (например, день рождения может быть от 1900 до 2019);</li>
17
<li>находятся ли переданные значения в черном или белом списках - например, можно запретить указывать в качестве имени пользователя матерные слова;</li>
17
<li>находятся ли переданные значения в черном или белом списках - например, можно запретить указывать в качестве имени пользователя матерные слова;</li>
18
<li>введены ли вообще какие-то данные;</li>
18
<li>введены ли вообще какие-то данные;</li>
19
<li>нет ли чего-то лишнего - пробелов в начале или конце,<a>HTML-кода</a>.</li>
19
<li>нет ли чего-то лишнего - пробелов в начале или конце,<a>HTML-кода</a>.</li>
20
</ul><p>Вот пример того, как данные могут проверяться на соответствие этим требованиям:</p>
20
</ul><p>Вот пример того, как данные могут проверяться на соответствие этим требованиям:</p>
21
private void RegistrationButton_Click(object sender, RoutedEventArgs e) //Метод, который вызывается при нажатии на кнопку регистрации { int id = 0; //Идентификатор устанавливается на ноль, его автоматически подберет СУБД string name = RegNameBox.Text.Trim(); //Обрезаются пробелы в начале и конце строк string login = RegLoginBox.Text.Trim(); string password = RegPasswordBox.Password.Trim(); string passwordConfirm = RegPasswordConfirmBox.Password.Trim(); DateTime birthDate = new DateTime(1800, 1, 1); //Создается дата 01.01.1800 - если этого не сделать, будет указана дата 01.01.0001 - она выходит за допустимые пределы try { birthDate = (DateTime)RegDateBox.SelectedDate; //Попытка получить дату из поля. Если пользователь ничего не выбрал, будет вызвано исключение } catch (Exception exc) { } if (!string.IsNullOrEmpty(login)) //Проверка заполнения поля с логином { if (!string.IsNullOrEmpty(name)) //Проверка заполнения поля с именем { if (!string.IsNullOrEmpty(password) && !string.IsNullOrEmpty(passwordConfirm)) //Проверка заполнения полей с паролями { if (password == passwordConfirm) //Проверка совпадения паролей { if (!birthDate.Equals(new DateTime(1800, 1, 1))) //Проверка заполнения даты { bool success = Database.Add(new User(id, name, login, password, birthDate, DateTime.Now)); //Попытка внести данные в таблицу if (success) //Если всё прошло успешно, пользователь сможет войти { MessageBox.Show("Congradulations! You can now log in!", "Success!", MessageBoxButton.OK, MessageBoxImage.Information); Show(LogInBorder); //Смена экрана } else //Если нет, он увидит сообщение, что что-то пошло не так { MessageBox.Show("Something went wrong! Maybe user with such data already exists!", "Error", MessageBoxButton.OK, MessageBoxImage.Warning); } } else { MessageBox.Show("The date field is empty!", "Error", MessageBoxButton.OK, MessageBoxImage.Warning); } } else { MessageBox.Show("Passwords are different!", "Error", MessageBoxButton.OK, MessageBoxImage.Warning); } } else { MessageBox.Show("One or both password fields are empty!", "Error", MessageBoxButton.OK, MessageBoxImage.Warning); } } else { MessageBox.Show("The name field is empty!", "Error", MessageBoxButton.OK, MessageBoxImage.Warning); } } else { MessageBox.Show("The login field is empty!", "Error", MessageBoxButton.OK, MessageBoxImage.Warning); } }<p>Вот что будет, если попробовать ввести некорректные данные:</p>
21
private void RegistrationButton_Click(object sender, RoutedEventArgs e) //Метод, который вызывается при нажатии на кнопку регистрации { int id = 0; //Идентификатор устанавливается на ноль, его автоматически подберет СУБД string name = RegNameBox.Text.Trim(); //Обрезаются пробелы в начале и конце строк string login = RegLoginBox.Text.Trim(); string password = RegPasswordBox.Password.Trim(); string passwordConfirm = RegPasswordConfirmBox.Password.Trim(); DateTime birthDate = new DateTime(1800, 1, 1); //Создается дата 01.01.1800 - если этого не сделать, будет указана дата 01.01.0001 - она выходит за допустимые пределы try { birthDate = (DateTime)RegDateBox.SelectedDate; //Попытка получить дату из поля. Если пользователь ничего не выбрал, будет вызвано исключение } catch (Exception exc) { } if (!string.IsNullOrEmpty(login)) //Проверка заполнения поля с логином { if (!string.IsNullOrEmpty(name)) //Проверка заполнения поля с именем { if (!string.IsNullOrEmpty(password) && !string.IsNullOrEmpty(passwordConfirm)) //Проверка заполнения полей с паролями { if (password == passwordConfirm) //Проверка совпадения паролей { if (!birthDate.Equals(new DateTime(1800, 1, 1))) //Проверка заполнения даты { bool success = Database.Add(new User(id, name, login, password, birthDate, DateTime.Now)); //Попытка внести данные в таблицу if (success) //Если всё прошло успешно, пользователь сможет войти { MessageBox.Show("Congradulations! You can now log in!", "Success!", MessageBoxButton.OK, MessageBoxImage.Information); Show(LogInBorder); //Смена экрана } else //Если нет, он увидит сообщение, что что-то пошло не так { MessageBox.Show("Something went wrong! Maybe user with such data already exists!", "Error", MessageBoxButton.OK, MessageBoxImage.Warning); } } else { MessageBox.Show("The date field is empty!", "Error", MessageBoxButton.OK, MessageBoxImage.Warning); } } else { MessageBox.Show("Passwords are different!", "Error", MessageBoxButton.OK, MessageBoxImage.Warning); } } else { MessageBox.Show("One or both password fields are empty!", "Error", MessageBoxButton.OK, MessageBoxImage.Warning); } } else { MessageBox.Show("The name field is empty!", "Error", MessageBoxButton.OK, MessageBoxImage.Warning); } } else { MessageBox.Show("The login field is empty!", "Error", MessageBoxButton.OK, MessageBoxImage.Warning); } }<p>Вот что будет, если попробовать ввести некорректные данные:</p>
22
<p><strong>Примечание</strong></p>
22
<p><strong>Примечание</strong></p>
23
<p><em>Чтобы улучшить пользовательский опыт, можно подсвечивать некорректно заполненные поля красным цветом.</em></p>
23
<p><em>Чтобы улучшить пользовательский опыт, можно подсвечивать некорректно заполненные поля красным цветом.</em></p>
24
<p>Несмотря на валидацию, иногда возникают ошибки (исключения), которые могут привести к закрытию приложения: отсутствие подключения к базе данных, неверный пароль, превышение интервала ожидания и так далее.</p>
24
<p>Несмотря на валидацию, иногда возникают ошибки (исключения), которые могут привести к закрытию приложения: отсутствие подключения к базе данных, неверный пароль, превышение интервала ожидания и так далее.</p>
25
<p>Все эти исключения можно обработать с помощью блока<em>try-catch</em> - например, поместить в него подключение к базе данных:</p>
25
<p>Все эти исключения можно обработать с помощью блока<em>try-catch</em> - например, поместить в него подключение к базе данных:</p>
26
public static bool Add(User user) { bool success = false; using (SqlConnection connection = new SqlConnection(cs)) { try { connection.Open(); //Попытка подключения success = true; //Запрос на добавление пользователя } catch(Exception exc) { success = false; //Если код внутри блока try выполнить не удалось, будет выполнен код в блоке catch } } return success; }<p>Так можно быть уверенным, что приложение не закроется, если возникнет какая-то ошибка.</p>
26
public static bool Add(User user) { bool success = false; using (SqlConnection connection = new SqlConnection(cs)) { try { connection.Open(); //Попытка подключения success = true; //Запрос на добавление пользователя } catch(Exception exc) { success = false; //Если код внутри блока try выполнить не удалось, будет выполнен код в блоке catch } } return success; }<p>Так можно быть уверенным, что приложение не закроется, если возникнет какая-то ошибка.</p>
27
<p><strong>Примечание</strong></p>
27
<p><strong>Примечание</strong></p>
28
<p><em>Обрабатывайте исключения везде, где они могут возникнуть.</em></p>
28
<p><em>Обрабатывайте исключения везде, где они могут возникнуть.</em></p>
29
<p>Параметры позволяют передавать данные отдельно от запроса. Это важно потому, что это один из лучших способов защитить базу от SQL-инъекции:</p>
29
<p>Параметры позволяют передавать данные отдельно от запроса. Это важно потому, что это один из лучших способов защитить базу от SQL-инъекции:</p>
30
public static bool Add(User user) //Добавление пользователя { bool success = false; using (SqlConnection connection = new SqlConnection(cs)) { try { connection.Open(); //Открытие соединения SqlCommand check = new SqlCommand("SELECT COUNT(*) FROM Users WHERE login = @login", connection); //Запрос для проверки существования пользователя check.Parameters.Add(new SqlParameter("@login", user.Login)); //Указывается параметр int count = (int)check.ExecuteScalar(); //Получение числа пользователей с таким логином if (count == 0) //Если таких пользователей нет, продолжается регистрация { SqlCommand command = new SqlCommand("INSERT INTO Users (name, login, password, birth_date, registration_date) VALUES (@name, @login, @password, @birth_date, @registration_date)", connection); //Команда для добавления пользователя //Параметры command.Parameters.Add(new SqlParameter("@name", user.Name)); command.Parameters.Add(new SqlParameter("@login", user.Login)); command.Parameters.Add(new SqlParameter("@password", user.Password)); command.Parameters.Add(new SqlParameter("@birth_date", user.BirthDate)); command.Parameters.Add(new SqlParameter("@registration_date", user.RegistrationDate)); int result = command.ExecuteNonQuery(); //Выполнение команды if (result == 1) //Если была добавлена одна строка, то всё прошло удачно { success = true; } } } catch (Exception exc) { } } return success; }<p>Таким образом, база данных сначала получает запрос, а потом подставляет значения в места, где указаны параметры. То есть, даже если пользователь попробует ввести команду удаления таблицы, ничего не произойдет - команда будет воспринята как обычная строка. Поэтому в базе может появиться поле с таким значением:</p>
30
public static bool Add(User user) //Добавление пользователя { bool success = false; using (SqlConnection connection = new SqlConnection(cs)) { try { connection.Open(); //Открытие соединения SqlCommand check = new SqlCommand("SELECT COUNT(*) FROM Users WHERE login = @login", connection); //Запрос для проверки существования пользователя check.Parameters.Add(new SqlParameter("@login", user.Login)); //Указывается параметр int count = (int)check.ExecuteScalar(); //Получение числа пользователей с таким логином if (count == 0) //Если таких пользователей нет, продолжается регистрация { SqlCommand command = new SqlCommand("INSERT INTO Users (name, login, password, birth_date, registration_date) VALUES (@name, @login, @password, @birth_date, @registration_date)", connection); //Команда для добавления пользователя //Параметры command.Parameters.Add(new SqlParameter("@name", user.Name)); command.Parameters.Add(new SqlParameter("@login", user.Login)); command.Parameters.Add(new SqlParameter("@password", user.Password)); command.Parameters.Add(new SqlParameter("@birth_date", user.BirthDate)); command.Parameters.Add(new SqlParameter("@registration_date", user.RegistrationDate)); int result = command.ExecuteNonQuery(); //Выполнение команды if (result == 1) //Если была добавлена одна строка, то всё прошло удачно { success = true; } } } catch (Exception exc) { } } return success; }<p>Таким образом, база данных сначала получает запрос, а потом подставляет значения в места, где указаны параметры. То есть, даже если пользователь попробует ввести команду удаления таблицы, ничего не произойдет - команда будет воспринята как обычная строка. Поэтому в базе может появиться поле с таким значением:</p>
31
password'); DELETE FROM table;<p>Хранимые процедуры позволяют еще быстрее и безопаснее выполнять запросы. Их суть в том, что запросы хранятся на сервере, а из программы туда просто передаются параметры.</p>
31
password'); DELETE FROM table;<p>Хранимые процедуры позволяют еще быстрее и безопаснее выполнять запросы. Их суть в том, что запросы хранятся на сервере, а из программы туда просто передаются параметры.</p>
32
<p>Создать хранимую процедуру можно в MS SQL Server Management Studio. Для этого нажмите правой кнопкой мыши на <em>название базы -> Programmability -> Stored Procedures</em>и контекстном меню выберите пункт<em>Stored Procedure…</em>:</p>
32
<p>Создать хранимую процедуру можно в MS SQL Server Management Studio. Для этого нажмите правой кнопкой мыши на <em>название базы -> Programmability -> Stored Procedures</em>и контекстном меню выберите пункт<em>Stored Procedure…</em>:</p>
33
<p>В появившемся окне нужно ввести вот такой код:</p>
33
<p>В появившемся окне нужно ввести вот такой код:</p>
34
CREATE PROCEDURE [dbo].[sp_Login] //Название процедуры @login nvarchar(200), //Получаемые параметры @password nvarchar(200) AS //Далее сам запрос SELECT * FROM Users WHERE login = @login AND password = @password GO //Конец процедуры<p>Чтобы процедура добавилась, нажмите<em>Execute</em>. Вот как выглядит запрос с использованием хранимой процедуры:</p>
34
CREATE PROCEDURE [dbo].[sp_Login] //Название процедуры @login nvarchar(200), //Получаемые параметры @password nvarchar(200) AS //Далее сам запрос SELECT * FROM Users WHERE login = @login AND password = @password GO //Конец процедуры<p>Чтобы процедура добавилась, нажмите<em>Execute</em>. Вот как выглядит запрос с использованием хранимой процедуры:</p>
35
SqlCommand command = new SqlCommand("sp_Login", connection); //Вместо запроса передается название процедуры command.CommandType = System.Data.CommandType.StoredProcedure; //Указывается тип команды command.Parameters.Add(new SqlParameter("@login", login)); //Передаются параметры command.Parameters.Add(new SqlParameter("@password", password));<p>Теперь сервер сможет выполнить запрос быстрее, потому что ему нужно только получить входные данные. Кроме того, злоумышленник не узнает подробности работы с СУБД, если ему удастся получить исходный код приложения.</p>
35
SqlCommand command = new SqlCommand("sp_Login", connection); //Вместо запроса передается название процедуры command.CommandType = System.Data.CommandType.StoredProcedure; //Указывается тип команды command.Parameters.Add(new SqlParameter("@login", login)); //Передаются параметры command.Parameters.Add(new SqlParameter("@password", password));<p>Теперь сервер сможет выполнить запрос быстрее, потому что ему нужно только получить входные данные. Кроме того, злоумышленник не узнает подробности работы с СУБД, если ему удастся получить исходный код приложения.</p>
36
<p>Всё вышеперечисленное защищает базу данных от неправильного ввода и появления ошибок. Но взломщик может попытаться получить доступ к ней напрямую. Если ему это удастся, то он сможет выполнять любые запросы.</p>
36
<p>Всё вышеперечисленное защищает базу данных от неправильного ввода и появления ошибок. Но взломщик может попытаться получить доступ к ней напрямую. Если ему это удастся, то он сможет выполнять любые запросы.</p>
37
<p>Чтобы уменьшить шансы на взлом, можно установить на базу данных пароль. Для этого зайдите в MS SQL Management Studio и добавьте нового пользователя:</p>
37
<p>Чтобы уменьшить шансы на взлом, можно установить на базу данных пароль. Для этого зайдите в MS SQL Management Studio и добавьте нового пользователя:</p>
38
Добавление нового пользователя в MS SQL Management Studio<p>А затем установите для него пароль:</p>
38
Добавление нового пользователя в MS SQL Management Studio<p>А затем установите для него пароль:</p>
39
Установка пароля в MS SQL Management Studio<p>Другой важный и полезный способ защиты - шифрование. Оно позволяет преобразовать информацию так, чтобы данные нельзя было прочесть без ключа. Шифрование можно установить на базу целиком в меню создания или настроек базы - для этого нужно изменить значение<em>Encryption Enabled</em>:</p>
39
Установка пароля в MS SQL Management Studio<p>Другой важный и полезный способ защиты - шифрование. Оно позволяет преобразовать информацию так, чтобы данные нельзя было прочесть без ключа. Шифрование можно установить на базу целиком в меню создания или настроек базы - для этого нужно изменить значение<em>Encryption Enabled</em>:</p>
40
Включение шифрования базы данных<p>Недостаток этого метода в том, что данные будут кодироваться и декодироваться при каждом запросе, что сильно скажется на производительности. Вместо этого лучше шифровать только отдельные столбцы, в которых содержатся:</p>
40
Включение шифрования базы данных<p>Недостаток этого метода в том, что данные будут кодироваться и декодироваться при каждом запросе, что сильно скажется на производительности. Вместо этого лучше шифровать только отдельные столбцы, в которых содержатся:</p>
41
<ul><li>пароли;</li>
41
<ul><li>пароли;</li>
42
<li>номера телефонов;</li>
42
<li>номера телефонов;</li>
43
<li>адреса почтовых ящиков;</li>
43
<li>адреса почтовых ящиков;</li>
44
<li>физические и юридические адреса;</li>
44
<li>физические и юридические адреса;</li>
45
<li>имена;</li>
45
<li>имена;</li>
46
<li>банковские реквизиты и другие конфиденциальные данные.</li>
46
<li>банковские реквизиты и другие конфиденциальные данные.</li>
47
</ul><p>Узнать о реализации такой защиты можно в <a>официальной документации Microsoft</a>. Хотя это не лучший способ шифрования, он тоже очень эффективен, потому что значительно снижает риск потери данных. Эффективнее использовать программное кодирование по стандарту<a>AES</a>(англ. Advanced Encryption Standard - Расширенный стандарт шифрования), однако это сильно повлияет на производительность. Поэтому в большинстве случаев такие меры излишни.</p>
47
</ul><p>Узнать о реализации такой защиты можно в <a>официальной документации Microsoft</a>. Хотя это не лучший способ шифрования, он тоже очень эффективен, потому что значительно снижает риск потери данных. Эффективнее использовать программное кодирование по стандарту<a>AES</a>(англ. Advanced Encryption Standard - Расширенный стандарт шифрования), однако это сильно повлияет на производительность. Поэтому в большинстве случаев такие меры излишни.</p>
48
<p>Также в параметрах базы данных можно установить и другие полезные опции. Например, разрешить только чтение - тогда нельзя будет добавлять никакие новые сведения, изменять или удалять старые. Это позволит защитить информацию от несанкционированного удаления или изменения - сделать это не сможет даже администратор. Единственная возможность редактировать read only базу будет доступна в Management Studio.</p>
48
<p>Также в параметрах базы данных можно установить и другие полезные опции. Например, разрешить только чтение - тогда нельзя будет добавлять никакие новые сведения, изменять или удалять старые. Это позволит защитить информацию от несанкционированного удаления или изменения - сделать это не сможет даже администратор. Единственная возможность редактировать read only базу будет доступна в Management Studio.</p>
49
<p>Несмотря на то что работа с базой данных может показаться простой, написать приложение, которое будет будет функционировать стабильно и без угрозы для хранимой информации, - сложная задача.</p>
49
<p>Несмотря на то что работа с базой данных может показаться простой, написать приложение, которое будет будет функционировать стабильно и без угрозы для хранимой информации, - сложная задача.</p>
50
<p>Конечно, это только часть методов. Также вы можете попробовать и другие варианты:</p>
50
<p>Конечно, это только часть методов. Также вы можете попробовать и другие варианты:</p>
51
<ul><li>транзакции;</li>
51
<ul><li>транзакции;</li>
52
<li>дополнительное шифрование;</li>
52
<li>дополнительное шифрование;</li>
53
<li>передачу данных в бинарном виде - и всё, на что хватит вашей фантазии или бюджета.</li>
53
<li>передачу данных в бинарном виде - и всё, на что хватит вашей фантазии или бюджета.</li>
54
</ul><a><b>Бесплатный курс по Python ➞</b>Мини-курс для новичков и для опытных кодеров. 4 крутых проекта в портфолио, живое общение со спикером. Кликните и узнайте, чему можно научиться на курсе. Смотреть программу</a>
54
</ul><a><b>Бесплатный курс по Python ➞</b>Мини-курс для новичков и для опытных кодеров. 4 крутых проекта в портфолио, живое общение со спикером. Кликните и узнайте, чему можно научиться на курсе. Смотреть программу</a>