<div><img src="https://top-fwz1.mail.ru/counter?id=3548135;js=na" style="position:absolute;left:-9999px;" alt="Top.Mail.Ru" /></div>
Публичное облако на базе VMware с управлением через vCloud Director
Вход / Регистрация

Как обеспечить безопасность веб-сайта: полное руководство по защите

Миша Курушин
Миша Курушин
Технический писатель
27 июня 2025 г.
7
26 минут чтения
Средний рейтинг статьи: 5

Виртуальный мир, как и реальный, — не самое безопасное место. Сайты в Интернете, подобно домам, подвержены взломам.

Их можно атаковать, из них можно вытягивать данные, в них можно вызывать сбои, ими можно манипулировать. А самое главное — пользователи зараженных сайтов могут стать разменной монетой в темных играх злоумышленников.

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

Именно о таких уязвимостях и пойдет речь в этой статье. Мы разберем самые распространенные угрозы, с которыми может столкнуться любой сайт в сети Интернет.

В дополнение к этому мы рассмотрим примеры небезопасных реализаций и способы устранения найденных уязвимостей — все, что необходимо для повышения безопасности веб-сайта.

В показанных примерах реализация backend-логики и организация безопасности сайта выполнена с помощью платформы Node.js в связке с библиотекой Express.js.

cloud

Инъекция кода (Code Injection)

Инъекция кода (Code Injection) — уязвимость, позволяющая внедрять на сайт вредоносный код извне и выполнять его как системный.

С помощью инъекций кода можно управлять удаленным хостом и выуживать приватные данные, которые на нем хранятся.

Для выполнения инъекции необходимо наличие уязвимого входного канала — места, через которое, подобно уколу с ядом, проникает вредоносный код. В протоколе HTTP есть несколько таких мест:

GET-запрос. Вредоносный код размещается в query-строке GET-запроса:

GET /products?category=books&sort=price_desc HTTP/1.1 <<< ЗДЕСЬ МОЖЕТ БЫТЬ ИНЪЕКЦИЯ
Host: example.ru
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
Accept: application/json

POST-запрос. Вредоносный код размещается в теле POST-запроса:

POST /auth/login HTTP/1.1
Host: 	example.ru
Content-Type: application/json
Content-Length: 58
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
Accept: application/json

{"username":"ivan","password":"P@ssw0rd!","rememberMe":true} <<< ЗДЕСЬ МОЖЕТ БЫТЬ ИНЪЕКЦИЯ

HTTP-заголовки. Вредоносный код размещается в одном из HTTP-заголовков:

GET /dashboard HTTP/1.1
Host: example.ru
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
Accept: text/html,application/xhtml+xml
Cookie: session_id=abc123xyz; theme=light; lang=ru <<< ЗДЕСЬ МОЖЕТ БЫТЬ ИНЪЕКЦИЯ

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

Тем не менее, инъекция кода — широкое понятие. Существует множество видов инъекций, но все они работают по одинаковому принципу.

SQL-инъекция

SQL-инъекция (SQL injection) — разновидность уязвимости инъекции кода, позволяющая выполнять произвольные SQL-запросы на стороне удаленного сервера.

С помощью таких запросов можно управлять таблицами базы данных: читать, изменять, удалять, создавать.

Проблема: сервер имеет небезопасный обработчик GET-запросов, который не валидирует полученные query-параметры и тем самым делает возможным выполнение SQL-инъекции:

...

app.get('/products', (req, res) => {
	const id = req.query.id; // чтение параметра из query-строки
	const sql = `SELECT * FROM products WHERE id = ${id}`; // прочитанный параметр подставляется в строку SQL-запроса без какой-либо проверки

	db.query(sql, (err, results) => { // место, где выполняется инъецированный код
		if (err) return res.status(500).send('Ошибка запроса');
		res.json(results);
	});
});

...

В этом случае злоумышленник может отправить к удаленному серверу GET-запрос, содержащий SQL-выражение в query-строке:

GET /products?id=10; DROP TABLE users;-- HTTP/1.1
Host: example.ru
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
Accept: text/html,application/xhtml+xml

То есть запрос выполняется по следующему адресу:

https://example.ru/products?id=10; DROP TABLE users;--

Таким образом, SQL-запрос, извлекающий данные:

SELECT * FROM products WHERE id = 10;

Превратится в SQL-запрос, извлекающий данные и удаляющий таблицу:

SELECT * FROM products WHERE id = 10; DROP TABLE users;--;

Подобно открытой форточке, злоумышленник может просунуть в это окошко любой необходимый ему инструмент, получив карт-бланш на управление базой данных на стороне удаленного сервера.

Решение: защититься от SQL-инъекций можно через многоуровневую валидацию данных:

  • Разделение кода и данных. Высокоуровневые библиотеки (JPA/Hibernate, Entity Framework, Django ORM) формируют слой ORM-абстракций (Object-Relational Mapping), которые позволяют работать с базами данных в представлении параметризованных объектов, а не прямых текстовых SQL-запросов. Вместо библиотек можно подготовить отдельные функции (или классы), генерирующие SQL-выражения на основе шаблонов и данных. Главное — не использовать конкатенацию строк для формирования SQL-запросов.
  • Валидация и фильтрация ввода. Каждый параметр проверяется на наличие допустимых значений с помощью регулярных выражений, числовых диапазонов, наборов разрешенных слов, ограничений длины и типов данных.
  • Контекстное экранирование. Замена определенных символов в данных, полученных от пользователя, на другие. Например, переменную из query-строки можно обернуть в одиночные кавычки, после чего она будет распознана SQL-движком не как опасная команда, а как безопасный строковый литерал.
  • Минимизация прав БД. Серверное приложение получает доступ к базе данных только с минимально необходимыми правами, а пользователь, от имени которого запускается сервер, вообще не имеет прав на системные папки и критические команды.
  • Автоматизированное тестирование. Инструменты автоматического анализа (SAST) и динамического сканирования (DAST) выявляют потенциальные инъекции еще до стадии продакшена, а пенетрационное тестирование (пентесты) симулирует реальные атаки для проверки применяемых мер защиты.

Следуя описанным мерам безопасности сайта, код, показанный ранее, можно преобразовать в более защищенную форму обработки пользовательских запросов:

...

app.get('/products', (req, res) => {
	// ФИЛЬТРАЦИЯ: преобразование параметра из query-строки в число (отбрасывание нечисловой части)
	const id = parseInt(req.query.id, 10);

	// ВАЛИДАЦИЯ: проверка корректности значения преобразованного параметра
	if (isNaN(id)) return res.status(400).send('Некорректный ID');

	// ПАРАМЕТРИЗАЦИЯ: использование шаблона SQL-запроса с плейсхолдером ? вместо прямой подстановки данных
	const sql = 'SELECT * FROM products WHERE id = ?';

	db.query(sql, [id], (err, results) => {
		if (err) return res.status(500).send('Ошибка запроса');
		res.json(results);
	});
});

...

Таким образом, комплексная валидация данных, полученных извне, исключает возможность выполнения SQL-инъекции и тем самым обеспечивает безопасность сайта в целом.

Cross-Site Scripting (XSS)

Межсайтовый скриптинг (Cross-Site Scripting, XSS) — разновидность уязвимости инъекции кода, позволяющая выполнять произвольный JavaScript-код в браузере другого пользователя.

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

В акрониме XSS используется X, чтобы не путаться с CSS — Cascading Style Sheets.

Есть несколько видов межсайтового скриптинга:

  • Reflected (Отраженный). Вредоносный код передается пользователю через специально сформированную ссылку и «отражается» сервером в ответе.
  • Stored (Хранимый). Вредоносный код сохраняется на стороне сервера и автоматически показывается всем посетителям.
  • DOM-base (DOM-модель). Вредоносный код внедряется в браузер пользователя за счет уязвимостей в клиентском JavaScript-коде и выполняется без прямого участия сервера.

Проблема Reflected: Сервер генерирует HTML-ответ на основе невалидированных пользовательских данных, тем самым делает возможным выполнение отраженного XSS:

...

app.get('/greet', (req, res) => {
	const name = req.query.name || 'Гость'; // чтение параметра из query-строки (если он отсутствует, ему присваивается значение по умолчанию)

	// прочитанный параметр вставляется в HTML-разметку
	const html = `
		<!DOCTYPE html>
		<html lang="ru">
			<head>
				<meta charset="UTF-8">
				<title>Приветствие</title>
			</head>
			<body>
				<h1>Привет, ${name}!</h1>
				<p>Как дела?</p>
			</body>
		</html>
	`;

	res.send(html);
});

...

В этом случае злоумышленник может отправить к удаленному серверу GET-запрос, который будет содержать в query-строке разметку с вредоносным кодом на JavaScript:

GET /greet?name=<script>alert('XSS')</script> HTTP/1.1
Host: example.ru
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
Accept: text/html,application/xhtml+xml

Проще говоря, запрос выполняется по следующему адресу:

http://example.ru/greet?name=<script>alert('XSS')</script>

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

В этом случае содержание открывшейся страницы примет следующий вид:

<!DOCTYPE html>
<html lang="ru">
	<head>
		<meta charset="UTF-8">
		<title>Приветствие</title>
	</head>
	<body>
		<h1>Привет, <script>alert('XSS')</script>!</h1>
		<p>Как дела?</p>
	</body>
</html>

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

Решение для Reflected: подобно SQL-инъекциям, для защиты от XSS необходимы методы валидации отображаемых данных:

  • Контекстная валидация данных. Параметры проверяются на соответствие допустимым значениям с помощью регулярных выражений, ограничений длины и типизированных преобразований. Возможно, это один из самых эффективных методов обеспечения безопасности веб-сайта.
  • Контекстное экранирование. Замена одних символов на другие для предотвращения исполнения вредоносного кода в неподходящем для него месте.
  • Безопасное API для работы с DOM. Использование innerText или textContent вместо innerHTML для трактовки вставляемых HTML-данных как текста, а не как кода. В случае с атрибутами задействование функции element.setAttribute('value', userValue) вместо явных конструкций <input value="${userValue}">.
  • Шаблонизаторы и фреймворки. Использование шаблонизаторов или фреймворков, в которых встроено автоматическое экранирование.

Используя эти методы, можно повысить безопасность показанного ранее кода, защитив его от выполнения отраженного XSS:

...

app.get('/greet', (req, res) => {
	const re = /^[A-Za-zА-Яа-я0-9\s\-_]{1,50}$/u; // регулярное выражение, разрешающее до 50 символов в виде латиницы, кириллицы, цифр, пробелов, дефисов и нижних подчеркиваний

	// ФИЛЬТРАЦИЯ: проверка параметра на предмет пустоты + обрезка пробелов по краям
	const rawName = (req.query.name || 'Гость').trim();

	// ВАЛИДАЦИЯ: проверка параметра по длине и содержанию
	if(rawName === '' || !re.test(rawName)) return res.status(400).send('Некорректное имя');

	// ЭКРАНИРОВАНИЕ: специальные символы <, >, &, " заменяются на HTML-коды
	const safeName = escapeHtml(rawName);

	// вставка параметра в HTML-разметку
	const html = `
		<!DOCTYPE html>
		<html lang="ru">
			<head>
				<meta charset="UTF-8">
				<title>Приветствие</title>
			</head>
			<body>
				<h1>Привет, ${safeName}!</h1>
				<p>Как дела?</p>
			</body>
		</html>
	`;

	res.send(html);
});

...

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

Однако без использования регулярного выражения экранирование превратило бы опасную строку:

<script>alert('XSS')</script>

В безопасный набор символов:

&lt;script&gt;alert(&#x27;XSS&#x27;)&lt;/script&gt;

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

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

...

const comments = []; // простое in-memory хранилище для комментариев (в реальном приложении это была бы база данных)

...

// сохранение комментария
app.post('/comments', (req, res) => {
	const userText = req.body.text || '';
	comments.push(userText);
});

...

// вывод комментариев
app.get('/comments', (req, res) => {
	let html = `
		<h1>Комментарии</h1>
		<ul>
	`;

	comments.forEach(comment => { html += `<li>${comment}</li>`; });
	html += `</ul>`;

	res.send(html);
});

...

В такой реализации есть два серьезных недостатка — отсутствие валидации во время добавления комментария и отсутствие валидации во время вывода добавленных комментариев.

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

В этом случае навыки социальной инженерии злоумышленнику не нужны совсем — посетители уязвимого сайта самостоятельно запустят вредоносный код.

Решение для Stored: чтобы убрать уязвимость XSS хранимого типа, необходимо добавить валидацию и фильтрацию пользовательских данных в тех местах, где они проходят через систему.

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

...

// функция HTML-экранирования
function escapeHTML(str) {
	if (typeof str !== 'string') return '';
	return str
		.replace(/&/g, '&amp;')
		.replace(/</g, '&lt;')
		.replace(/>/g, '&gt;')
		.replace(/"/g, '&quot;')
		.replace(/'/g, '&#x27;');
}

...

app.post('/comments', (req, res) => {
	const userText = req.body.text || '';
	if (userText !== '') comments.push(escapeHTML(userText)); // ЭКРАНИРОВАНИЕ
});

...

app.get('/comments', (req, res) => {
	let html = `
		<h1>Комментарии</h1>
		<ul>
	`;

	 comments.forEach((comment, index) => {
	 	 const safeComment = escapeHTML(comment); // ЭКРАНИРОВАНИЕ
	 	 html += `<li><strong>#${index + 1}:</strong> ${safeComment}</li>`;
	 });

	html += `</ul>`;

	res.send(html);
});

...

Теперь код скрипта, содержащийся в комментарии пользователя, превратится в безопасную форму еще на стадии сохранения в хранилище:

<li>&lt;script&gt;alert('XSS')&lt;/script&gt;</li>

Однако во время рендера браузер покажет исходную форму кода без его выполнения:

<script>alert('XSS')</script>

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

Привет! С помощью функции alert() в JavaScript можно показывать всплывающие сообщения:

<script>alert('Это всплывающее сообщение')</script>

То имеет смысл сохранить комментарий в исходном виде, а во время рендера выполнять фильтрацию.

Тем не менее, большей защиты от XSS можно добиться с помощью динамической генерации страниц, которая дает повышенный контроль над отображаемым контентом.

В этом случае сперва загружается базовая версия сайта (Single Page Application, SPA), а уже потом дополнительно загружается контент: новости, статьи, комментарии и т. п.

Во время вставки загруженных комментариев в отрисованную страницу вместо опасного innerHTML необходимо использовать безопасные textContent или innerText:

...

justText = "<script>alert('XSS')</script>";

document.getElementById('someBlock').innerHTML = justText; // ОПАСНО: скрипт будет выполнен
document.getElementById('someBlock').innerText = justText; // БЕЗОПАСНО: скрипт будет отображен
document.getElementById('someBlock').textContent = justText; // БЕЗОПАСНО: скрипт будет отображен

...

Таким образом код, явно сохраненный в тексте комментариев, будет показан в виде обычного текста без какого-либо выполнения.

Проблема DOM-base: межсайтовый скриптинг на уровне DOM отличается от межсайтового скриптинга с отражением только тем, что вставка вредоносного кода в разметку страницы выполняется без участия удаленного сервера — исключительно на локальном компьютере:

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>Пример DOM-XSS</title>
	<script>
		window.addEventListener('DOMContentLoaded', () => {
			const params = new URLSearchParams(window.location.search); // разбор query-строки на ключи и значения
			const message = params.get('msg'); // извлечение параметра
			document.getElementById('welcome').innerHTML = message;
		});
	</script>
</head>
<body>
	<div id="welcome"></div>
</body>
</html>

Злоумышленник может отправить жертве ссылку, содержащую вредоносный код в query-строке, — скрипт будет динамически вставлен в страницу и сразу же выполнен:

https://example.ru/greet?msg=<script>alert('XSS')</script>

Решение для DOM-base: для предотвращения XSS на уровне DOM-дерева необходимо придерживаться некоторых правил во время динамической генерации страницы:

  • Использование текстовой вставки. Вместо innerHTML применять textContent или innerText.
  • Исключение функций выполнения кода. Не использовать eval(), new Function(), setTimeout() и другие конструкции, напрямую выполняющие код.
  • Фильтрация и валидация данных. Проверять критические данные с помощью регулярных выражений.

Уязвимый код, показанный ранее, можно и нужно модифицировать:

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>Пример без DOM-XSS</title>
	<script>
		window.addEventListener('DOMContentLoaded', () => {
			const params = new URLSearchParams(window.location.search);
			const message = params.get('msg');

			const welcomeElement = document.getElementById('welcome');
			if (!welcomeElement || message === null) return; // дополнительная проверка на существование элемента

			// регулярное выражение, разрешающее русские и латинские буквы, цифры, пробелы и базовые знаки препинания
			const validPattern = /^[A-Za-zА-Яа-яЁё0-9\s.,!?'"()\-\:;]+$/;

			// ВАЛИДАЦИЯ: проверка сообщения на соответствие регулярному выражению
 			if (!validPattern.test(message)) {
				welcomeElement.textContent = 'Некорректный формат текста.';
				return;
			}

			// ЭКРАНИРОВАНИЕ: использование textContent вместо innerHTML побуждает браузер автоматически экранировать символы < и >, предотвращая выполнение скриптов
			welcomeElement.textContent = message;
		});
	</script>
</head>
<body>
	<div id="welcome"></div>
</body>
</html>

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

Command-инъекция

Command-инъекция (Command injection) — разновидность уязвимости инъекции кода, позволяющая выполнять произвольные команды в операционной системе удаленного сервера.

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

Проблема: как и в остальных видах инъекций, уязвимый код использует пользовательские данные для выполнения системных команд:

...

app.get('/list', (req, res) => {
	const filename = req.query.filename;

	if (!filename) return res.status(400).send('Нет параметра filename');

	const cmd = `ls /home/uploads/${filename}`;

	exec(cmd, (err, stdout, stderr) => {
		if (err) return res.status(500).send(`Ошибка выполнения команды`);
		res.send(`<pre>${stdout}</pre>`);
	});
});

...

Помимо имени файла, злоумышленник может передать дополнительные системные команды:

GET /greet?filename=test; rm -rf / HTTP/1.1
Host: example.ru
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
Accept: text/html,application/xhtml+xml

То есть адрес GET-запроса будет таким:

https://example.ru/greet?filename=test; rm -rf /

Дополнительная команда rm попытается удалить все каталоги корневой директории — то есть фактически всю систему целиком. Флаги -r и -f указывают на рекурсивное и принудительное удаление соответственно.

Решение: как и в остальных случаях, для предотвращения инъекции опасных команд необходима валидация пользовательских данных и более безопасный способ выполнения команд, в которых содержатся пользовательские данные:

  • Валидация имени файла. Регулярные выражения проверяют наличие пробелов, слешей и других спецсимволов, которые используются внедрения вредоносных команд.
  • Исполнение файла. Вместо функции exec используется execFile, которая передает аргументы напрямую без запуска через shell, исключая возможность выполнения произвольной команды.

Соответственно, код выше модифицируется способами, похожими на защиту от других видов инъекций:

const path = require('path');
const { execFile } = require('child_process');

app.get('/list', (req, res) => {
	const filename = req.query.filename;

	if (!filename) return res.status(400).send('Нет параметра filename');
	if (!/^[A-Za-z0-9._-]+$/.test(filename)) return res.status(400).send('Недопустимое имя файла'); //разрешаем только буквы, цифры, точки, дефисы и подчеркивания

	const fullPath = path.join('/home/uploads', filename); // формируем безопасный путь к файлу

	// execFile запускает ls без shell-интерпретации, передавая аргументы напрямую
	execFile('ls', [fullPath], (err, stdout, stderr) => {
		if (err) return res.status(500).send('Ошибка выполнения команды');
		res.send(`<pre>${stdout}</pre>`);
	});
});

На самом деле существует множество других видов инъекций — мы показали лишь самые распространенные. Инъецировать можно всё что угодно — от кода и разметки до команд и шаблонов.

Уязвимость такого рода возможна в любом месте, где пользовательские данные тем или иным способом влияют на системную логику серверного приложения.

Поэтому любая внешняя (по отношению к серверу) информация требует тщательной валидации перед использованием. В противном случае она способна превратиться в инструмент похищения данных и управления сервером.

Межсайтовая подделка запроса (Cross-Site Request Forgery, CSRF)

Межсайтовая подделка запроса (Cross-Site Request Forgery, CSRF) — уязвимость, позволяющая принудить пользователя выполнить нежелательный запрос сайту, где он уже авторизован.

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

  1. Когда пользователь заходит на определенные сайты (например, интернет-магазины, маркетплейсы или онлайн-банки), он авторизуется в личном кабинете и входит в свой профиль. В этот момент браузер сохраняет куки-файлы (cookies) на локальном компьютере, чтобы пользователь не вводил логин и пароль при каждом входе.
  2. Злоумышленник предлагает доверчивому пользователю перейти на произвольный сайт. На этом сайте может быть форма, которая либо автоматически (за счет скрипта), либо вручную (по клику пользователя) отправляет уязвимому сайту запрос на выполнение определенных действий, а браузер автоматически прикрепляет к этому запросу соответствующие куки.
  3. После обработки запроса, содержащего куки авторизации, уязвимый сайт выполняет те действия с профилем пользователя, которые от него требует злоумышленник. Пользователь же ничего не подозревает.

Как видно, CSRF не крадет что-либо (например, конфиденциальные данные) напрямую, а лишь выполняет определенные действия от имени пользователя. Однако, это не делает CSRF менее эффективным — такая уязвимость является самой распространенной.

Можно выделить несколько ключевых компонентов CSRF-атаки:

  • Требуемое действие. На уязвимом сайте есть возможность выполнения действия от имени пользователя, которое приведет к результату, нужному злоумышленнику.
  • Параметры запроса. Заранее известные параметры запроса, которые уязвимый сайт посчитает корректными для выполнения действия.
  • Cookies-файлы. Куки-файлы, без который действие не может быть выполнено — даже если все параметры запроса указаны верно.

Таким образом, чтобы уязвимость CSRF сработала, необходимо наличие всех перечисленных компонентов.

Проблема

...

// хранилище пользователей (вместо настоящей базы данных)
let users = {
	alice: {
		password: 'alicepass',
		shippingAddress: 'ул. Ленина, д. 1, кв. 10'
	},
	bob: {
		password: 'bobpass',
		shippingAddress: 'пр. Гагарина, д. 5, кв. 3'
	}
};

...

app.post('/updateAddress', (req, res) => {
	// проверка сессии (авторизации) пользователя по куки
	if (!req.session || !req.session.username) return res.status(401).send('Сначала авторизуйтесь');

	const user = req.session.username; // извлечение имени пользователя из сессии
	const newAddr = req.body.newAddress; // извлечение нового адреса доставки из тела POST-запроса

	if (!newAddr || newAddr.trim().length < 5) return res.status(400).send('Неправильный формат адреса'); // проверка корректности нового адреса доставки

	users[user].shippingAddress = newAddr.trim(); // обновление адреса доставки хранилище пользователей

	res.send(`
		<h2>Адрес доставки обновлен!</h2>
		<p>Новый адрес для ${user}: <strong>${users[user].shippingAddress}</strong></p>
	`);
});

...

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

Решение: существует целый комплекс мер, позволяющих исключить уязвимость CSRF:

  • CSRF-токенизация. Уникальная последовательность символов, которая отправляется сервером в момент инициации выполнения действия (например, во время отправки определенной формы). Именно этот токен должен вернуть пользователь во время выполнения действия.
  • Same-Site Cookies. Специальный флаг, который разрешает использование куки только в рамках конкретного доменного имени.
  • Подтверждение действий. Для некоторых действий имеет смысл внедрить механику дополнительного подтверждения. Например, нажимать кнопку, перемещать графический ползунок, вводить капчу. 

Во-первых, необходимо добавить генерацию CSRF-токена в той форме, через которую пользователь должен самостоятельно менять адрес доставки:

...

const crypto = require('crypto');

...

// В роуте, где вы отдаёте форму (например, GET /profile)
app.get('/profile', (req, res) => {
	if (!req.session || !req.session.username) return res.redirect('/login'); // проверка авторизации пользователя

	const token = crypto.randomBytes(24).toString('hex'); // генерация случайного CSRF-токен
	req.session.csrfToken = token; // сохранение сгенерированного CSRF-токена в сессии

	// внутри HTML-разметки с формой есть скрытое поле со сгенерированным CSRF-токеном
	res.send(`
		<h1>Профиль пользователя: ${req.session.username}</h1>
		<p>Текущий адрес доставки: ${users[req.session.username].shippingAddress}</p>

		<h2>Изменить адрес доставки</h2>
		<form action="/updateAddress" method="POST">
			<input type="hidden" name="csrfToken" value="${token}" />
      
			<label>Новый адрес доставки:</label><br>
			<input type="text" name="newAddress" value="${users[req.session.username].shippingAddress}" size="50" /><br><br>
			<button type="submit">Сохранить</button>
		</form>
	`);
});

...

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

...

app.post('/updateAddress', (req, res) => {
	if (!req.session || !req.session.username) return res.status(401).send('Сначала авторизуйтесь');

	const sentToken = req.body.csrfToken; // извлечение CSRF-токена из тела POST-запроса
	const sessionToken = req.session.csrfToken; // извлечение CSRF-токена из сессионного куки
	if (!sentToken || sentToken !== sessionToken) return res.status(403).send('Неверный CSRF-токен'); // валидация извлеченных CSRF-токенов

	const user = req.session.username;
	const newAddr = req.body.newAddress;

	if (!newAddr || newAddr.trim().length < 5) return res.status(400).send('Неправильный формат адреса');

	users[user].shippingAddress = newAddr.trim();

	res.send(`
		<h2>Адрес доставки обновлен!</h2>
		<p>Новый адрес для ${user}: <strong>${users[user].shippingAddress}</strong></p>
	`);
});

...

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

Broken Access Control

Нарушенный контроль доступа (Broken Access Control) — уязвимость, позволяющая выполнять запрещенные действия или получать приватные данные, обходя процесс проверки прав пользователя.

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

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

Проблема: чаще всего уязвимые сайты предоставляют небезопасную прямую ссылку на объект — IDOR (Insecure Direct Object Reference):

...

const documents = {
	'123': { id: '123', ownerId: 'user123', content: 'Мой личный текст' },
	'124': { id: '124', ownerId: 'user456', content: 'Чужой текст' },
};

...

app.get('/document/:id', (req, res) => {
	const docId = req.params.id;
	const document = documents[docId];

	if (!document) return res.status(404).send({ error: 'Документ не найден' });

	return res.json({
		id: document.id,
		content: document.content
	});
});

...

В такой реализации, во-первых, отсутствует проверка авторизации пользователя по кукам, а во-вторых — не учитываются права доступа к документу.

Решение

В код, показанный выше, необходимо добавить дополнительные проверки:

...

const sessions = {
	'550e8400-e29b-41d4-a716-446655440000': { id: 'user123', name: 'Иван' },
	'4d924a41-7e31-409a-8ecf-ea7f6f1e7f83': { id: 'user456', name: 'Мария' }
};

...

app.get('/document/:id', (req, res) => {
	const sessionId = req.cookies.session_id; // извлечение идентификатора сессии
	const user = sessions[sessionId]; // поиск активной сессии
	if (!user) return res.status(401).send({ error: 'Неавторизованный пользователь' }); // проверка наличия активной сессии

	const docId = req.params.id;
	const document = documents[docId];

	if (!document) return res.status(404).send({ error: 'Документ не найден' });

	if (document.ownerId !== user.id) return res.status(403).send({ error: 'Доступ к документу запрещен' }); // проверка прав доступа

	return res.json({
		id: document.id,
		content: document.content
	});
});

...

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

  • Многоуровневые проверки. По критическим маршрутам и эндпоинтам выпполняется проверка аутентификации и прав пользователя.
  • Запрет по умолчанию (Deny by Default). Открытый доступ есть только к тем ресурсам, к которым он действительно необходим.
  • Минимизация прав (Least Privilege). У каждого пользователя есть только те роли и права, которые ему действительно необходимы. Необоснованные широкие права не предоставляются.

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

Подготовили для вас выгодные тарифы на облачные серверы

Заключение

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

Например, организация OWASP (Open Worldwide Application Security Project), включающая в себя множество корпораций, образовательных организаций и частных лиц со всего мира, в исследовании за 2021 год сообщает, что до 19% приложений в сети Интернет подвержены различным видам инъекции кода.

В другом отчете, подготовленном сервисом поиска уязвимостей Acunetix в 2020 году, 8% сайтов в Интернете были уязвимы к SQL-инъекциям, 25% — к Cross-Site Scripting (XSS) и 36% — к Cross-Site Request Forgery (CSRF).

Image1

Резюме отчета безопасности Acunetix за 2020 год

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

27 июня 2025 г.
7
26 минут чтения
Средний рейтинг статьи: 5

Читайте также

Хотите внести свой вклад?
Участвуйте в нашей контент-программе за
вознаграждение или запросите нужную вам инструкцию
img-server
Пока нет комментариев