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

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

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

Начать курс

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

Rust считается одним из самых безопасных языков программирования благодаря своей системе владения (ownership) и строгой типизации. Однако даже в Rust можно допустить ошибки, особенно при использовании unsafe блоков или неправильной работе с внешними библиотеками. В этой статье мы разберем топ-10 уязвимостей, с которыми сталкиваются начинающие разработчики на Rust, и покажем, как их избежать.

1. Небезопасное использование unsafe кода

Главная особенность Rust — это безопасность памяти без сборщика мусора. Однако ключевое слово unsafe отключает часть проверок компилятора. Новички часто злоупотребляют им, не понимая последствий.

// Опасно: разыменование сырого указателя без проверки
fn dangerous() {
    let ptr = 0x12345 as *const i32;
    unsafe {
        println!("Значение: {}", *ptr); // Может вызвать segfault
    }
}

// Безопасно: используем проверенные методы
fn safe() {
    let x = 42;
    let ptr = &x as *const i32;
    unsafe {
        if !ptr.is_null() {
            println!("Значение: {}", *ptr);
        }
    }
}

Рекомендация: Используйте unsafe только когда это действительно необходимо, и всегда проверяйте указатели на null.

2. Утечки памяти через Box::leak и Rc циклы

Хотя Rust предотвращает большинство утечек памяти, циклические ссылки с Rc или RefCell могут привести к утечкам. Также опасен Box::leak, который намеренно "забывает" память.

use std::rc::Rc;
use std::cell::RefCell;

#[derive(Debug)]
struct Node {
    value: i32,
    next: Option<Rc<RefCell<Node>>>,
}

fn create_cycle() {
    let a = Rc::new(RefCell::new(Node { value: 1, next: None }));
    let b = Rc::new(RefCell::new(Node { value: 2, next: None }));
    
    // Создаем цикл: a -> b -> a
    a.borrow_mut().next = Some(b.clone());
    b.borrow_mut().next = Some(a.clone());
    // Память никогда не освободится!
}

Решение: Используйте Weak<T> для разрыва циклических ссылок или пересмотрите архитектуру.

3. Гонки данных (Data Races) в многопоточном коде

Rust гарантирует отсутствие гонок данных в безопасном коде, но при неправильном использовании unsafe или Send/Sync трейтов можно получить проблемы.

use std::thread;
use std::sync::Arc;
use std::sync::Mutex;

// Опасно: разделяем изменяемое состояние без синхронизации
fn data_race_example() {
    let mut counter = 0;
    let mut handles = vec![];
    
    for _ in 0..10 {
        handles.push(thread::spawn(|| {
            counter += 1; // Ошибка компиляции: cannot borrow
        }));
    }
}

// Безопасно: используем Mutex
fn safe_mutex() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];
    
    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        handles.push(thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += 1;
        }));
    }
}

4. Переполнение буфера (Buffer Overflow)

В Rust переполнение буфера маловероятно в безопасном коде, но возможно при использовании unsafe или работе с FFI (Foreign Function Interface).

// Опасно: небезопасное копирование в буфер
fn unsafe_copy(src: &[u8], dst: &mut [u8]) {
    unsafe {
        std::ptr::copy_nonoverlapping(
            src.as_ptr(),
            dst.as_mut_ptr(),
            src.len() // Если src.len() > dst.len() - переполнение!
        );
    }
}

// Безопасно: используем slice::copy_from_slice
fn safe_copy(src: &[u8], dst: &mut [u8]) {
    if src.len() <= dst.len() {
        dst[..src.len()].copy_from_slice(src);
    } else {
        panic!("Buffer too small");
    }
}

5. Неправильная обработка ошибок и паники

Игнорирование ошибок или неправильное использование unwrap() может привести к панике и аварийному завершению программы.

// Плохо: игнорируем ошибку
fn read_file_bad(path: &str) -> String {
    std::fs::read_to_string(path).unwrap() // Паника, если файл не найден
}

// Хорошо: обрабатываем ошибку
fn read_file_good(path: &str) -> Result<String, std::io::Error> {
    std::fs::read_to_string(path)
}

// Еще лучше: используем expect для понятного сообщения
fn read_file_better(path: &str) -> String {
    std::fs::read_to_string(path)
        .expect(&format!("Не удалось прочитать файл: {}", path))
}

6. Уязвимости в зависимостях (Cargo)

Использование устаревших или непроверенных крейтов (crates) может привести к уязвимостям. Всегда проверяйте зависимости.

// В Cargo.toml
[dependencies]
# Плохо: фиксированная версия без обновлений
old_crate = "1.0.0"

# Хорошо: используем semver-совместимые версии
serde = { version = ">=1.0, <2.0", features = ["derive"] }
reqwest = { version = "0.12", features = ["json"] }

Инструмент: Используйте cargo audit для проверки уязвимостей в зависимостях.

7. Проблемы с временем жизни (Lifetime) и заимствованием

Неправильное управление временем жизни может привести к висячим указателям (dangling pointers) в unsafe коде или сложным ошибкам компиляции.

// Проблема: возвращаем ссылку на локальную переменную
fn bad_lifetime() -> &'static str {
    let local = String::from("hello");
    &local // Ошибка: local умирает после                

Блоги

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