Инъекции и разграничение доступа — Примеры
Пример 1 — Кавычка, которая меняет оператор
При ревью поиска заказов ниже обнаруживается запрос, собранный соединением строк, где присланный пользователем идентификатор подставлен прямо в оператор. Перепишите его так, чтобы ввод путешествовал как данные, и назовите символ, который нужен атаке, чтобы добраться до базы.
// orders.ts — как на ревью
app.get("/orders/:id", (req, res) => {
const rows = db.query(
`SELECT * FROM orders WHERE id = '${req.params.id}'`
);
res.json(rows);
});
Решение
// orders.ts — после исправления
app.get("/orders/:id", (req, res) => {
const rows = db.query(
"SELECT * FROM orders WHERE id = $1",
[req.params.id]
);
res.json(rows);
});
Теперь значение идёт отдельным параметром и не может изменить структуру оператора. Для инъекции нужна одиночная кавычка, чтобы закрыть строковый литерал и начать писать SQL; как связанный параметр этот символ — просто данные. Обычное половинчатое исправление — параметризовать идентификатор и оставить ту же конкатенацию ниже для столбца сортировки, где плейсхолдер привязки стоять не может.
Пример 2 — Чужой счёт
Эндпоинт детали ниже добавлен после страницы списка, которая фильтрует по вошедшему аккаунту. Он загружает любой пришедший идентификатор. Имея действительную сессию и id, принадлежащий другому аккаунту, напишите проверку, закрывающую дыру, и выберите ответ осознанно.
GET /invoices/1042
session.accountId = 7
invoice 1042.accountId = 9
-> 200 OK, тело: счёт другого аккаунта
Решение
GET /invoices/1042
invoice = invoices.find(req.params.id)
if (!invoice || invoice.accountId !== session.accountId)
-> 404 Not Found
Авторизовать маршрут — не то же самое, что авторизовать объект, поэтому проверка выполняется при каждой загрузке относительно аккаунта из сессии, а не присланного клиентом id. Возвращается «не найдено», а не «запрещено», потому что признание существования счёта 1042 — само по себе раскрытие. Обычная ошибка — довериться фильтру на странице списка и решить, что страница детали его наследует; это не так.