В современных информационных системах пользовательский интерфейс отделен от СУБД сетевым уровнем. Любые данные, вводимые человеком через формы на веб-странице или в мобильном приложении, проходят многоступенчатую цепочку проверок. В промышленной архитектуре реализуется принцип «Defense in Depth» (эшелонированная оборона):
Распределение ответственности между уровнями архитектуры представлено в сравнительной таблице:
| Характеристика | Клиентский контроль (Frontend) | Серверный контроль (Backend / СУБД) |
|---|---|---|
| Среда выполнения | Браузер пользователя (V8, JavaScript / TypeScript, HTML5 Constraint API). | Сервер приложений (Node.js, Python, Java, Go) и ядро реляционной СУБД (PostgreSQL). |
| Главная цель | Мгновенный отклик интерфейса, снижение холостого трафика, подсказка правильного формата. | Гарантия целостности данных (ACID), исключение инъекций, обработка конкурентных транзакций. |
| Скорость отклика | 0–5 миллисекунд (выполняется локально в оперативной памяти клиентского устройства). | 15–150 миллисекунд (задержка сети, чтение блокировок, обращение к дисковым индексам). |
| Степень доверия | Нулевая. Код клиента может быть отключен, переписан через консоль разработчика или обойден запросом через curl. | Абсолютная. Ядро СУБД физически отменяет транзакцию и не допускает некорректную запись на диск. |
| Инструменты реализации | HTML5-атрибуты (required, pattern), события input, blur, маски полей, RegExp в JS. |
Ограничения CHECK, UNIQUE, FOREIGN KEY, триггеры BEFORE INSERT/UPDATE, транзакции SELECT FOR UPDATE. |
| Типичные сценарии | Проверка формата email, проверка длины и контрольной суммы ИНН, проверка Дата_выезда > Дата_заезда. | Проверка уникальности ИНН среди миллионов клиентов, проверка остатка на счете, блокировка слота времени. |
Регулярные выражения (Regular Expressions, RegExp) являются стандартом де-факто для проверки формата текстовых данных в формах ввода.
Самая распространенная ошибка разработчиков — отсутствие якорей начала строки (^) и конца строки ($). Без якорей регулярное выражение ищет любое совпадение в подстроке, что делает валидацию фиктивной:
// НЕПРАВИЛЬНО (без якорей):
const badRegex = /\d{10}/;
badRegex.test("Мой ИНН 1234567890 и еще мусор"); // Вернет TRUE! (пропустит грязь в БД)
// ПРАВИЛЬНО (строго от начала до конца):
const goodRegex = /^\d{10}$/;
goodRegex.test("1234567890"); // TRUE (ровно 10 цифр)
goodRegex.test("Мой ИНН 1234567890"); // FALSE (отклонено)
| Поле экранной формы | Спецификация формата | Регулярное выражение (RegExp) |
|---|---|---|
| ИНН юридического лица | Строго 10 арабских цифр | ^\d{10}$ |
| ИНН индивидуального предпринимателя | Строго 12 арабских цифр | ^\d{12}$ |
| ОГРН юридического лица | Строго 13 арабских цифр | ^\d{13}$ |
| БИК Банка РФ | Строго 9 арабских цифр (начинается с 04) | ^04\d{7}$ |
| Банковский счет РФ (расчетный/корр.) | Строго 20 цифр (начинается с 407, 408, 301 и др.) | ^\d{20}$ |
| Мобильный телефон РФ | Формат с кодом страны +7 и 10 цифрами номера | ^\+7\d{10}$ или с маской ^\+7 \(\d{3}\) \d{3}-\d{2}-\d{2}$ |
| Адрес электронной почты (Email) | Имя пользователя, символ @, домен и зона от 2 букв | ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ |
Одной лишь проверки регулярным выражением ^\d{10}$ недостаточно, так как 10-й знак ИНН юридического лица является контрольным разрядом, вычисляемым по специальным весовым коэффициентам:
// Алгоритм проверки контрольного разряда ИНН 10 знаков:
function validateInn10(inn) {
if (!/^\d{10}$/.test(inn)) return false;
const weights = [2, 4, 10, 3, 5, 9, 4, 6, 8];
let sum = 0;
for (let i = 0; i < 9; i++) {
sum += parseInt(inn[i], 10) * weights[i];
}
const checkDigit = (sum % 11) % 10;
return checkDigit === parseInt(inn[9], 10);
}
SQL-инъекция возникает в том случае, когда непроверенный ввод пользователя напрямую конкатенируется в тело SQL-запроса, в результате чего интерпретатор СУБД воспринимает данные как управляющие команды.
"SELECT * FROM accounts WHERE login = '" + login + "'", злоумышленник вводит admin' OR '1'='1.SELECT * FROM accounts WHERE login = 'admin' OR '1'='1', что приводит к обходу аутентификации и компрометации системы.
Почему параметризация дает 100% защиту?
Параметризованный запрос разделяет выполнение на два независимых этапа:
$1, $2) в бинарное дерево синтаксического разбора (AST — Abstract Syntax Tree). Структура запроса жестко фиксируется в памяти.' ; -- DROP TABLE, они не могут выйти за рамки строкового значения и не могут быть выполнены как код!-- Как параметризация выглядит на уровне протокола PostgreSQL:
-- Шаг 1: Подготовка запроса с типами параметров
PREPARE find_user (text, text) AS
SELECT id, email, full_name FROM users WHERE login = $1 AND password_hash = $2;
-- Шаг 2: Безопасное выполнение с любыми специальными символами
EXECUTE find_user('admin\' OR 1=1 --', 'secret_hash_value');
-- Запрос будет искать пользователя с буквальным логином "admin' OR 1=1 --" и не вернет лишних данных!
Некоторые бизнес-правила невозможно выразить статическим предикатом CHECK (например, проверка наличия товара на складе перед созданием заказа или списание баланса с лицевого счета). Для таких задач в СУБД применяются серверные триггеры BEFORE INSERT OR UPDATE и транзакционные блокировки.
CREATE OR REPLACE FUNCTION check_stock_availability()
RETURNS TRIGGER AS $$
DECLARE
current_stock INT;
BEGIN
-- Блокируем строку товара для предотвращения параллельного изменения остатка
SELECT stock_quantity INTO current_stock
FROM warehouse_products
WHERE product_id = NEW.product_id
FOR UPDATE;
IF current_stock < NEW.quantity THEN
RAISE EXCEPTION 'Недостаточно товара на складе: запрошено %, в наличии %',
NEW.quantity, current_stock
USING ERRCODE = 'check_violation';
END IF;
-- Автоматически уменьшаем складской остаток
UPDATE warehouse_products
SET stock_quantity = stock_quantity - NEW.quantity
WHERE product_id = NEW.product_id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_check_stock
BEFORE INSERT ON order_items
FOR EACH ROW
EXECUTE FUNCTION check_stock_availability();
Начинающие разработчики часто пишут в коде бэкенда:
// Антипаттерн "Проверка перед вставкой":
if (db.query("SELECT COUNT(*) FROM bookings WHERE room_id = 1 AND date = '2026-10-15'") == 0) {
db.query("INSERT INTO bookings (room_id, date) VALUES (1, '2026-10-15')");
}
Если два пользователя нажмут кнопку одновременно, оба параллельных потока выполнят SELECT, оба получат результат 0, после чего оба выполнят INSERT. В результате в базе образуется овербукинг (двойное бронирование)!
Решение на уровне СУБД: создание уникального индекса UNIQUE (room_id, booking_date) или ограничения исключения EXCLUDE USING gist (room_id WITH =, booking_range WITH &&). В этом случае СУБД аппаратно заблокирует вторую транзакцию и вернет ошибку 23505 или 23P01.
В случае отказа транзакции на стороне СУБД сервер приложений обязан перехватить исключение, извлечь код SQLSTATE и сопоставить его со словарем дружественных экранных сообщений.
SQLSTATE, constraint, table, null pointer или стектрейсы. Сообщение должно четко объяснять:
Если на веб-форме написан совершенный JavaScript-валидатор, проверяющий формат и контрольную сумму всех полей, можно ли отказаться от ограничений CHECK и NOT NULL в схеме базы данных?
Почему параметризованный запрос делает невозможной SQL-инъекцию, даже если входная строка пользователя содержит кавычки и опасные инструкции ; DROP TABLE users; --?
Какую уязвимость порождает регулярное выражение /\d{10}/ по сравнению с /^\d{10}$/ при проверке ИНН?
Какую опасность несет вывод сырого сообщения ERROR: duplicate key value violates unique constraint "clients_inn_key" прямо на экран пользователя и как его правильно обработать?