Раніше я вважав, що написання власного рівня автентифікації — це знак почесті. Якщо ти розумієшся на JWT та вмієш хешувати паролі, то що тут складного? Я налаштував Node backend, видавав токени і вважав справу зробленою. Код працював. Тести проходили. Але потім я почав читати про те, як насправді відбуваються реальні атаки, і підлога пішла з-під ніг. Кожен розділ про ін'єкції, перерахування (enumeration) та витоки через побічні канали змушував мене повертатися до редагування коду з важким серцем. У моїй системі автентифікації були не просто прогалини; у ній були чотири розчинені двері, які я встановив власноруч. Ось що я виявив і що саме я змінив.
SQL-ін'єкція через конкатенацію рядків
Перша помилка була найстарішим трюком у світі. Я брав ввід користувача і вставляв його прямо в SQL-рядки. У моєму маршруті логіну я брав email із тіла запиту та конкатенував його в запит на кшталт SELECT * FROM users WHERE email = '${email}'. Це здавалося безпечним, оскільки я контролював фронтенд. Це небезпечне припущення. Зловмиснику не потрібен ваш фронтенд. Один спеціально сформований POST-запит міг перетворити цю перевірку входу на витік даних або повне видалення бази даних. Надішліть такий payload, як ' OR '1'='1 або, що ще гірше, стекований запит, який видаляє таблиці, — і якщо рядок виконується сирим, ваші дані зникнуть. Я не дав базі даних жодного способу відрізнити мій код від даних зловмисника.
Виправленням було не посилення валідації вводу чи ручне екранування рядків. Справжнім рішенням стали параметризовані запити. Я перейшов на node-postgres і почав використовувати плейсхолдери на кшталт $1. Запит стає шаблоном: SELECT * FROM users WHERE email = $1. Драйвер надсилає SQL-запит і значення через окремі канали. База даних сприймає ввід суворо як дані, незалежно від того, які символи він містить. Ця одна зміна закриває весь клас ін'єкційних атак. Це простіше читати, легше підтримувати, і це позбавляє необхідності бути майстром регулярних виразів щоразу, коли ви пишете умову WHERE.
Перерахування email через повідомлення про помилки
Моя друга помилка виглядала як хороший UX. Коли користувач вводив неправильний email, я повертав User not found. Коли email був правильним, а пароль — ні, я повертав Incorrect password. Це здавалося корисним. Але це також стало інструментом для розвідки зловмисниками. Скрипти для перерахування можуть буквально засипати ваш endpoint для входу тисячами адрес електронної пошти. Якщо тіло відповіді або статус-код змінюються залежно від того, чи існує обліковий запис, скрипт може скласти верифікований список ваших користувачів. Цей список стає основою для credential stuffing, цілеспрямованого фішингу та подальших спроб brute-force.
Мені довелося прийняти той факт, що зручність для користувача іноді має поступатися безпеці. Я змінив усі шляхи невдалого входу так, щоб вони повертали один і той самий рядок: Invalid credentials. Жодних підказок. Жодної розгалуженої логіки у відповіді на помилку. Незалежно від того, чи не вказано email, чи пароль невірний, чи обліковий запис заблоковано, текст залишається ідентичним. Це також стосується процесів реєстрації та скидання пароля: не розкривайте, чи є ця адреса вже у вашій системі. Одне загальне повідомлення усуває витік інформації, на який покладаються зловмисники.
Атаки за часом під час порівняння паролів
Третя помилка була невидимою. Я...
