Предотвращение SQL-инъекций
Пользовательские значения никогда не попадают в SQL-текст — они привязываются через плейсхолдеры параметров (зависит от диалекта, например $1 или ?)
Понимание SQL-инъекций
SQL-инъекции остаются одной из самых критических уязвимостей веб-приложений
SQL-инъекция возникает, когда недоверенные данные включаются в SQL-текст без безопасной привязки параметров. Атакующий может изменить смысл запроса, получить доступ к неавторизованным данным, изменить записи или инициировать нежелательные операции.
Распространённые шаблоны атак
Обход аутентификации
admin' OR '1'='1 Попытка обойти вход, делая условие WHERE всегда истинным
Экфильтрация данных
' UNION SELECT password FROM users-- Попытка извлечь данные из других таблиц через UNION
Разрушительные команды
'; DROP TABLE users; -- Попытка добавить дополнительные операторы для удаления данных
Злоупотребление ошибкой порядка параметров
Эксплуатация: если плейсхолдеры и параметры перепутаны, проверка прав может использовать неверные значения Пример: WHERE userId=$1 AND isAdmin=$2, но params=[true, 123] вместо [123, true] может ошибочно дать доступ администратора
Многоуровневая стратегия защиты
Каждый запрос проходит через несколько уровней проверки безопасности
Уровень 1: Автоматическая привязка параметров
Все пользовательские значения преобразуются в параметры. Значения не включаются в SQL-текст как литералы.
- Пользовательские значения представлены плейсхолдерами, зависящими от диалекта (например $1 или ?)
- Массивы параметризуются (расширением плейсхолдеров или как массив-параметры — зависит от диалекта/стратегии)
- NULL и опциональные значения обрабатываются без конкатенации пользовательского ввода в SQL-текст
- Значения даты/времени параметризуются
- JSON-значения параметризуются
Уровень 2: Валидация имён полей
Каждое имя поля проверяется по метаданным Prisma schema перед генерацией запроса.
- Разрешены только поля, определённые в схеме
- Фильтры отношений валидируются по метаданным схемы
- Произвольные имена полей не принимаются
- Неподдерживаемые/вычисляемые поля отклоняются там, где применимо
- Payload прототипного загрязнения отклоняется (например __proto__, constructor)
Уровень 3: Санитизация идентификаторов
Имена таблиц, столбцов и псевдонимов валидируются и безопасно квотируются/экранируются при необходимости.
- Управляющие символы отклоняются
- Зарезервированные ключевые слова квотируются при необходимости
- Квалификация по схеме поддерживается при соответствующей настройке
- Экранирование двойных кавычек (или эквивалент диалекта) для идентификаторов
- Ограничение длины идентификатора соблюдается по правилам диалекта
Уровень 4: Валидация операторов
Для каждого типа поля разрешены только известные безопасные операторы.
- Строковые операторы: contains, startsWith, endsWith
- Числовые операторы: lt, lte, gt, gte
- Операторы массивов: in, notIn с проверкой типов
- Логические операторы: AND, OR, NOT валидируются структурно
- Неизвестные операторы отклоняются немедленно
Уровень 5: Гарантия порядка параметров
Строгий порядок гарантирует соответствие каждого плейсхолдера своему параметру без возможности несовпадения.
- Последовательный счётчик предотвращает переупорядочивание
- Синхронное построение SQL-текста и массива параметров
- Нет промежуточного буфера или операций переупорядочивания
- Проверка тестами: SQL и параметры всегда согласованы
- Соответствие один-к-одному: позиция N → params[N-1] (или эквивалентное отображение для диалекта)
Формальные гарантии безопасности
Наброски доказательств устойчивости к инъекциям при допущениях дизайна (привязка параметров, отсутствие raw-SQL обхода, корректная работа драйвера)
Теорема 1: Изоляция значений
Для всех предоставленных пользователем значений V не существует пути выполнения, при котором V появляется как SQL-текст в сгенерированной строке запроса.
По построению, все пути кода, обрабатывающие значения пользователя, вызывают addParameter(value), который: 1. Сохраняет значение в отдельном массиве params 2. Возвращает токен плейсхолдера для SQL-текста 3. В SQL-строку добавляется только токен плейсхолдера 4. Драйвер БД получает SQL-текст и значения параметров раздельно ∴ Пользовательские значения не парсятся как SQL-синтаксис
const addParameter = (params: any[], value: any) => {
params.push(value)
const index = params.length
return `$${index}`
} Теорема 2: Замкнутость имён полей
Множество имён полей F в любом сгенерированном запросе является подмножеством полей, определённых в схеме S, т.е. F ⊆ S.
Для каждой ссылки на поле: 1. Имя поля извлекается из объекта запроса 2. Выполняется проверка по метаданным схемы 3. Если поля нет в схеме, выбрасывается ошибка 4. Только валидированные поля попадают в генерацию SQL ∴ Произвольные имена полей не могут появиться в SQL
const validateField = (field: string, model: Model) => {
if (!model.fields.has(field)) {
throw new Error(`Field ${field} does not exist`)
}
return model.fields.get(field)
} Теорема 3: Безопасность операторов
Для любого оператора O, применённого к полю F, O входит в множество разрешённых операторов для типа поля T.
Алгоритм валидации оператора: 1. Извлечь тип поля T из схемы 2. Определить allowed_ops(T) = { валидные операторы для типа T } 3. Для оператора O в запросе: - Если O ∉ allowed_ops(T), выбросить ошибку - Иначе применить оператор с привязкой параметров ∴ Можно использовать только операторы, подходящие типу
const ALLOWED_OPS: Record<string, string[]> = {
String: ['contains', 'startsWith', 'endsWith'],
Int: ['lt', 'lte', 'gt', 'gte']
}
if (!ALLOWED_OPS[fieldType]?.includes(operator)) {
throw new Error('Invalid operator')
} Теорема 4: Безопасность идентификаторов
Все SQL-идентификаторы I валидируются, чтобы исключить управляющие символы и обеспечить безопасное квотирование/экранирование по правилам диалекта.
Обработка идентификатора: 1. Отклонить, если есть управляющие символы 2. Экранировать внутренние кавычки по правилам диалекта 3. Квотировать идентификатор при необходимости (ключевые слова или спецсимволы) 4. Применять ограничения длины по правилам диалекта ∴ Идентификатор не может сломать синтаксис SQL или внедрить токены
const quoteIdentifier = (id: string) => {
if (/[\x00-\x1F]/.test(id)) {
throw new Error('Invalid characters')
}
const escaped = id.replace(/"/g, '""')
const needsQuoting = /[^a-z0-9_]/i.test(id) || isReservedKeyword(id)
return needsQuoting ? `"${escaped}"` : id
} Теорема 5: Согласованность порядка параметров
Для каждой позиции плейсхолдера N, появившейся в SQL-тексте, params[N-1] содержит точное значение для этой позиции без возможности переупорядочивания или несовпадения.
Гарантия порядка параметров: 1. Один трекер поддерживает порядок вставки 2. Каждый add() добавляет значение в params и сразу возвращает следующий плейсхолдер 3. SQL-текст и массив params строятся синхронно 4. Переупорядочивание не происходит ∴ Соответствие один-к-одному сохраняется
class ParameterTracker {
private params: any[] = []
add(value: any): string {
this.params.push(value)
return `$${this.params.length}`
}
getParams(): any[] {
return this.params
}
}
const tracker = new ParameterTracker()
const sql = `WHERE email = ${tracker.add(email)} AND age > ${tracker.add(age)}`
const params = tracker.getParams() Полное покрытие тестами безопасности
137 тестов проверяют защиту от распространённых векторов SQL-инъекций и крайних случаев (текущий набор)
Параметризация значений (basic.test.ts)
- Строки с кавычками: user'with'quotes
- Строки с точками с запятой: user;extra
- SQL-ключевые слова как значения: DROP TABLE users
- Сложная инъекция: '; DROP TABLE users; --
- Union-атаки: ' UNION SELECT * FROM users--
- Boolean-атаки: admin' OR '1'='1
- Инъекция комментариев: test@example.com' -- comment
Валидация имён полей (identifiers.test.ts)
- Отклонение вредоносных имён полей в SELECT
- Отклонение вредоносных имён полей в WHERE
- Отклонение вредоносных имён полей в ORDER BY
- Отклонение SQL-инъекций в именах полей
- Отклонение несуществующих полей
- Отклонение прототипного загрязнения: __proto__, constructor
Безопасность LIKE-шаблонов (like-patterns.test.ts)
- Инъекция подстановочных символов: %' OR '1'='1
- Инъекция подчёркивания: test_' OR '1'='1
- Обработка обратного слеша: test\\'; DROP--
- Множественные подстановки: %_%'; DROP--
- Инъекция без учёта регистра: '; UNION SELECT--
Безопасность операторов массивов (array-operators.test.ts)
- IN с вредоносными массивами: ['; DROP--', 'UNION SELECT--']
- NOT IN с инъекцией: ['; TRUNCATE--', 'DELETE FROM--']
- Обработка пустых массивов
- Валидация больших массивов (100+ элементов)
- Обработка массивов смешанных типов
Крайние случаи (edge-cases.test.ts)
- Unicode-инъекция: \u0027 OR \u00271\u0027=\u00271
- Hex-инъекция: 0x31=0x31--
- URL-кодирование: %27%3B%20DROP%20TABLE
- Составные запросы: '; DROP TABLE users; SELECT
- Инъекции по времени (зависит от диалекта): WAITFOR DELAY '00:00:05'--
- UNION-инъекция: UNION ALL SELECT null, password
- Попытки вторичной инъекции
Проверка порядка параметров (basic.test.ts)
- Последовательные позиции плейсхолдеров
- Массив параметров соответствует порядку плейсхолдеров
- Сложные запросы сохраняют порядок между условиями
- Вложенные условия OR/AND сохраняют последовательность параметров
- Фильтры отношений сохраняют правильное соответствие параметров
Безопасный против небезопасного: сравнение кода
❌ Небезопасно: конкатенация строк
const email = req.body.email
const sql = "SELECT * FROM users WHERE email = '" + email + "'" ✅ Безопасно: автоматическая привязка параметров
const email = req.body.email
const { sql, params } = toSQL('User', 'findMany', {
where: { email }
})
const result = { sql, params } Детали реализации
Управление параметрами
Централизованное отслеживание параметров гарантирует привязку каждого пользовательского значения и стабильный порядок
class ParameterTracker {
private params: any[] = []
add(value: any): string {
this.params.push(value)
return `$${this.params.length}`
}
getParams(): any[] {
return this.params
}
} Валидация полей
Валидация на основе схемы предотвращает произвольный доступ к полям
function validateField(
fieldName: string,
model: ModelInfo
): FieldInfo {
const field = model.fields.get(fieldName)
if (!field) {
throw new Error(
`Field "${fieldName}" does not exist`
)
}
return field
} Квотирование идентификаторов
Валидация и квотирование идентификаторов с учётом диалекта предотвращает синтаксическую инъекцию через идентификаторы
function quoteIdentifier(identifier: string): string {
if (/[\x00-\x1F]/.test(identifier)) {
throw new Error('Invalid control characters')
}
const escaped = identifier.replace(/"/g, '""')
const needsQuoting = /[^a-z0-9_]/i.test(identifier) || isReservedKeyword(identifier)
if (needsQuoting) {
return `"${escaped}"`
}
return identifier
} Лучшие практики безопасности
Никогда не отключайте валидацию
Не обходите валидацию схемы или проверку полей. Это критически важные уровни безопасности.
Держите схему актуальной
Убедитесь, что Prisma schema точно отражает структуру вашей базы данных.
Валидируйте типы входных данных
TypeScript даёт безопасность на этапе компиляции, но валидация во время выполнения добавляет защиту в глубину.
Отслеживайте шаблоны запросов
Логируйте и отслеживайте сгенерированный SQL и параметры в продакшене для обнаружения аномалий и неправильного использования.
Безопасность через дизайн
Защита от SQL-инъекций встроена в каждый уровень этой библиотеки. Просмотрите тесты безопасности и детали реализации напрямую.