Организация интерфейса с пользователем: контроль

1. Проблема доверия: почему нельзя верить интерфейсу?

Иллюзия клиентской безопасности

JavaScript в браузере исполняется на устройстве пользователя. Любой злоумышленник или тестировщик может:

  • Отключить JS в настройках браузера за 2 секунды;
  • Удалить атрибут required через DevTools (F12);
  • Отправить сырой HTTP POST запрос через curl или Postman.

Принцип «Zero Trust» (Нулевое доверие)

Интерфейс и база данных решают принципиально разные задачи:

  • Frontend: делает взаимодействие комфортным, подсказывает формат и предотвращает глупые опечатки.
  • СУБД: гарантирует физическую целостность и сохранность данных при любых атаках и сбоях.

2. Двухуровневая модель эшелонированного контроля

Уровень 1: Frontend (Клиентский UX)

  • Мгновенный отклик (0 мс): валидация на лету при наборе символов (события input, blur);
  • Маски ввода: подсказки форматов телефонов, счетов и дат;
  • Блокировка кнопки: деактивация кнопки «Отправить», пока форма заполнена с ошибками.

Уровень 2: Backend & СУБД (Ядро данных)

  • Параметризованные запросы: барьер против внедрения SQL-инъекций;
  • Ограничения СУБД: CHECK, UNIQUE, NOT NULL, FOREIGN KEY, EXCLUDE;
  • Триггеры бизнес-логики: проверка складских остатков и балансов в транзакциях.

3. Регулярные выражения: правила составления масок

Ключевое правило безопасности регулярных выражений — строгое ограничение начала и конца строки:

// КАТАСТРОФА: без якорей /^...$/
const weakPattern = /\d{10}/;
weakPattern.test("мусор 1234567890 грязь"); // Вернет TRUE!

// ЭТАЛОН: строгие границы начала (^) и конца ($)
const strictPattern = /^\d{10}$/;
strictPattern.test("1234567890");          // TRUE (строго 10 цифр)
strictPattern.test("мусор 1234567890");    // FALSE (отклонено)

Популярные маски: ИНН юрлица: /^\d{10}$/ | БИК банка: /^04\d{7}$/ | ОГРН: /^\d{13}$/ | Email: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/

4. Клиентская валидация контрольных сумм (ИНН-10)

Зачем нужна проверка контрольного разряда?

Случайная опечатка в одной цифре ИНН пройдет проверку по количеству знаков ^\d{10}$, но приведет к созданию некорректного контрагента.

10-я цифра ИНН вычисляется математически по весовым коэффициентам: [2, 4, 10, 3, 5, 9, 4, 6, 8].

function checkInn10(inn) {
  if (!/^\d{10}$/.test(inn)) return false;
  const w = [2, 4, 10, 3, 5, 9, 4, 6, 8];
  let sum = 0;
  for (let i = 0; i < 9; i++) {
    sum += parseInt(inn[i]) * w[i];
  }
  return (sum % 11) % 10 === parseInt(inn[9]);
}

5. Анатомия катастрофы: инъекция SQL-кода

Что происходит, когда программист соединяет строки запроса через конкатенацию:

// Опасный код бэкенда:
String sql = "SELECT * FROM users WHERE login = '" + req.get("login") + "' AND pass = '" + pass + "'";

// Если злоумышленник введет в поле логина:  admin' --
// Запрос превращается в:
SELECT * FROM users WHERE login = 'admin' --' AND pass = '...'
// Символы -- превращают весь остаток запроса в комментарий! Вход без пароля выполнен!
Вывод: Никогда, ни при каких обстоятельствах не собирайте SQL-команду из текстовых строк пользователя!

6. Абсолютная защита: Prepared Statements

1. Фаза PREPARE (Разбор запроса)

СУБД парсит SQL-текст с плейсхолдерами $1, $2, строит дерево синтаксического разбора (AST) и компилирует план. На этом шаге структура запроса зафиксирована навсегда.

2. Фаза EXECUTE (Передача данных)

Параметры передаются по бинарному протоколу отдельно. Любые кавычки или спецсимволы обрабатываются ядром как литеральный текст. Внедрить код невозможно математически!

-- Протокол PostgreSQL Prepared Statements:
PREPARE auth_stmt (text, text) AS
  SELECT id, full_name FROM users WHERE login = $1 AND pass_hash = $2;

EXECUTE auth_stmt('admin' OR 1=1 --', 'hash123'); -- Ищет буквального юзера с таким именем!

7. Серверный контроль: ограничения CHECK и NOT NULL

Ограничения в СУБД действуют как последняя линия обороны, которую невозможно обойти:

CREATE TABLE bank_accounts (
  account_id BIGSERIAL PRIMARY KEY,
  account_number CHAR(20) NOT NULL,
  bik CHAR(9) NOT NULL,
  current_balance NUMERIC(15,2) NOT NULL DEFAULT 0.00,
  is_active BOOLEAN NOT NULL DEFAULT TRUE,
  -- Серверные предикаты целостности:
  CONSTRAINT chk_account_format CHECK (account_number ~ '^\d{20}$'),
  CONSTRAINT chk_bik_format CHECK (bik ~ '^04\d{7}$'),
  CONSTRAINT chk_non_negative_balance CHECK (current_balance >= 0.00)
);
Даже если на бэкенде произойдет программный баг, попытка списать баланс ниже нуля вызовет ошибку СУБД check_violation.

8. Триггеры бизнес-логики: проверка остатков перед записью

Когда условия зависят от данных других таблиц, декларативного CHECK недостаточно:

CREATE OR REPLACE FUNCTION trg_verify_stock() RETURNS TRIGGER AS $$
BEGIN
  -- Блокируем складской остаток от конкурентных изменений
  IF (SELECT stock_qty FROM products WHERE id = NEW.product_id FOR UPDATE) < NEW.quantity THEN
    RAISE EXCEPTION 'Товар отсутствует на складе в требуемом количестве!'
      USING ERRCODE = 'check_violation';
  END IF;
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER check_stock_before_order
BEFORE INSERT ON order_items
FOR EACH ROW EXECUTE FUNCTION trg_verify_stock();

9. Аномалия Race Condition и блокировки транзакций

Проблема Time-of-Check to Time-of-Use

Пользователь А и Пользователь Б одновременно оформляют последний билет на рейс. Оба видят «свободно 1 место». Без транзакционной защиты оба выполнят вставку, вызвав овербукинг!

Решения на уровне СУБД

  • SELECT ... FOR UPDATE: пессимистическая построчная блокировка записи на время транзакции;
  • EXCLUDE USING gist: аппаратный запрет на пересечение временных интервалов брони;
  • Изоляция SERIALIZABLE: автоматический откат конфликтующей транзакции (SQLSTATE 40001).

10. Стандартизация ошибок: коды SQLSTATE

Международный стандарт ISO/IEC 9075 кодирует все ошибки СУБД пятизначными шифрами:

Класс 23 — Нарушение целостности

  • 23505: unique_violation (дубликат ключа)
  • 23514: check_violation (нарушен CHECK)
  • 23503: foreign_key_violation (нет родителя)
  • 23P01: exclusion_violation (пересечение)

Классы 40 и P0 — Транзакции и триггеры

  • 40001: serialization_failure (гонка транзакций)
  • 40P01: deadlock_detected (взаимная блокировка)
  • P0001: raise_exception (пользовательский триггер)

11. Архитектурный паттерн Error Mapping

Трансляция технического исключения СУБД в безопасное экранное уведомление:

Ужасный UX и брешь в ИБ (Сырой вывод)

ERROR: duplicate key value violates 
unique constraint "clients_inn_key"
DETAIL: Key (inn)=(7701234567) already exists.

Раскрывает злоумышленнику схему БД, имена полей и таблиц.

Профессиональный дружественный маппинг

switch (err.code) {
  case '23505':
    return "Организация с данным ИНН уже зарегистрирована в системе.";
  case '23P01':
    return "Выбранный номер уже занят на эти даты.";
}

Понятно пользователю, не раскрывает технических деталей.

12. Дифференцированные сценарии практикума

Вариант 1: Регистрация юрлица

Валидация ИНН (10 цифр + контрольная сумма) и ОГРН. Перехват 23505 duplicate key при повторной регистрации.

Вариант 2: Банковский перевод

Проверка счета/БИК, клиентский запрет отрицательных сумм. Перехват серверной ошибки недостатка средств.

Вариант 3: Оформление заказа

Маски телефона и email. Перехват ошибки триггера P0001 out of stock при параллельной покупке последнего товара.

Вариант 4: Бронирование отеля

Клиентский контроль Дата_выезда > Дата_заезда. Перехват 23P01 exclusion_violation наложение интервалов.

13. Инженерный манифест надежного интерфейса

Пять правил разработчика

  1. Никогда не верь клиенту — дублируй проверки в СУБД.
  2. Всегда используй Prepared Statements — забудь конкатенацию SQL.
  3. Всегда ставь якоря ^ и $ в регулярных выражениях.
  4. Блокируй конкурентные строки через FOR UPDATE или EXCLUDE.
  5. Маппируй коды SQLSTATE в человеческие подсказки.

Результат внедрения

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

14. Классика веб-безопасности: Little Bobby Tables

xkcd 327: Exploits of a Mom
Тематический интернет-мем: «Exploits of a Mom» (культовый веб-комикс про «Little Bobby Tables», xkcd №327).
Автор: Randall Munroe. Первоисточник: xkcd.com/327. Лицензия: Creative Commons Attribution-NonCommercial 2.5 Generic (CC BY-NC 2.5).
Связь с темой занятия: Мама назвала сына Robert'); DROP TABLE Students; --. Когда школа вставила имя ученика через конкатенацию SQL-строки, база данных стерла всех студентов года. Занятие 24 учит будущих разработчиков: использование параметризованных запросов (Prepared Statements) и строгий контроль входных данных в интерфейсе навсегда исключают подобные инциденты!
1 / 15