JS / Безопасность
Проверка данных регулярными выражениями в JavaScript: практические шаблоны и ограничения
Как валидировать коды, имена и телефоны через RegExp, когда выбрать парсер и почему клиентская проверка не заменяет серверную.
Содержание статьи
- Закрепляйте проверку на всей строке
- Проверка имени пользователя
- Артикул с именованными группами
- Email: не пытайтесь воспроизвести весь стандарт
- Телефон: сначала нормализация, затем проверка
- URL лучше разбирать через URL
- Дата: формат и календарь — разные проверки
- Динамический шаблон
- Опасность катастрофического backtracking
- Состояние флагов g и y
- Что проверить тестами
- Что запомнить
Регулярное выражение хорошо проверяет форму строки: допустимые символы, длину, префикс или разделители. Оно не подтверждает, что email существует, дата реальна, а пользователь имеет право выполнить действие.
Надёжная валидация обычно состоит из нескольких шагов:
- убедиться, что получено значение ожидаемого типа;
- нормализовать его только по понятным правилам;
- проверить длину и формат;
- проверить бизнес-ограничения;
- повторить все проверки на сервере.
Закрепляйте проверку на всей строке
const codePattern = /^WEB-\d{4}$/;console.log(codePattern.test('WEB-2048')); // trueconsole.log(codePattern.test('XWEB-2048Y')); // falseБез ^ и $ шаблон нашёл бы подходящий фрагмент внутри неверной строки.
Проверка имени пользователя
Допустим, продукт разрешает буквы любых языков, цифры, _ и -, а длина должна быть от 3 до 24 символов:
const usernamePattern = /^[\p{L}\p{N}_-]{3,24}$/u;function validateUsername(value) { if (typeof value !== 'string') { return 'Имя должно быть строкой.'; } const username = value.trim(); if (!usernamePattern.test(username)) { return 'Используйте 3–24 буквы, цифры, дефис или подчёркивание.'; } return null;}Флаг u нужен для Unicode property escapes. Решение о допустимых символах — продуктовый allowlist, а не универсальное правило для всех сайтов.
Проверка уникальности имени выполняется только на сервере и должна быть защищена уникальным ограничением базы.
Артикул с именованными группами
const skuPattern = /^(?<category>[A-Z]{3})-(?<year>20\d{2})-(?<number>\d{5})$/;function parseSku(value) { const match = value.match(skuPattern); if (!match?.groups) { return null; } return { category: match.groups.category, year: Number(match.groups.year), number: match.groups.number, };}Regex одновременно проверяет структуру и извлекает части. Допустимые категории и годы после этого проверяются отдельным allowlist и диапазоном.
Email: не пытайтесь воспроизвести весь стандарт
Слишком строгий самодельный шаблон отклоняет действительные адреса, а очень сложный всё равно не доказывает существование ящика. На клиенте начните с HTML:
<label for="email">Email</label><input id="email" name="email" type="email" maxlength="254" autocomplete="email" required>Если JavaScript нужен для немедленной обратной связи, используйте встроенную проверку:
const email = document.querySelector('#email');email?.addEventListener('input', () => { email.setCustomValidity(''); if (email.validity.typeMismatch) { email.setCustomValidity('Введите email в формате name@example.com.'); }});Сервер проверяет адрес своим валидатором, нормализует доменную часть согласно требованиям продукта и отправляет письмо подтверждения. Единственный надёжный способ проверить владение адресом — подтверждение по одноразовой ссылке.
Телефон: сначала нормализация, затем проверка
Форматы телефонов зависят от страны и бизнес-требований. Для российского номера можно принять распространённые разделители, удалить их и привести ведущую 8 к коду страны:
function normalizeRussianPhone(value) { if (typeof value !== 'string') { return null; } const digits = value.replace(/\D/g, ''); const normalized = digits.startsWith('8') && digits.length === 11 ? `7${digits.slice(1)}` : digits; return /^7\d{10}$/.test(normalized) ? `+${normalized}` : null;}console.log(normalizeRussianPhone('+7 (999) 123-45-67')); // '+79991234567'Это продуктовый пример, а не международный валидатор. Для нескольких стран понадобится библиотека или серверный сервис с актуальными метаданными нумерации.
URL лучше разбирать через URL
function parseHttpUrl(value) { try { const url = new URL(value); if (!['http:', 'https:'].includes(url.protocol)) { return null; } return url; } catch { return null; }}Regex для полного URL быстро становится хрупким. Даже синтаксически правильный URL нельзя автоматически считать безопасным: при серверной загрузке дополнительно защищайтесь от SSRF, запрещайте внутренние адреса и следуйте allowlist.
Дата: формат и календарь — разные проверки
const datePattern = /^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})$/;function isCalendarDate(value) { const match = value.match(datePattern); if (!match?.groups) { return false; } const year = Number(match.groups.year); const month = Number(match.groups.month); const day = Number(match.groups.day); const date = new Date(Date.UTC(year, month - 1, day)); return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;}Одного \d{4}-\d{2}-\d{2} недостаточно: оно принимает 2026-99-99.
Динамический шаблон
Если нужно искать буквальный пользовательский текст, экранируйте метасимволы:
function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');}const query = 'file.js';const pattern = new RegExp(escapeRegExp(query), 'i');console.log(pattern.test('Open FILE.JS')); // trueНе позволяйте недоверенной строке становиться произвольным regex. Сложный шаблон способен потреблять много процессорного времени.
Опасность катастрофического backtracking
Выражения с вложенными неоднозначными квантификаторами могут работать экспоненциально:
// Опасная форма для длинной строки без завершающего X.const riskyPattern = /^(a+)+X$/;Снижайте неоднозначность, ограничивайте длину входа и тестируйте почти подходящие строки. Регулярное выражение из внешнего источника нельзя выполнять без строгого контроля. Для публичного сервера это может стать причиной отказа в обслуживании.
Состояние флагов g и y
Объект RegExp с g или y меняет lastIndex при test():
const pattern = /^WEB/g;console.log(pattern.test('WEB-1')); // trueconsole.log(pattern.test('WEB-1')); // falseДля обычной валидации не используйте эти флаги. Они нужны для последовательного поиска, а не ответа «вся строка подходит или нет».
Что проверить тестами
Для каждого правила подготовьте:
- минимальную и максимальную допустимую длину;
- пустую строку,
null, массив и число; - символы разных алфавитов;
- значение на один символ длиннее лимита;
- почти совпадающую длинную строку;
- пробелы по краям и внутри;
- недопустимый протокол или диапазон;
- нормальный рабочий пример.
Что запомнить
Regex проверяет синтаксическую форму, а не истинность данных. Закрепляйте шаблон, ограничивайте вход, учитывайте Unicode и не используйте g для валидации. Email, URL, даты и телефоны требуют дополнительных механизмов, а клиентская проверка всегда повторяется на сервере.