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

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

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

Начать курс

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

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

В этой статье мы разберем топ-10 уязвимостей Rust приложений, которые должен знать каждый разработчик. Вы узнаете не только причины их возникновения, но и получите конкретные примеры кода и методы защиты.

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

unsafe — это самый мощный, но и самый опасный инструмент Rust. Он отключает проверки компилятора, позволяя выполнять разыменование сырых указателей, вызов unsafe функций и доступ к изменяемым статическим переменным. По данным исследований, более 70% всех критических уязвимостей в Rust-проектах связаны именно с unsafe кодом.

Пример уязвимости: разыменование нулевого указателя в unsafe блоке.

fn dangerous() {
    let ptr: *const i32 = std::ptr::null();
    unsafe {
        println!("{}", *ptr); // Undefined Behavior!
    }
}

Как защититься:

  • Минимизируйте использование unsafe. Выносите его в небольшие, тщательно протестированные функции.
  • Всегда проверяйте указатели на null перед разыменованием.
  • Используйте инструменты вроде Miri для обнаружения undefined behavior.

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

Rust гарантирует отсутствие гонок данных в безопасном коде благодаря системе типов. Однако при использовании unsafe или неправильной синхронизации гонки все же возможны. Особенно опасны гонки при работе с глобальными изменяемыми статиками.

Пример уязвимости: доступ к статической изменяемой переменной без синхронизации.

static mut COUNTER: u32 = 0;

fn increment() {
    unsafe {
        COUNTER += 1; // Гонка данных в многопоточной среде
    }
}

Как защититься:

  • Используйте Mutex, RwLock или атомарные типы (AtomicU32).
  • Избегайте глобальных изменяемых состояний. Отдавайте предпочтение передаче владения через каналы (mpsc).
  • Применяйте Send и Sync трейты осознанно.

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

В безопасном Rust переполнение буфера практически невозможно — компилятор проверяет границы массивов. Но при работе с низкоуровневыми структурами в unsafe или при использовании FFI (Foreign Function Interface) переполнение становится реальной угрозой.

Пример уязвимости: копирование данных за пределы буфера через FFI.

use std::ffi::CString;

extern "C" {
    fn unsafe_copy(dest: *mut u8, src: *const u8, len: usize);
}

fn copy_vulnerable(data: &[u8]) {
    let dest = CString::new("").unwrap();
    unsafe {
        unsafe_copy(dest.into_raw(), data.as_ptr(), data.len()); // Переполнение!
    }
}

Как защититься:

  • Всегда проверяйте длину буфера на стороне Rust перед вызовом C-функций.
  • Используйте крейты-обертки для C-библиотек, которые гарантируют безопасность.
  • Применяйте санитайзеры (AddressSanitizer) при тестировании.

4. Утечки памяти (Memory Leaks)

Хотя Rust не имеет сборщика мусора, его система владения обычно предотвращает утечки. Однако утечки возможны при создании циклических ссылок с Rc или Arc, а также при использовании Box::leak или ручном управлении памятью в unsafe.

Пример уязвимости: циклическая ссылка через Rc и RefCell.

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

struct Node {
    value: i32,
    next: Option>>,
}

fn create_cycle() {
    let a = Rc::new(RefCell::new(Node { value: 1, next: None }));
    let b = Rc::new(RefCell::new(Node { value: 2, next: Some(a.clone()) }));
    a.borrow_mut().next = Some(b.clone()); // Циклическая ссылка - утечка!
}

Как защититься:

  • Используйте Weak<T> для разрыва циклических ссылок.
  • Избегайте Box::leak в production-коде.
  • Применяйте инструменты профилирования памяти (Valgrind, heaptrack).

5. Инъекции команд (Command Injection)

Даже в Rust можно допустить уязвимость инъекции, если передавать пользовательский ввод напрямую в командную оболочку. Это классическая проблема, которая не зависит от безопасности памяти.

Пример уязвимости: использование std::process::Command с неэкранированным вводом.

use std::process::Command;

fn execute_user_command(user_input: &str) {
    // Опасно! Пользователь может ввести "rm -rf /"
    let output = Command::new("sh")
        .arg("-c")
        .arg(user_input)
        .output()
        .unwrap();
}

Как защититься:

  • Никогда не передавайте пользовательский ввод в shell-команды.
  • Используйте безопасные альтернативы — разбивайте команду на аргументы.
  • Применяйте санитизацию ввода или белые спи

Блоги

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