Как защитить базу данных в C#

Запросы к базе данных — уязвимая часть любого приложения. Объясняем, как защитить их от возможных атак и последующих ошибок и сбоев.

Если в приложении нет защиты от атак или ошибок, то оно будет не только нестабильным, но и опасным. Например, оно может закрыться в самый неподходящий момент — и пропадут не сохраненные пользователем данные, его могут взломать — и информацию из СУБД украдут или удалят.

Невозможно написать приложение, которое будет абсолютно стабильно и безопасно работать с базой данных. Однако есть простые способы защиты от большинства угроз:

  • проверка вводимой информации;
  • обработка исключений;
  • использование параметров и процедур;
  • шифрование данных и установка паролей.

Рассмотрим эти способы на примере регистрации и аутентификации в WPF-приложении (полный код приложения можно найти в этом репозитории на GitHub).

Евгений Кучерявый

Пишет о разработке сайтов, в свободное время создает игры. Мечтает открыть свою студию и выпускать ламповые RPG.


Валидация данных

Валидация — это проверка вводимых данных: в идеале нужно проверять всё, что только вносится в базу. К примеру, смотрим:

  • правильный ли выбран тип — цифры в int, а не в string, и даты, выбранные с помощью DatePicker, преобразованы в DateTime из DateTime?;
  • находится ли значение в допустимых пределах — строки не превышают указанную длину, цифры умещаются в свой тип, а даты находятся в заданных пределах (например, день рождения может быть от 1900 до 2019);
  • находятся ли переданные значения в черном или белом списках — например, можно запретить указывать в качестве имени пользователя матерные слова;
  • введены ли вообще какие-то данные;
  • нет ли чего-то лишнего — пробелов в начале или конце, HTML-кода.

Вот пример того, как данные могут проверяться на соответствие этим требованиям:

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);
	}
}

Вот что будет, если попробовать ввести некорректные данные:

Примечание


Чтобы улучшить пользовательский опыт, можно подсвечивать некорректно заполненные поля красным цветом.


Обработка исключений

Несмотря на валидацию, иногда возникают ошибки (исключения), которые могут привести к закрытию приложения: отсутствие подключения к базе данных, неверный пароль, превышение интервала ожидания и так далее.

Все эти исключения можно обработать с помощью блока try-catch — например, поместить в него подключение к базе данных:

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;
}

Так можно быть уверенным, что приложение не закроется, если возникнет какая-то ошибка.

Примечание


Обрабатывайте исключения везде, где они могут возникнуть.


Использование параметров

Параметры позволяют передавать данные отдельно от запроса. Это важно потому, что это один из лучших способов защитить базу от SQL-инъекции:

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;
}

Таким образом, база данных сначала получает запрос, а потом подставляет значения в места, где указаны параметры. То есть, даже если пользователь попробует ввести команду удаления таблицы, ничего не произойдет — команда будет воспринята как обычная строка. Поэтому в базе может появиться поле с таким значением:

password'); DELETE FROM table;

Использование процедур

Хранимые процедуры позволяют еще быстрее и безопаснее выполнять запросы. Их суть в том, что запросы хранятся на сервере, а из программы туда просто передаются параметры.

Создать хранимую процедуру можно в MS SQL Server Management Studio. Для этого нажмите правой кнопкой мыши на название базы —> Programmability —> Stored Procedures и контекстном меню выберите пункт Stored Procedure…:

В появившемся окне нужно ввести вот такой код:

CREATE PROCEDURE [dbo].[sp_Login] //Название процедуры
	@login nvarchar(200), //Получаемые параметры
	@password nvarchar(200)	
AS //Далее сам запрос
	SELECT * FROM Users WHERE login = @login AND password = @password
GO //Конец процедуры

Чтобы процедура добавилась, нажмите Execute. Вот как выглядит запрос с использованием хранимой процедуры:

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));

Теперь сервер сможет выполнить запрос быстрее, потому что ему нужно только получить входные данные. Кроме того, злоумышленник не узнает подробности работы с СУБД, если ему удастся получить исходный код приложения.

Шифрование и установка пароля

Всё вышеперечисленное защищает базу данных от неправильного ввода и появления ошибок. Но взломщик может попытаться получить доступ к ней напрямую. Если ему это удастся, то он сможет выполнять любые запросы.

Чтобы уменьшить шансы на взлом, можно установить на базу данных пароль. Для этого зайдите в MS SQL Management Studio и добавьте нового пользователя:

Добавление нового пользователя в MS SQL Management Studio

А затем установите для него пароль:

Установка пароля в MS SQL Management Studio

Другой важный и полезный способ защиты — шифрование. Оно позволяет преобразовать информацию так, чтобы данные нельзя было прочесть без ключа. Шифрование можно установить на базу целиком в меню создания или настроек базы — для этого нужно изменить значение Encryption Enabled:

Включение шифрования базы данных

Недостаток этого метода в том, что данные будут кодироваться и декодироваться при каждом запросе, что сильно скажется на производительности. Вместо этого лучше шифровать только отдельные столбцы, в которых содержатся:

  • пароли;
  • номера телефонов;
  • адреса почтовых ящиков;
  • физические и юридические адреса;
  • имена;
  • банковские реквизиты и другие конфиденциальные данные.

Узнать о реализации такой защиты можно в официальной документации Microsoft. Хотя это не лучший способ шифрования, он тоже очень эффективен, потому что значительно снижает риск потери данных. Эффективнее использовать программное кодирование по стандарту AES (англ. Advanced Encryption Standard — Расширенный стандарт шифрования), однако это сильно повлияет на производительность. Поэтому в большинстве случаев такие меры излишни.

Также в параметрах базы данных можно установить и другие полезные опции. Например, разрешить только чтение — тогда нельзя будет добавлять никакие новые сведения, изменять или удалять старые. Это позволит защитить информацию от несанкционированного удаления или изменения — сделать это не сможет даже администратор. Единственная возможность редактировать read only базу будет доступна в Management Studio.

Заключение

Несмотря на то что работа с базой данных может показаться простой, написать приложение, которое будет будет функционировать стабильно и без угрозы для хранимой информации, — сложная задача.

Конечно, это только часть методов. Также вы можете попробовать и другие варианты:

  • транзакции;
  • дополнительное шифрование;
  • передачу данных в бинарном виде — и всё, на что хватит вашей фантазии или бюджета.

Работа с базами данных входит в курс «Профессия C#-разработчик», в котором вы подробнее изучите ADO.NET, а также освоите LINQ to SQL и Entity Framework, которые помогут облегчить работу с СУБД.

Курс

Профессия C#-разработчик


130 часов — и вы научитесь писать программы на языке, созданном Microsoft. Вы создадите 5 проектов для портфолио, даже если до этого никогда не программировали. После обучения — гарантированное трудоустройство.

Хочешь получать крутые статьи по программированию?
Подпишись на рассылку Skillbox