- Аутентификация и авторизация
- Криптография
- Инъекции
- Безопасность баз данных
- Валидация данных
- Утечки информации
- Атаки на доступность
Что искать:
- Отсутствие проверки временных меток (timestamp)
- Отсутствие nonce (одноразовых токенов)
- Бесконечное время жизни токенов
Пример уязвимости:
// ❌ УЯЗВИМО - нет проверки свежести данных
fn validate_telegram_data(raw: &str, hash: &str, bot_token: &str) -> bool {
let computed_hash = compute_hmac(raw, bot_token);
computed_hash == hash // Хэш правильный, но данные могут быть старыми!
}Как эксплуатируется:
- Злоумышленник перехватывает валидный запрос (например, через MITM)
- Сохраняет данные аутентификации
- Воспроизводит их позже для получения доступа
Правильное решение:
// ✅ БЕЗОПАСНО
fn validate_telegram_data(
raw: &str,
hash: &str,
bot_token: &str,
max_age: u64
) -> bool {
let parsed = parse_data(raw);
// 1. Проверяем свежесть данных
if let Some(auth_date) = parsed.get("auth_date") {
let timestamp = auth_date.parse::<u64>()?;
let now = current_timestamp();
if now - timestamp > max_age {
return false; // Данные устарели
}
} else {
return false; // Нет временной метки
}
// 2. Проверяем подпись
let computed_hash = compute_hmac(raw, bot_token);
computed_hash == hash
}Где искать:
- Аутентификация по токенам
- API endpoints с подписями
- OAuth flows
- Webhook handlers
Ключевые слова для поиска:
rg "validate|auth|verify" --type rust
rg "timestamp|auth_date|created_at" --type rustЧто искать:
// ❌ УЯЗВИМО
fn decode_jwt(token: &str) -> Result<Claims> {
jsonwebtoken::decode(
token,
&key,
&Validation::default() // Не проверяет exp!
)
}Правильно:
// ✅ БЕЗОПАСНО
fn decode_jwt(token: &str) -> Result<Claims> {
let mut validation = Validation::default();
validation.validate_exp = true; // Проверяем expiration
validation.leeway = 60; // 60 секунд погрешности
jsonwebtoken::decode(token, &key, &validation)
}Что искать:
- Endpoints аутентификации без ограничений
- Возможность brute-force атак
Пример уязвимости:
// ❌ УЯЗВИМО - можно перебирать пароли
async fn login(username: &str, password: &str) -> Result<Token> {
let user = db.find_user(username).await?;
if verify_password(password, &user.password_hash) {
Ok(generate_token(user.id))
} else {
Err(Error::InvalidCredentials)
}
}Правильно:
// ✅ БЕЗОПАСНО
async fn login(username: &str, password: &str, ip: IpAddr) -> Result<Token> {
// 1. Проверяем rate limit
if !rate_limiter.check(ip, 5, Duration::from_secs(60)) {
return Err(Error::TooManyRequests);
}
let user = db.find_user(username).await?;
// 2. Используем constant-time comparison
if verify_password(password, &user.password_hash) {
Ok(generate_token(user.id))
} else {
// 3. Учитываем попытку
rate_limiter.increment(ip);
// 4. Добавляем задержку перед ответом (против timing attacks)
tokio::time::sleep(Duration::from_millis(500)).await;
Err(Error::InvalidCredentials)
}
}Что искать:
rg "md5|sha1|DES|RC4" --type rust # Слабые алгоритмыПримеры:
// ❌ ПЛОХО - MD5 криптографически сломан
use md5::Md5;
let hash = Md5::digest(password);
// ❌ ПЛОХО - SHA1 тоже
use sha1::Sha1;
let hash = Sha1::digest(token);
// ✅ ХОРОШО - используй современные алгоритмы
use sha2::Sha256;
let hash = Sha256::digest(data);
// ✅ ЕЩЁ ЛУЧШЕ - для паролей используй специализированные функции
use argon2::{Argon2, PasswordHasher};
let hash = Argon2::default().hash_password(password, &salt)?;Что искать:
# Поиск потенциальных секретов
rg "password.*=.*\"" --type rust
rg "secret.*=.*\"" --type rust
rg "api_key.*=.*\"" --type rust
rg "token.*=.*\"" --type rust
rg "private_key" --type rustПример уязвимости:
// ❌ КРИТИЧЕСКАЯ УЯЗВИМОСТЬ
const API_KEY: &str = "sk_live_51H..."; // Никогда!
const DB_PASSWORD: &str = "admin123"; // Катастрофа!
fn encrypt(data: &[u8]) -> Vec<u8> {
let key = b"mysecretkey12345"; // ❌ Хардкод ключа
aes_encrypt(data, key)
}Правильно:
// ✅ ПРАВИЛЬНО - из переменных окружения
fn get_api_key() -> Result<String> {
std::env::var("API_KEY")
.map_err(|_| Error::MissingApiKey)
}
fn encrypt(data: &[u8]) -> Result<Vec<u8>> {
// Ключ из безопасного хранилища
let key = get_encryption_key()?;
aes_encrypt(data, &key)
}Проблема из нашего кода:
// ❌ БЫЛО - может вызвать panic
let mut mac = HmacSha256::new_from_slice(&secret)
.expect("HMAC initialization failed");Почему плохо:
expect()может вызвать panic- В production panic = DOS атака
Правильно:
// ✅ СТАЛО
let mac = HmacSha256::new_from_slice(&secret).ok()?;Что искать:
rg "format!.*SELECT|query.*format!" --type rust
rg "execute.*&format!" --type rustПример уязвимости:
// ❌ SQL INJECTION!
async fn get_user(username: &str) -> Result<User> {
let query = format!("SELECT * FROM users WHERE username = '{}'", username);
// ^^^^^^^^
// Если username = "admin' OR '1'='1", то получим всех пользователей!
db.execute(&query).await
}Правильно:
// ✅ БЕЗОПАСНО - используем параметризованные запросы
async fn get_user(username: &str) -> Result<User> {
sqlx::query_as!(
User,
"SELECT * FROM users WHERE username = $1",
username // Автоматически экранируется
)
.fetch_one(&db)
.await
}Что искать:
rg "Command::new|std::process::Command" --type rustПример уязвимости:
// ❌ COMMAND INJECTION!
fn resize_image(filename: &str) -> Result<()> {
let cmd = format!("convert {} -resize 100x100 output.jpg", filename);
// ^^^^^^^^
// filename = "input.jpg; rm -rf /" приведет к удалению файлов!
std::process::Command::new("sh")
.arg("-c")
.arg(&cmd)
.output()?;
Ok(())
}Правильно:
// ✅ БЕЗОПАСНО
fn resize_image(filename: &str) -> Result<()> {
// 1. Валидируем имя файла
if !is_valid_filename(filename) {
return Err(Error::InvalidFilename);
}
// 2. Не используем shell - передаем аргументы напрямую
std::process::Command::new("convert")
.arg(filename) // Безопасно - не интерпретируется shell'ом
.arg("-resize")
.arg("100x100")
.arg("output.jpg")
.output()?;
Ok(())
}
fn is_valid_filename(name: &str) -> bool {
// Только буквы, цифры, точки, дефисы
name.chars().all(|c| c.is_alphanumeric() || c == '.' || c == '-')
}Автоматический анализ: Используйте sql-query-analyzer для статического анализа SQL-запросов с 18 встроенными правилами безопасности и производительности.
Установка:
cargo install sql-query-analyzerЛокальное использование:
sql-query-analyzer analyze -s schema.sql -q queries.sql --format textИнтеграция с GitHub Actions:
- name: SQL Query Analysis
uses: RAprogramm/sql-query-analyzer@v1
with:
schema: db/schema.sql
queries: db/queries.sql
fail-on-error: 'true'
post-comment: 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}Критичность: Error
Что искать:
rg "UPDATE.*SET" --type rust
rg "UPDATE.*SET" --type sqlПример уязвимости:
-- ❌ ОПАСНО: затрагивает ВСЕ строки таблицы
UPDATE users SET status = 'inactive';Правильно:
-- ✅ БЕЗОПАСНО: явное условие
UPDATE users SET status = 'inactive' WHERE last_login < '2024-01-01';Критичность: Error
Что искать:
rg "DELETE FROM" --type rust
rg "DELETE FROM" --type sqlПример уязвимости:
-- ❌ ОПАСНО: удаляет ВСЕ строки
DELETE FROM sessions;Правильно:
-- ✅ БЕЗОПАСНО: явное условие
DELETE FROM sessions WHERE expired_at < NOW();Критичность: Warning
Влияние на безопасность: Может вызвать DOS через исчерпание памяти
Пример уязвимости:
-- ❌ МОЖЕТ ВЫЗВАТЬ OOM на больших таблицах
SELECT * FROM logs;Правильно:
-- ✅ БЕЗОПАСНО: ограниченный набор результатов
SELECT * FROM logs LIMIT 1000;
-- ИЛИ: конкретные колонки
SELECT id, message, created_at FROM logs LIMIT 1000;Критичность: Warning
Влияние на безопасность: Предотвращает использование индекса, позволяет DOS через медленные запросы
Пример уязвимости:
-- ❌ МЕДЛЕННО: полное сканирование таблицы, индекс не используется
SELECT * FROM users WHERE email LIKE '%@gmail.com';Правильно:
-- ✅ ЛУЧШЕ: используйте колонку суффикса или полнотекстовый поиск
SELECT * FROM users WHERE email_domain = 'gmail.com';Критичность: Warning
Влияние на безопасность: Деградация производительности, вектор DOS
Пример уязвимости:
-- ❌ МЕДЛЕННО: база должна просканировать и пропустить 100000 строк
SELECT * FROM products ORDER BY id LIMIT 10 OFFSET 100000;Правильно:
-- ✅ БЫСТРО: keyset пагинация
SELECT * FROM products WHERE id > 100000 ORDER BY id LIMIT 10;Критичность: Error
Влияние на безопасность: Экспоненциальный размер результата, исчерпание памяти
Пример уязвимости:
-- ❌ ОПАСНО: возвращает rows_a * rows_b результатов
SELECT * FROM users, orders;Правильно:
-- ✅ БЕЗОПАСНО: явное условие соединения
SELECT * FROM users
JOIN orders ON users.id = orders.user_id;Критичность: Warning
Влияние на безопасность: Паттерн N+1 запросов, перегрузка базы данных
Пример уязвимости:
-- ❌ МЕДЛЕННО: подзапрос выполняется для КАЖДОЙ строки
SELECT
id,
name,
(SELECT COUNT(*) FROM orders WHERE user_id = users.id) as order_count
FROM users;Правильно:
-- ✅ БЫСТРО: один запрос с JOIN
SELECT u.id, u.name, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name;Критичность: Warning
Влияние на безопасность: Обход индекса, медленные запросы
Пример уязвимости:
-- ❌ МЕДЛЕННО: UPPER() предотвращает использование индекса
SELECT * FROM users WHERE UPPER(email) = 'TEST@EXAMPLE.COM';Правильно:
-- ✅ БЫСТРО: храните нормализованные данные или используйте функциональный индекс
SELECT * FROM users WHERE email_lower = 'test@example.com';Критичность: Warning
Влияние на безопасность: Неожиданное поведение с NULL, проблемы производительности
Пример уязвимости:
-- ❌ ОПАСНО: возвращает пустой результат если подзапрос содержит NULL
SELECT * FROM users WHERE id NOT IN (SELECT user_id FROM banned);Правильно:
-- ✅ БЕЗОПАСНО: корректно обрабатывает NULL
SELECT * FROM users u
WHERE NOT EXISTS (SELECT 1 FROM banned b WHERE b.user_id = u.id);Критичность: Info
Влияние на безопасность: Полное сканирование таблицы
Пример уязвимости:
-- ❌ МЕДЛЕННО: сканирует всю таблицу
SELECT * FROM audit_logs;Правильно:
-- ✅ ЛУЧШЕ: добавьте фильтрацию или пагинацию
SELECT * FROM audit_logs
WHERE created_at > NOW() - INTERVAL '7 days'
LIMIT 1000;Критичность: Warning
Что искать: Колонки в WHERE/JOIN без индексов
Пример:
-- Если колонка 'status' не имеет индекса, это медленно
SELECT * FROM orders WHERE status = 'pending';Исправление: Добавьте индекс в схему:
CREATE INDEX idx_orders_status ON orders(status);Критичность: Warning
Что искать: Ссылки на несуществующие колонки
Пример:
-- Если 'user_name' не существует (должно быть 'username')
SELECT user_name FROM users;- Нет
UPDATEбез условияWHERE? - Нет
DELETEбез условияWHERE? - Все запросы используют параметризованные выражения?
- Нет конкатенации строк в SQL?
-
LIMITприменяется к неограниченным запросам?
- Нет ведущих wildcards в паттернах
LIKE? - Keyset пагинация вместо большого
OFFSET? - Нет декартовых произведений (отсутствующие условия JOIN)?
- Нет N+1 запросов (скалярные подзапросы в SELECT)?
- Индексы существуют для фильтруемых колонок?
- Внешние ключи определены и применяются?
- Ограничения валидируют данные на уровне базы?
- Транзакции используются для многошаговых операций?
# Установка sql-query-analyzer
cargo install sql-query-analyzer
# Анализ запросов по схеме
sql-query-analyzer analyze -s schema.sql -q queries.sql
# Вывод в SARIF для интеграции с CI
sql-query-analyzer analyze -s schema.sql -q queries.sql --format sarif
# Отключение конкретных правил при необходимости
sql-query-analyzer analyze -s schema.sql -q queries.sql --disabled-rules PERF003,STYLE001GitHub Actions с загрузкой SARIF:
- name: SQL Query Analysis
uses: RAprogramm/sql-query-analyzer@v1
with:
schema: db/schema.sql
queries: db/queries.sql
format: sarif
upload-sarif: 'true'
fail-on-error: 'true'Что искать в API handlers:
// ❌ НЕТ ВАЛИДАЦИИ
#[derive(Deserialize)]
pub struct CreateUser {
pub email: String, // Может быть "не-email"
pub age: i32, // Может быть -100
pub username: String, // Может быть пустым или 10000 символов
}
async fn create_user(Json(data): Json<CreateUser>) -> Result<Json<User>> {
// Прямо создаем без проверок!
let user = db.create_user(data).await?;
Ok(Json(user))
}Правильно:
// ✅ С ВАЛИДАЦИЕЙ
use validator::Validate;
#[derive(Deserialize, Validate)]
pub struct CreateUser {
#[validate(email)]
pub email: String,
#[validate(range(min = 0, max = 150))]
pub age: i32,
#[validate(length(min = 3, max = 50))]
#[validate(regex = "USERNAME_REGEX")]
pub username: String,
}
async fn create_user(Json(data): Json<CreateUser>) -> Result<Json<User>> {
// 1. Валидируем
data.validate()
.map_err(|_| StatusCode::BAD_REQUEST)?;
// 2. Дополнительная бизнес-валидация
if db.username_exists(&data.username).await? {
return Err(Error::UsernameTaken);
}
// 3. Создаем
let user = db.create_user(data).await?;
Ok(Json(user))
}Пример из реального кода:
// ❌ ПОТЕНЦИАЛЬНАЯ ПРОБЛЕМА
fn calculate_total(price: u64, quantity: u32) -> u64 {
price * quantity as u64 // Может переполниться!
// Если price = u64::MAX и quantity = 2, получим wraparound
}Правильно:
// ✅ БЕЗОПАСНО
fn calculate_total(price: u64, quantity: u32) -> Result<u64> {
price.checked_mul(quantity as u64)
.ok_or(Error::Overflow)
}
// Или в debug/test билдах будет panic:
#[cfg(debug_assertions)]
fn calculate_total_debug(price: u64, quantity: u32) -> u64 {
price * quantity as u64 // В debug - panic при overflow
}Что искать:
rg "tracing::.*password|log.*token|debug.*secret" --type rustПример уязвимости:
// ❌ УТЕЧКА СЕКРЕТОВ В ЛОГИ
async fn login(username: &str, password: &str) -> Result<Token> {
tracing::info!("Login attempt: {} with password {}", username, password);
// ^^^^^^^^
// Пароль попадет в логи!
// ...
}Правильно:
// ✅ БЕЗОПАСНО
async fn login(username: &str, password: &str) -> Result<Token> {
tracing::info!("Login attempt for user: {}", username);
// Пароль НЕ логируем никогда!
// Для дебага можно логировать хэш (но лучше не надо)
tracing::debug!("Password hash: {}", hash_for_debug_only(password));
// ...
}Пример уязвимости:
// ❌ РАСКРЫВАЕТ СТРУКТУРУ БД
async fn get_user(id: Uuid) -> Result<Json<User>, (StatusCode, String)> {
let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id)
.fetch_one(&db)
.await
.map_err(|e| (
StatusCode::INTERNAL_SERVER_ERROR,
format!("Database error: {}", e) // ❌ Детали БД клиенту!
))?;
Ok(Json(user))
}Правильно:
// ✅ БЕЗОПАСНО
async fn get_user(id: Uuid) -> Result<Json<User>, StatusCode> {
let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id)
.fetch_one(&db)
.await
.map_err(|e| {
// Детально логируем для разработчиков
tracing::error!("Failed to fetch user {}: {}", id, e);
// Клиенту - только общее сообщение
StatusCode::INTERNAL_SERVER_ERROR
})?;
Ok(Json(user))
}- Проверяется свежесть токенов/временных меток?
- Используется rate limiting?
- JWT токены имеют expiration?
- Проверяется подпись/HMAC?
- Нет hardcoded credentials?
- Используются современные алгоритмы (SHA-256+, AES-256)?
- Нет хардкод ключей и секретов?
- Правильная обработка ошибок (нет panic)?
- Для паролей используется Argon2/bcrypt?
- SQL запросы параметризованы?
- Нет использования format! в SQL?
- Command execution безопасен?
- Валидируются имена файлов/пути?
- Все входные данные валидируются?
- Проверяются диапазоны чисел?
- Email/URL валидируются?
- Длина строк ограничена?
- Секреты не логируются?
- Детали ошибок не раскрываются клиенту?
- Debug info только в development?
# Clippy с безопасностью
cargo clippy -- -W clippy::all -W clippy::pedantic -W clippy::nursery
# Проверка зависимостей
cargo audit
# Поиск секретов
rg "(?i)(password|secret|api_key|token).*=.*['\"]" --type rust