Виртуальный мир, как и реальный, — не самое безопасное место. Сайты в Интернете, подобно домам, подвержены взломам.
Их можно атаковать, из них можно вытягивать данные, в них можно вызывать сбои, ими можно манипулировать. А самое главное — пользователи зараженных сайтов могут стать разменной монетой в темных играх злоумышленников.
Персональные данные и денежные средства могут быть украдены посредством множества ухищрений, эксплуатирующих многочисленные уязвимости сайтов.
Именно о таких уязвимостях и пойдет речь в этой статье. Мы разберем самые распространенные угрозы, с которыми может столкнуться любой сайт в сети Интернет.
В дополнение к этому мы рассмотрим примеры небезопасных реализаций и способы устранения найденных уязвимостей — все, что необходимо для повышения безопасности веб-сайта.
В показанных примерах реализация backend-логики и организация безопасности сайта выполнена с помощью платформы Node.js в связке с библиотекой Express.js.
cloud
Инъекция кода (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 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-инъекций можно через многоуровневую валидацию данных:
Следуя описанным мерам безопасности сайта, код, показанный ранее, можно преобразовать в более защищенную форму обработки пользовательских запросов:
...
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) — разновидность уязвимости инъекции кода, позволяющая выполнять произвольный JavaScript-код в браузере другого пользователя.
Такой код может перехватывать сессии, подменять содержимое страниц, перенаправлять на фишинговые сайты — варианты зависят от фантазии злоумышленника.
В акрониме XSS используется X, чтобы не путаться с CSS — Cascading Style Sheets.
Есть несколько видов межсайтового скриптинга:
Проблема 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 необходимы методы валидации отображаемых данных:
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>
В безопасный набор символов:
<script>alert('XSS')</script>
Таким образом запуск потенциально вредоносного кода в браузере пользователя был бы полностью предотвращен.
Проблема 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, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
...
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><script>alert('XSS')</script></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 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
указывают на рекурсивное и принудительное удаление соответственно.
Решение: как и в остальных случаях, для предотвращения инъекции опасных команд необходима валидация пользовательских данных и более безопасный способ выполнения команд, в которых содержатся пользовательские данные:
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) — уязвимость, позволяющая принудить пользователя выполнить нежелательный запрос сайту, где он уже авторизован.
Алгоритм работы уязвимости выглядит примерно так:
Как видно, CSRF не крадет что-либо (например, конфиденциальные данные) напрямую, а лишь выполняет определенные действия от имени пользователя. Однако, это не делает CSRF менее эффективным — такая уязвимость является самой распространенной.
Можно выделить несколько ключевых компонентов CSRF-атаки:
Таким образом, чтобы уязвимость 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-токена в той форме, через которую пользователь должен самостоятельно менять адрес доставки:
...
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) — уязвимость, позволяющая выполнять запрещенные действия или получать приватные данные, обходя процесс проверки прав пользователя.
Уязвимости, связанные с контролем доступа, не настолько явны и формальны, как любые другие уязвимости. Они больше выглядят как недоработки, нежели как серьезные архитектурные недостатки — своего рода «опечатки» в логике приложения.
Зачастую в тех местах, где очевидна проверка прав доступа, она по невнимательности разработчика не выполняется. Таким образом появляется уязвимость.
Проблема: чаще всего уязвимые сайты предоставляют небезопасную прямую ссылку на объект — 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
});
});
...
Чтобы не допускать возникновения уязвимости нарушенного контроля доступа, необходимо придерживаться некоторых правил при проектировании логики серверного приложения:
Говоря проще, важно не создавать переизбыток горизонтальных и вертикальных привилегий, а также проверять пользовательские права во всех критических местах приложения — везде, где пользователь читает данные и управляет функциями.
Подготовили для вас выгодные тарифы на облачные серверы
Многим может показаться, что описанные уязвимости и показанные примеры уже не актуальны в мире современных приложений, протоколов, фреймворков и библиотек. Однако это не так.
Например, организация OWASP (Open Worldwide Application Security Project), включающая в себя множество корпораций, образовательных организаций и частных лиц со всего мира, в исследовании за 2021 год сообщает, что до 19% приложений в сети Интернет подвержены различным видам инъекции кода.
В другом отчете, подготовленном сервисом поиска уязвимостей Acunetix в 2020 году, 8% сайтов в Интернете были уязвимы к SQL-инъекциям, 25% — к Cross-Site Scripting (XSS) и 36% — к Cross-Site Request Forgery (CSRF).
Резюме отчета безопасности Acunetix за 2020 год
Настройка безопасности сайта по большей части заключается в выборе допустимых и недопустимых пользовательских данных, отправляемых как в сторону сервера, так и в сторону пользователя. Фильтрация и валидация — главный ключ к безопасности.