🔒 Безопасность на первом месте

Предотвращение SQL-инъекций

Пользовательские значения никогда не попадают в SQL-текст — они привязываются через плейсхолдеры параметров (зависит от диалекта, например $1 или ?)

100%
Привязка параметров
Все пользовательские значения используют плейсхолдеры
0
Интерполяция значений
Данные пользователя не появляются в SQL-тексте
Гарантия
Сохранение порядка
Плейсхолдеры соответствуют параметрам 1:1
137
Тестов безопасности
Покрытие попыток инъекций (текущий набор)

Понимание 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
}

Лучшие практики безопасности

Никогда не отключайте валидацию

Не обходите валидацию схемы или проверку полей. Это критически важные уровни безопасности.

Do: Используйте библиотеку по дизайну с полной валидацией
Don't: Обходить валидацию схемы или вставлять raw SQL в SQL-текст

Держите схему актуальной

Убедитесь, что Prisma schema точно отражает структуру вашей базы данных.

Do: Запускайте prisma db pull и prisma generate после изменений схемы
Don't: Использовать устаревшие DMMF или определения схемы

Валидируйте типы входных данных

TypeScript даёт безопасность на этапе компиляции, но валидация во время выполнения добавляет защиту в глубину.

Do: Используйте сгенерированные типы Prisma для аргументов запросов
Don't: Приводить пользовательский ввод к any перед передачей в запросы

Отслеживайте шаблоны запросов

Логируйте и отслеживайте сгенерированный SQL и параметры в продакшене для обнаружения аномалий и неправильного использования.

Do: Включите логирование запросов и анализируйте шаблоны
Don't: Запускать в продакшене без мониторинга

Безопасность через дизайн

Защита от SQL-инъекций встроена в каждый уровень этой библиотеки. Просмотрите тесты безопасности и детали реализации напрямую.