Безопасность Rust приложений: топ-10 уязвимостей для начинающих

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Безопасность 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!("{}****                

Блоги

Рекомендаций по книгам