Безопасность Rust приложений: топ-10 уязвимостей для начинающих
Rust позиционируется как безопасный системный язык, который предотвращает целые классы уязвимостей на этапе компиляции. Однако это не означает, что Rust-приложения абсолютно неуязвимы. Начинающие разработчики часто совершают ошибки, которые могут привести к серьезным проблемам безопасности. В этой статье мы разберем топ-10 уязвимостей в Rust-приложениях, которые должен знать каждый новичок.
1. Неправильное использование unsafe кода
unsafe блоки — самая частая причина уязвимостей в Rust. Они отключают проверки безопасности, что может привести к разыменованию нулевых указателей, гонкам данных и повреждению памяти.
// Опасный пример
unsafe {
let ptr = std::ptr::null::<i32>();
println!("{}", *ptr); // UB! Segmentation fault
}
// Безопасный вариант
fn safe_access(ptr: *const i32) -> Option<i32> {
if ptr.is_null() {
return None;
}
unsafe { Some(*ptr) }
}
Рекомендация: Минимизируйте использование unsafe. Если он необходим, изолируйте его в небольшие функции и тщательно документируйте инварианты.
2. Гонки данных через Send и Sync
Rust защищает от гонок данных через систему владения, но некорректная реализация типажей Send и Sync может нарушить эту защиту.
use std::cell::UnsafeCell;
use std::sync::Arc;
struct UnsafeCounter(UnsafeCell<u32>);
// Опасная реализация
unsafe impl Send for UnsafeCounter {}
unsafe impl Sync for UnsafeCounter {}
fn main() {
let counter = Arc::new(UnsafeCounter(UnsafeCell::new(0)));
let mut handles = vec![];
for _ in 0..10 {
let c = counter.clone();
handles.push(std::thread::spawn(move || {
// Гонка данных!
unsafe { *c.0.get() += 1; }
}));
}
}
Рекомендация: Используйте Arc<Mutex<T>> или Arc<RwLock<T>> вместо ручной реализации синхронизации.
3. Небезопасная десериализация данных
При десериализации данных из внешних источников (JSON, YAML, bincode) злоумышленник может подсунуть специально сформированный payload.
use serde::Deserialize;
#[derive(Deserialize)]
struct Config {
path: String,
buffer_size: usize,
}
fn load_config(data: &str) -> Result<Config, serde_json::Error> {
// Потенциальная уязвимость: слишком большие числа
let config: Config = serde_json::from_str(data)?;
if config.buffer_size > 1_000_000 {
return Err(serde_json::Error::custom("Buffer too large"));
}
Ok(config)
}
Рекомендация: Всегда проверяйте границы значений после десериализации. Используйте #[serde(deserialize_with = "...")] для кастомных валидаторов.
4. Утечки памяти через Box<dyn Error>
Частое возвращение Box<dyn Error> может привести к утечкам памяти, особенно при работе с большими объемами данных.
// Проблемный подход
fn process_data(data: &[u8]) -> Result<(), Box<dyn std::error::Error>> {
// ... обработка
Ok(())
}
// Лучший подход
#[derive(Debug)]
enum AppError {
InvalidData(String),
IoError(std::io::Error),
}
fn process_data_safe(data: &[u8]) -> Result<(), AppError> {
// ... обработка
Ok(())
}
Рекомендация: Создавайте собственные типы ошибок с помощью thiserror или anyhow для лучшей производительности и безопасности.
5. Переполнение буфера в FFI
При взаимодействии с C-кодом через FFI (Foreign Function Interface) легко допустить классическое переполнение буфера.
extern "C" {
fn vulnerable_function(input: *const u8, len: usize);
}
fn safe_ffi_wrapper(data: &[u8]) {
unsafe {
// Опасность: нет проверки длины
vulnerable_function(data.as_ptr(), data.len());
// Безопасно: явно ограничиваем длину
let max_len = std::cmp::min(data.len(), 1024);
vulnerable_function(data.as_ptr(), max_len);
}
}
Рекомендация: Всегда проверяйте размеры буферов при вызове C-функций. Используйте std::ffi::CStr для строк.
6. Уязвимости в обработке пользовательского ввода
Инъекции команд и path traversal остаются актуальными даже в Rust.
use std::process::Command;
// Опасный пример
fn run_command(user_input: &str) {
let output = Command::new("sh")
.arg("-c")
.arg(user_input) // Инъекция команды!
.output();
}
// Безопасный пример
fn run_safe_command(input: &str) {
// Валидация: только буквы и цифры
if !input.chars().all(|c| c.is_alphanumeric()) {
eprintln!("Invalid input");
return;
}
let output = Command::new("echo")
.arg(input)
.output();
}
Рекомендация: Используйте sanitizer крейты (например, sanitize-filename) и избегайте передачи пользовательского ввода напрямую в shell.
7. Небезопасное логирование
Логирование конфиденциальных данных (пароли, токены) может привести к их утечке.
use log::info;
fn login(username: &str, password: &str) {
// Опасность: логируем пароль
info!("Login attempt: user={}, password={}", username, password);
// Безопасно: логируем только факт попытки
info!("Login attempt for user: {}", username);
// Или используем маскирование
let masked = mask_password(password);
info!("Login attempt: user={}, password={}", username, masked);
}
fn mask_password(pwd: &str) -> String {
if pwd.len() > 4 {
format!("{}****