Обход аутентификации
admin' OR '1'='1Попытка обойти вход, делая условие WHERE всегда истинным
Пользовательские значения никогда не попадают в SQL-текст — они привязываются через плейсхолдеры параметров (зависит от диалекта, например $1 или ?)
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] может ошибочно дать доступ администратора
Каждый запрос проходит через несколько уровней проверки безопасности
Все пользовательские значения преобразуются в параметры. Значения не включаются в SQL-текст как литералы.
Каждое имя поля проверяется по метаданным схемы Prisma перед генерацией запроса.
Имена таблиц, столбцов и псевдонимов валидируются и безопасно квотируются/экранируются при необходимости.
Для каждого типа поля разрешены только известные безопасные операторы.
Строгий порядок гарантирует, что каждый плейсхолдер соответствует ровно одному параметру в порядке добавления, что исключает несовпадения.
Наброски доказательств устойчивости к инъекциям при допущениях дизайна (привязка параметров, отсутствие raw-SQL обхода, корректная работа драйвера)
Для всех предоставленных пользователем значений 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}`
}Множество имён полей 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)
}Для любого оператора 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')
}Все 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
}Для каждой позиции плейсхолдера 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()Текущий набор проверяет защиту от распространённых векторов SQL-инъекций и крайних случаев
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 точно отражает структуру вашей базы данных.
TypeScript даёт безопасность на этапе компиляции, но валидация во время выполнения добавляет защиту в глубину.
Логируйте и отслеживайте сгенерированный SQL и параметры в продакшене для обнаружения аномалий и неправильного использования.
Защита от SQL-инъекций встроена в каждый уровень этой библиотеки. Просмотрите тесты безопасности и детали реализации напрямую.